Skip to content

handle bounded integers#987

Draft
sunshowers wants to merge 2 commits intomainfrom
sunshowers/spr/handle-bounded-numbers
Draft

handle bounded integers#987
sunshowers wants to merge 2 commits intomainfrom
sunshowers/spr/handle-bounded-numbers

Conversation

@sunshowers
Copy link
Contributor

@sunshowers sunshowers commented Feb 24, 2026

Track and handle bounded integers using newtypes. (Not floats yet, though.)

This is a pretty large PR, though most of it is tests -- I hope the general intent is clear.

Created using spr 1.3.6-beta.1
@sunshowers sunshowers changed the title handle bounded numbers handle bounded integers Feb 24, 2026
Created using spr 1.3.6-beta.1
@sunshowers sunshowers marked this pull request as draft February 24, 2026 20:15
@sunshowers
Copy link
Contributor Author

(marking as draft since it's not hugely important and we might go in a completely different direction)

Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a pass; thanks for looking at this

Comment on lines +1010 to +1018
// Reject contradictory bounds (min > max).
if let (Some(lo), Some(hi)) = (min, max) {
if lo > hi {
return Err(Error::InvalidSchema {
type_name: type_name.into_option(),
reason: format!("minimum ({}) is greater than maximum ({})", lo, hi,),
});
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we want to model this as an error or Never; consider this example:

{
  "allOf": [
    {
      "type": "object",
      "properties": {
        "number": {
          "type": "integer",
          "minimum": 10
        }
      }
    },
    {
      "type": "object",
      "properties": {
        "number": {
          "type": "integer",
          "maximum": 5
        }
      }
    }
  ]
}

... and imagine that the two parts of the allOf are useful schemas (referenced) in their own right and the objects contain other fields.

These two properties would merge to a value that was unsatisfiable: there is no value for the number property that is valid, but the overall object is still representable e.g. like:

pub struct Foo {
    number: Option<Never>,
}

let max_is_exact = max.map_or(true, |m| (m - *imax).abs() <= f64::EPSILON);

if min == Some(1.)
&& (max_is_exact || get_type_name(&type_name, metadata).is_none())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should usually have a type name we can apply -- we should have a test for this case

// Bounds match the format type exactly.
return Ok((TypeEntry::new_integer(ty), metadata));
} else if get_type_name(&type_name, metadata).is_some() {
// Sub-range bounds on a named type: generate a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap comments to 80?

// redundant checks which result in a warning (e.g.,
// `value < 0` on u8).
//
// For min >= 1 we could use a NonZero type, but that
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also say that adds complexity without any particular value.

We could offer conversions to NonZero types... but meh. That's not actually that useful I'd expect.

Comment on lines +1149 to +1159
if lo > hi {
return Err(Error::InvalidSchema {
type_name: type_name.into_option(),
reason: format!(
"no valid integers in range \
(effective minimum {} > \
effective maximum {} after \
rounding fractional bounds)",
lo, hi,
),
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this different than the check above?

Comment on lines +1420 to +1427
// If this is just a wrapper around a string or integer, we can derive
// some more useful traits.
if is_str || is_int {
derive_set.extend(["PartialOrd", "Ord", "PartialEq", "Eq", "Hash"]);
}
if is_int {
derive_set.insert("Copy");
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest

Suggested change
// If this is just a wrapper around a string or integer, we can derive
// some more useful traits.
if is_str || is_int {
derive_set.extend(["PartialOrd", "Ord", "PartialEq", "Eq", "Hash"]);
}
if is_int {
derive_set.insert("Copy");
}
// If this is just a wrapper around a string, we can derive
// some more useful traits.
if is_str {
derive_set.extend(["PartialOrd", "Ord", "PartialEq", "Eq", "Hash"]);
}
if is_int {
derive_set.extend(["PartialOrd", "Ord", "PartialEq", "Eq", "Hash", "Copy"]);
}

Comment on lines +342 to +345
Named integer types with sub-range bounds (e.g., a `uint8` with `maximum: 63`)
generate constrained newtypes with `TryFrom` validation. Anonymous integer
properties with bounds that don't match a standard Rust type are not yet
constrained.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truly anonymous types are rare (and arguably bugs). Note that this is under the WIP heading so we may want to rephrase with that in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants