use serde_json::{Map, Value};
use super::schema::resolved;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Bounds {
pub pattern: Option<String>,
pub minimum: Option<i64>,
pub maximum: Option<i64>,
pub unsatisfiable: bool,
}
pub(super) fn bounds_of(schema: &Value, defs: &Map<String, Value>) -> Bounds {
let schema = resolved(schema, defs);
let minimum = schema.get("minimum").and_then(Value::as_f64).map(f64::ceil);
let maximum = schema
.get("maximum")
.and_then(Value::as_f64)
.map(f64::floor);
let (floor, ceiling) = (minimum.and_then(as_bound), maximum.and_then(as_bound));
Bounds {
pattern: schema
.get("pattern")
.and_then(Value::as_str)
.map(str::to_owned),
minimum: floor,
maximum: ceiling,
unsatisfiable: minimum.is_some_and(|value| value > i64::MAX as f64)
|| maximum.is_some_and(|value| value < i64::MIN as f64)
|| matches!((floor, ceiling), (Some(low), Some(high)) if low > high),
}
}
fn as_bound(value: f64) -> Option<i64> {
#[expect(
clippy::cast_possible_truncation,
reason = "guarded by the range check, and a float-to-int cast saturates"
)]
(value >= i64::MIN as f64 && value <= i64::MAX as f64).then_some(value as i64)
}
#[cfg(test)]
mod tests {
use super::super::FieldSet;
use super::super::fixtures::{props, real_field_set};
use super::*;
use serde_json::json;
#[test]
fn a_pattern_is_read_through_every_hop_a_schema_can_put_it_behind() {
let defs = props(json!({
"UpDuration": { "type": "string", "pattern": r"^\d+(ms|h|m|s)?$" },
}));
let p = props(json!({
"inline": { "type": "string", "pattern": r"^\d+(ms|h|m|s)?$" },
"one_hop": { "$ref": "#/$defs/UpDuration" },
"two_hops": { "anyOf": [{ "$ref": "#/$defs/UpDuration" }, { "type": "null" }] },
"plain": { "type": "string" },
}));
let set = FieldSet::from_properties(&p, &defs, &[]);
for key in ["inline", "one_hop", "two_hops"] {
assert_eq!(
set.by_key(key).unwrap().bounds.pattern.as_deref(),
Some(r"^\d+(ms|h|m|s)?$"),
"{key}"
);
}
assert_eq!(set.by_key("plain").unwrap().bounds, Bounds::default());
}
#[test]
fn integer_bounds_are_read_and_a_fractional_one_is_normalised() {
let p = props(json!({
"keep": { "type": "integer", "minimum": 1 },
"ratio": { "type": "integer", "minimum": 0.5, "maximum": 2.5 },
"both": { "type": "integer", "minimum": -3, "maximum": 64 },
"plain": { "type": "integer" },
}));
let set = FieldSet::from_properties(&p, &Default::default(), &[]);
assert_eq!(set.by_key("keep").unwrap().bounds.minimum, Some(1));
assert_eq!(set.by_key("both").unwrap().bounds.minimum, Some(-3));
assert_eq!(set.by_key("both").unwrap().bounds.maximum, Some(64));
let ratio = &set.by_key("ratio").unwrap().bounds;
assert_eq!(ratio.minimum, Some(1), "0.5 rounds up, so 0 is refused");
assert_eq!(ratio.maximum, Some(2), "2.5 rounds down, so 3 is refused");
assert!(!ratio.unsatisfiable);
assert_eq!(set.by_key("plain").unwrap().bounds, Bounds::default());
}
#[test]
fn a_range_with_nothing_in_it_is_recorded_as_such() {
let p = props(json!({
"inverted": { "type": "integer", "minimum": 3, "maximum": 1 },
"inverted_after_rounding": {
"type": "integer", "minimum": 1.2, "maximum": 1.8,
},
"past_i64": { "type": "integer", "minimum": 1e30 },
"under_i64": { "type": "integer", "maximum": -1e30 },
"wide_open": { "type": "integer", "minimum": -1e30, "maximum": 1e30 },
}));
let set = FieldSet::from_properties(&p, &Default::default(), &[]);
for key in [
"inverted",
"inverted_after_rounding",
"past_i64",
"under_i64",
] {
assert!(set.by_key(key).unwrap().bounds.unsatisfiable, "{key}");
}
let open = &set.by_key("wide_open").unwrap().bounds;
assert!(!open.unsatisfiable);
assert_eq!((open.minimum, open.maximum), (None, None));
}
#[test]
fn the_real_schema_publishes_the_bounds_the_gate_reads() {
let set = real_field_set();
assert_eq!(
set.by_key("max_memory").unwrap().bounds.pattern.as_deref(),
Some(r"^\d+(G|M|K)?$")
);
assert_eq!(
set.by_key("kill_timeout")
.unwrap()
.bounds
.pattern
.as_deref(),
Some(r"^\d+(ms|h|m|s)?$")
);
assert_eq!(set.by_key("max_restarts").unwrap().bounds.minimum, Some(0));
assert_eq!(
set.by_key("cwd").unwrap().bounds,
Bounds::default(),
"a plain path field is held to nothing"
);
}
}