use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RefinedSort {
pub base: Arc<str>,
pub constraints: Vec<RefinementConstraint>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RefinementConstraint {
pub kind: Arc<str>,
pub value: Arc<str>,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum RefinementError {
#[error("constraint {kind} violated: value {value} not in range")]
NumericViolation {
kind: String,
value: String,
},
#[error("format constraint {kind} violated")]
FormatViolation {
kind: String,
},
}
impl RefinedSort {
#[must_use]
pub fn from_constraints(base: &str, constraints: &[(String, String)]) -> Self {
Self {
base: Arc::from(base),
constraints: constraints
.iter()
.map(|(k, v)| RefinementConstraint {
kind: Arc::from(k.as_str()),
value: Arc::from(v.as_str()),
})
.collect(),
}
}
#[must_use]
pub fn subsort_of(&self, other: &Self) -> bool {
if self.base != other.base {
return false;
}
for other_c in &other.constraints {
let dominated = self.constraints.iter().any(|self_c| {
self_c.kind == other_c.kind
&& constraint_tighter(&self_c.kind, &self_c.value, &other_c.value)
});
if !dominated {
return false;
}
}
if self.constraints.len() == other.constraints.len()
&& self.constraints.iter().all(|sc| {
other
.constraints
.iter()
.any(|oc| sc.kind == oc.kind && sc.value == oc.value)
})
{
return false;
}
true
}
}
fn constraint_tighter(kind: &str, self_val: &str, other_val: &str) -> bool {
let parse_both = || -> Option<(f64, f64)> {
let s = self_val.parse::<f64>().ok()?;
let o = other_val.parse::<f64>().ok()?;
Some((s, o))
};
match kind {
"maxLength" | "maximum" | "exclusiveMaximum" | "maxItems" | "maxProperties" => {
parse_both().is_some_and(|(s, o)| s <= o)
}
"minLength" | "minimum" | "exclusiveMinimum" | "minItems" | "minProperties" => {
parse_both().is_some_and(|(s, o)| s >= o)
}
_ => self_val == other_val,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn subsort_same_base_tighter_max() {
let narrow = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
let wide = RefinedSort::from_constraints("string", &[("maxLength".into(), "300".into())]);
assert!(narrow.subsort_of(&wide));
assert!(!wide.subsort_of(&narrow));
}
#[test]
fn subsort_same_base_tighter_min() {
let narrow = RefinedSort::from_constraints("int", &[("minimum".into(), "10".into())]);
let wide = RefinedSort::from_constraints("int", &[("minimum".into(), "0".into())]);
assert!(narrow.subsort_of(&wide));
assert!(!wide.subsort_of(&narrow));
}
#[test]
fn subsort_different_base_returns_false() {
let a = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
let b = RefinedSort::from_constraints("int", &[("maxLength".into(), "200".into())]);
assert!(!a.subsort_of(&b));
}
#[test]
fn identical_constraints_not_strict_subsort() {
let a = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
let b = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
assert!(!a.subsort_of(&b));
}
#[test]
fn additional_constraint_makes_subsort() {
let narrow = RefinedSort::from_constraints(
"string",
&[
("maxLength".into(), "100".into()),
("minLength".into(), "5".into()),
],
);
let wide = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
assert!(narrow.subsort_of(&wide));
}
#[test]
fn from_constraints_round_trip() {
let sort = RefinedSort::from_constraints(
"string",
&[
("maxLength".into(), "300".into()),
("format".into(), "uri".into()),
],
);
assert_eq!(&*sort.base, "string");
assert_eq!(sort.constraints.len(), 2);
assert_eq!(&*sort.constraints[0].kind, "maxLength");
assert_eq!(&*sort.constraints[0].value, "300");
}
}