panproto_gat/
refinement.rs1use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct RefinedSort {
14 pub base: Arc<str>,
16 pub constraints: Vec<RefinementConstraint>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct RefinementConstraint {
23 pub kind: Arc<str>,
25 pub value: Arc<str>,
27}
28
29#[derive(Debug, Clone, thiserror::Error)]
31pub enum RefinementError {
32 #[error("constraint {kind} violated: value {value} not in range")]
34 NumericViolation {
35 kind: String,
37 value: String,
39 },
40 #[error("format constraint {kind} violated")]
42 FormatViolation {
43 kind: String,
45 },
46}
47
48impl RefinedSort {
49 #[must_use]
51 pub fn from_constraints(base: &str, constraints: &[(String, String)]) -> Self {
52 Self {
53 base: Arc::from(base),
54 constraints: constraints
55 .iter()
56 .map(|(k, v)| RefinementConstraint {
57 kind: Arc::from(k.as_str()),
58 value: Arc::from(v.as_str()),
59 })
60 .collect(),
61 }
62 }
63
64 #[must_use]
70 pub fn subsort_of(&self, other: &Self) -> bool {
71 if self.base != other.base {
72 return false;
73 }
74
75 for other_c in &other.constraints {
78 let dominated = self.constraints.iter().any(|self_c| {
79 self_c.kind == other_c.kind
80 && constraint_tighter(&self_c.kind, &self_c.value, &other_c.value)
81 });
82 if !dominated {
83 return false;
84 }
85 }
86
87 if self.constraints.len() == other.constraints.len()
90 && self.constraints.iter().all(|sc| {
91 other
92 .constraints
93 .iter()
94 .any(|oc| sc.kind == oc.kind && sc.value == oc.value)
95 })
96 {
97 return false;
98 }
99
100 true
101 }
102}
103
104fn constraint_tighter(kind: &str, self_val: &str, other_val: &str) -> bool {
107 let parse_both = || -> Option<(f64, f64)> {
108 let s = self_val.parse::<f64>().ok()?;
109 let o = other_val.parse::<f64>().ok()?;
110 Some((s, o))
111 };
112
113 match kind {
114 "maxLength" | "maximum" | "exclusiveMaximum" | "maxItems" | "maxProperties" => {
116 parse_both().is_some_and(|(s, o)| s <= o)
117 }
118 "minLength" | "minimum" | "exclusiveMinimum" | "minItems" | "minProperties" => {
120 parse_both().is_some_and(|(s, o)| s >= o)
121 }
122 _ => self_val == other_val,
124 }
125}
126
127#[cfg(test)]
128#[allow(clippy::unwrap_used, clippy::expect_used)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn subsort_same_base_tighter_max() {
134 let narrow = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
135 let wide = RefinedSort::from_constraints("string", &[("maxLength".into(), "300".into())]);
136 assert!(narrow.subsort_of(&wide));
137 assert!(!wide.subsort_of(&narrow));
138 }
139
140 #[test]
141 fn subsort_same_base_tighter_min() {
142 let narrow = RefinedSort::from_constraints("int", &[("minimum".into(), "10".into())]);
143 let wide = RefinedSort::from_constraints("int", &[("minimum".into(), "0".into())]);
144 assert!(narrow.subsort_of(&wide));
145 assert!(!wide.subsort_of(&narrow));
146 }
147
148 #[test]
149 fn subsort_different_base_returns_false() {
150 let a = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
151 let b = RefinedSort::from_constraints("int", &[("maxLength".into(), "200".into())]);
152 assert!(!a.subsort_of(&b));
153 }
154
155 #[test]
156 fn identical_constraints_not_strict_subsort() {
157 let a = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
158 let b = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
159 assert!(!a.subsort_of(&b));
160 }
161
162 #[test]
163 fn additional_constraint_makes_subsort() {
164 let narrow = RefinedSort::from_constraints(
165 "string",
166 &[
167 ("maxLength".into(), "100".into()),
168 ("minLength".into(), "5".into()),
169 ],
170 );
171 let wide = RefinedSort::from_constraints("string", &[("maxLength".into(), "100".into())]);
172 assert!(narrow.subsort_of(&wide));
173 }
174
175 #[test]
176 fn from_constraints_round_trip() {
177 let sort = RefinedSort::from_constraints(
178 "string",
179 &[
180 ("maxLength".into(), "300".into()),
181 ("format".into(), "uri".into()),
182 ],
183 );
184 assert_eq!(&*sort.base, "string");
185 assert_eq!(sort.constraints.len(), 2);
186 assert_eq!(&*sort.constraints[0].kind, "maxLength");
187 assert_eq!(&*sort.constraints[0].value, "300");
188 }
189}