1use rust_decimal::Decimal;
12
13use super::types::Value;
14
15#[derive(Debug, Clone)]
21pub enum Facet {
22 Length(usize),
23 MinLength(usize),
24 MaxLength(usize),
25 Pattern(super::regex::Pattern),
27 Enumeration(Vec<String>),
28 MinInclusive(Bound),
32 MaxInclusive(Bound),
33 MinExclusive(Bound),
34 MaxExclusive(Bound),
35 TotalDigits(u32),
36 FractionDigits(u32),
37 ExplicitTimezone(TimezoneRequirement),
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum TimezoneRequirement {
48 Required,
49 Prohibited,
50 Optional,
51}
52
53#[derive(Debug, Clone)]
61pub enum Bound {
62 Decimal(Decimal),
63 Int(i128),
64 Float(f32),
65 Double(f64),
66 Value(super::types::Value),
67}
68
69#[derive(Debug, Clone, Default)]
72pub struct FacetSet {
73 pub facets: Vec<Facet>,
74}
75
76impl FacetSet {
77 pub fn push(&mut self, f: Facet) { self.facets.push(f); }
78 pub fn is_empty(&self) -> bool { self.facets.is_empty() }
79}
80
81#[derive(Debug, Clone)]
85pub struct FacetViolation {
86 pub facet_name: &'static str,
87 pub detail: String,
88}
89
90impl FacetViolation {
91 fn new(name: &'static str, detail: impl Into<String>) -> Self {
92 Self { facet_name: name, detail: detail.into() }
93 }
94}
95
96impl Facet {
99 pub fn check(&self, value: &Value, lex: &str) -> Result<(), FacetViolation> {
103 use Facet::*;
104 match self {
105 Length(n) => check_length(*n, value, lex),
106 MinLength(n) => check_min_length(*n, value, lex),
107 MaxLength(n) => check_max_length(*n, value, lex),
108 Pattern(p) => {
109 if p.is_match(lex) {
110 Ok(())
111 } else {
112 Err(FacetViolation::new("pattern",
113 format!("value {lex:?} does not match {:?}", p.src())))
114 }
115 }
116 Enumeration(opts) => {
117 if opts.iter().any(|o| o == lex) {
122 Ok(())
123 } else {
124 Err(FacetViolation::new("enumeration",
125 format!("value {lex:?} not in enumeration")))
126 }
127 }
128 MinInclusive(b) => check_order(value, b, Ordering::MinInclusive),
129 MaxInclusive(b) => check_order(value, b, Ordering::MaxInclusive),
130 MinExclusive(b) => check_order(value, b, Ordering::MinExclusive),
131 MaxExclusive(b) => check_order(value, b, Ordering::MaxExclusive),
132 TotalDigits(n) => check_total_digits(*n, value, lex),
133 FractionDigits(n) => check_fraction_digits(*n, value, lex),
134 ExplicitTimezone(req) => check_explicit_timezone(*req, value),
135 }
136 }
137}
138
139fn check_explicit_timezone(
144 req: TimezoneRequirement, value: &Value,
145) -> Result<(), FacetViolation> {
146 use TimezoneRequirement::*;
147 let has_tz = match value {
148 Value::DateTime(v) => v.tz_min.is_some(),
149 Value::Date(v) => v.tz_min.is_some(),
150 Value::Time(v) => v.tz_min.is_some(),
151 Value::GYearMonth(v) => v.tz_min.is_some(),
152 Value::GYear(v) => v.tz_min.is_some(),
153 Value::GMonthDay(v) => v.tz_min.is_some(),
154 Value::GDay(v) => v.tz_min.is_some(),
155 Value::GMonth(v) => v.tz_min.is_some(),
156 _ => return Ok(()),
160 };
161 match (req, has_tz) {
162 (Required, false) => Err(FacetViolation::new("explicitTimezone",
163 "value lacks a timezone offset but the type requires one")),
164 (Prohibited, true) => Err(FacetViolation::new("explicitTimezone",
165 "value carries a timezone offset but the type prohibits one")),
166 (Optional, _) | (Required, true) | (Prohibited, false) => Ok(()),
167 }
168}
169
170#[derive(Copy, Clone)]
171enum Ordering { MinInclusive, MaxInclusive, MinExclusive, MaxExclusive }
172
173fn check_order(value: &Value, bound: &Bound, op: Ordering) -> Result<(), FacetViolation> {
174 use std::cmp::Ordering as O;
175
176 let cmp = match (value, bound) {
177 (Value::Int(a), Bound::Int(b)) => a.cmp(b),
178 (Value::Decimal(a), Bound::Decimal(b)) => a.cmp(b),
179 (Value::Decimal(a), Bound::Int(b)) => a.cmp(&Decimal::from(*b)),
180 (Value::Int(a), Bound::Decimal(b)) => Decimal::from(*a).cmp(b),
181 (Value::Float(a), Bound::Float(b)) => a.partial_cmp(b).unwrap_or(O::Equal),
182 (Value::Double(a), Bound::Double(b)) => a.partial_cmp(b).unwrap_or(O::Equal),
183 (v, Bound::Value(b)) => match compare_values(v, b) {
184 Some(o) => o,
185 None => return Err(FacetViolation::new("range",
186 format!("incomparable values: {v:?} vs {b:?}"))),
187 },
188 _ => return Err(FacetViolation::new("range",
189 format!("order facet bound type doesn't match value type ({value:?} vs {bound:?})"))),
190 };
191 let ok = match op {
192 Ordering::MinInclusive => !matches!(cmp, O::Less),
193 Ordering::MaxInclusive => !matches!(cmp, O::Greater),
194 Ordering::MinExclusive => matches!(cmp, O::Greater),
195 Ordering::MaxExclusive => matches!(cmp, O::Less),
196 };
197 if ok {
198 Ok(())
199 } else {
200 Err(FacetViolation::new(match op {
201 Ordering::MinInclusive => "minInclusive",
202 Ordering::MaxInclusive => "maxInclusive",
203 Ordering::MinExclusive => "minExclusive",
204 Ordering::MaxExclusive => "maxExclusive",
205 }, format!("value {value:?} fails bound {bound:?}")))
206 }
207}
208
209fn check_total_digits(limit: u32, _v: &Value, lex: &str) -> Result<(), FacetViolation> {
212 let count = lex.chars().filter(|c| c.is_ascii_digit()).count();
213 let stripped = lex.trim_start_matches(['+', '-']);
214 let no_lead_zero: String = if let Some(dot) = stripped.find('.') {
215 let (int_part, frac_part) = stripped.split_at(dot);
216 let int_no_lead = int_part.trim_start_matches('0');
217 let int_no_lead = if int_no_lead.is_empty() { "" } else { int_no_lead };
218 format!("{int_no_lead}{frac_part}")
219 .chars().filter(|c| c.is_ascii_digit()).collect()
220 } else {
221 stripped.trim_start_matches('0')
222 .chars().filter(|c| c.is_ascii_digit()).collect()
223 };
224 let actual = no_lead_zero.len().max(if count == 0 { 0 } else { 1 });
225 if actual as u32 <= limit {
226 Ok(())
227 } else {
228 Err(FacetViolation::new("totalDigits",
229 format!("expected at most {limit} digits, got {actual}")))
230 }
231}
232
233fn check_fraction_digits(limit: u32, _v: &Value, lex: &str) -> Result<(), FacetViolation> {
235 let actual = match lex.find('.') {
236 None => 0,
237 Some(dot) => lex[dot + 1..].chars().filter(|c| c.is_ascii_digit()).count(),
238 };
239 if actual as u32 <= limit {
240 Ok(())
241 } else {
242 Err(FacetViolation::new("fractionDigits",
243 format!("expected at most {limit} fraction digits, got {actual}")))
244 }
245}
246
247fn length_unit(v: &Value, lex: &str) -> usize {
253 match v {
254 Value::Bytes(b) => b.len(),
255 _ => lex.chars().count(),
256 }
257}
258
259fn check_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
260 let actual = length_unit(v, lex);
261 if actual == n {
262 Ok(())
263 } else {
264 Err(FacetViolation::new("length",
265 format!("expected length {n}, got {actual}")))
266 }
267}
268
269fn check_min_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
270 let actual = length_unit(v, lex);
271 if actual >= n {
272 Ok(())
273 } else {
274 Err(FacetViolation::new("minLength",
275 format!("expected at least {n}, got {actual}")))
276 }
277}
278
279fn check_max_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
280 let actual = length_unit(v, lex);
281 if actual <= n {
282 Ok(())
283 } else {
284 Err(FacetViolation::new("maxLength",
285 format!("expected at most {n}, got {actual}")))
286 }
287}
288
289pub fn compare_values(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
301 use Value::*;
302 match (a, b) {
303 (DateTime(x), DateTime(y)) => Some(x.cmp(y)),
304 (Date(x), Date(y)) => date_cmp(x, y),
305 (Time(x), Time(y)) => time_cmp(x, y),
306 (GYearMonth(x), GYearMonth(y)) => g_year_month_cmp(x, y),
307 (GYear(x), GYear(y)) => Some(x.year.cmp(&y.year)),
308 (GMonthDay(x), GMonthDay(y)) => Some((x.month, x.day).cmp(&(y.month, y.day))),
309 (GDay(x), GDay(y)) => Some(x.day.cmp(&y.day)),
310 (GMonth(x), GMonth(y)) => Some(x.month.cmp(&y.month)),
311 (Duration(x), Duration(y)) => duration_cmp(x, y),
312 (Int(x), Int(y)) => Some(x.cmp(y)),
316 (Decimal(x), Decimal(y)) => Some(x.cmp(y)),
317 (Decimal(x), Int(y)) => Some(x.cmp(&rust_decimal::Decimal::from(*y))),
318 (Int(x), Decimal(y)) => Some(rust_decimal::Decimal::from(*x).cmp(y)),
319 (Float(x), Float(y)) => x.partial_cmp(y),
320 (Double(x), Double(y)) => x.partial_cmp(y),
321 _ => None,
322 }
323}
324
325fn date_cmp(a: &super::datetime::XsdDate, b: &super::datetime::XsdDate)
329 -> Option<std::cmp::Ordering>
330{
331 let to_dt = |d: &super::datetime::XsdDate| super::datetime::XsdDateTime {
332 year: d.year, month: d.month, day: d.day,
333 hour: 0, minute: 0, second: 0, nanos: 0,
334 tz_min: d.tz_min,
335 };
336 Some(to_dt(a).cmp(&to_dt(b)))
337}
338
339fn time_cmp(a: &super::datetime::XsdTime, b: &super::datetime::XsdTime)
343 -> Option<std::cmp::Ordering>
344{
345 let to_utc_seconds = |t: &super::datetime::XsdTime| -> i64 {
346 let raw = (t.hour as i64) * 3600 + (t.minute as i64) * 60 + (t.second as i64);
347 raw - (t.tz_min.unwrap_or(0) as i64) * 60
348 };
349 match (a.tz_min, b.tz_min) {
350 (Some(_), Some(_)) | (None, None) => {
351 let ord = to_utc_seconds(a).cmp(&to_utc_seconds(b));
352 if ord == std::cmp::Ordering::Equal {
353 Some(a.nanos.cmp(&b.nanos))
354 } else {
355 Some(ord)
356 }
357 }
358 _ => None,
359 }
360}
361
362fn g_year_month_cmp(a: &super::datetime::XsdGYearMonth, b: &super::datetime::XsdGYearMonth)
363 -> Option<std::cmp::Ordering>
364{
365 Some((a.year, a.month).cmp(&(b.year, b.month)))
366}
367
368fn duration_cmp(a: &super::datetime::XsdDuration, b: &super::datetime::XsdDuration)
373 -> Option<std::cmp::Ordering>
374{
375 use std::cmp::Ordering::*;
376 let m = a.months.cmp(&b.months);
377 let s = (a.seconds as i128 * 1_000_000_000 + a.nanos as i128)
378 .cmp(&(b.seconds as i128 * 1_000_000_000 + b.nanos as i128));
379 match (m, s) {
380 (Equal, s) => Some(s),
381 (m, Equal) => Some(m),
382 (Less, Less) => Some(Less),
383 (Greater, Greater) => Some(Greater),
384 _ => {
390 const SECS_PER_MONTH: i128 = 30 * 86_400;
391 let an = a.months as i128 * SECS_PER_MONTH * 1_000_000_000
392 + a.seconds as i128 * 1_000_000_000
393 + a.nanos as i128;
394 let bn = b.months as i128 * SECS_PER_MONTH * 1_000_000_000
395 + b.seconds as i128 * 1_000_000_000
396 + b.nanos as i128;
397 Some(an.cmp(&bn))
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use super::super::types::{BuiltinType, SimpleType};
406
407 fn with_facets(b: BuiltinType, fs: Vec<Facet>) -> SimpleType {
408 let mut t = SimpleType::of_builtin(b);
409 for f in fs { t.facets.push(f); }
410 t
411 }
412
413 #[test]
414 fn length_facet_passes_and_fails() {
415 let t = with_facets(BuiltinType::String, vec![Facet::Length(3)]);
416 assert!(t.validate("abc").is_ok());
417 assert!(t.validate("abcd").is_err());
418 assert!(t.validate("ab").is_err());
419 }
420
421 #[test]
422 fn min_max_length_combo() {
423 let t = with_facets(BuiltinType::String, vec![
424 Facet::MinLength(2), Facet::MaxLength(4),
425 ]);
426 assert!(t.validate("ab").is_ok());
427 assert!(t.validate("abcd").is_ok());
428 assert!(t.validate("a").is_err());
429 assert!(t.validate("abcde").is_err());
430 }
431
432 #[test]
433 fn enumeration_facet() {
434 let t = with_facets(BuiltinType::Token, vec![
435 Facet::Enumeration(vec!["red".into(), "green".into(), "blue".into()]),
436 ]);
437 assert!(t.validate("red").is_ok());
438 assert!(t.validate("yellow").is_err());
439 }
440
441 #[test]
442 fn pattern_facet_matches() {
443 let p = super::super::regex::Pattern::compile(r"\d{3}-\d{4}").unwrap();
444 let t = with_facets(BuiltinType::String, vec![Facet::Pattern(p)]);
445 assert!(t.validate("555-1234").is_ok());
446 assert!(t.validate("12-3456").is_err());
447 }
448
449 #[test]
450 fn length_uses_unicode_codepoints_not_bytes() {
451 let t = with_facets(BuiltinType::String, vec![Facet::Length(2)]);
453 assert!(t.validate("中文").is_ok());
454 assert!(t.validate("ab").is_ok());
455 }
456}