use rust_decimal::Decimal;
use super::types::Value;
#[derive(Debug, Clone)]
pub enum Facet {
Length(usize),
MinLength(usize),
MaxLength(usize),
Pattern(super::regex::Pattern),
Enumeration(Vec<String>),
MinInclusive(Bound),
MaxInclusive(Bound),
MinExclusive(Bound),
MaxExclusive(Bound),
TotalDigits(u32),
FractionDigits(u32),
ExplicitTimezone(TimezoneRequirement),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimezoneRequirement {
Required,
Prohibited,
Optional,
}
#[derive(Debug, Clone)]
pub enum Bound {
Decimal(Decimal),
Int(i128),
Float(f32),
Double(f64),
Value(super::types::Value),
}
#[derive(Debug, Clone, Default)]
pub struct FacetSet {
pub facets: Vec<Facet>,
}
impl FacetSet {
pub fn push(&mut self, f: Facet) { self.facets.push(f); }
pub fn is_empty(&self) -> bool { self.facets.is_empty() }
}
#[derive(Debug, Clone)]
pub struct FacetViolation {
pub facet_name: &'static str,
pub detail: String,
}
impl FacetViolation {
fn new(name: &'static str, detail: impl Into<String>) -> Self {
Self { facet_name: name, detail: detail.into() }
}
}
impl Facet {
pub fn check(&self, value: &Value, lex: &str) -> Result<(), FacetViolation> {
use Facet::*;
match self {
Length(n) => check_length(*n, value, lex),
MinLength(n) => check_min_length(*n, value, lex),
MaxLength(n) => check_max_length(*n, value, lex),
Pattern(p) => {
if p.is_match(lex) {
Ok(())
} else {
Err(FacetViolation::new("pattern",
format!("value {lex:?} does not match {:?}", p.src())))
}
}
Enumeration(opts) => {
if opts.iter().any(|o| o == lex) {
Ok(())
} else {
Err(FacetViolation::new("enumeration",
format!("value {lex:?} not in enumeration")))
}
}
MinInclusive(b) => check_order(value, b, Ordering::MinInclusive),
MaxInclusive(b) => check_order(value, b, Ordering::MaxInclusive),
MinExclusive(b) => check_order(value, b, Ordering::MinExclusive),
MaxExclusive(b) => check_order(value, b, Ordering::MaxExclusive),
TotalDigits(n) => check_total_digits(*n, value, lex),
FractionDigits(n) => check_fraction_digits(*n, value, lex),
ExplicitTimezone(req) => check_explicit_timezone(*req, value),
}
}
}
fn check_explicit_timezone(
req: TimezoneRequirement, value: &Value,
) -> Result<(), FacetViolation> {
use TimezoneRequirement::*;
let has_tz = match value {
Value::DateTime(v) => v.tz_min.is_some(),
Value::Date(v) => v.tz_min.is_some(),
Value::Time(v) => v.tz_min.is_some(),
Value::GYearMonth(v) => v.tz_min.is_some(),
Value::GYear(v) => v.tz_min.is_some(),
Value::GMonthDay(v) => v.tz_min.is_some(),
Value::GDay(v) => v.tz_min.is_some(),
Value::GMonth(v) => v.tz_min.is_some(),
_ => return Ok(()),
};
match (req, has_tz) {
(Required, false) => Err(FacetViolation::new("explicitTimezone",
"value lacks a timezone offset but the type requires one")),
(Prohibited, true) => Err(FacetViolation::new("explicitTimezone",
"value carries a timezone offset but the type prohibits one")),
(Optional, _) | (Required, true) | (Prohibited, false) => Ok(()),
}
}
#[derive(Copy, Clone)]
enum Ordering { MinInclusive, MaxInclusive, MinExclusive, MaxExclusive }
fn check_order(value: &Value, bound: &Bound, op: Ordering) -> Result<(), FacetViolation> {
use std::cmp::Ordering as O;
let cmp = match (value, bound) {
(Value::Int(a), Bound::Int(b)) => a.cmp(b),
(Value::Decimal(a), Bound::Decimal(b)) => a.cmp(b),
(Value::Decimal(a), Bound::Int(b)) => a.cmp(&Decimal::from(*b)),
(Value::Int(a), Bound::Decimal(b)) => Decimal::from(*a).cmp(b),
(Value::Float(a), Bound::Float(b)) => a.partial_cmp(b).unwrap_or(O::Equal),
(Value::Double(a), Bound::Double(b)) => a.partial_cmp(b).unwrap_or(O::Equal),
(v, Bound::Value(b)) => match compare_values(v, b) {
Some(o) => o,
None => return Err(FacetViolation::new("range",
format!("incomparable values: {v:?} vs {b:?}"))),
},
_ => return Err(FacetViolation::new("range",
format!("order facet bound type doesn't match value type ({value:?} vs {bound:?})"))),
};
let ok = match op {
Ordering::MinInclusive => !matches!(cmp, O::Less),
Ordering::MaxInclusive => !matches!(cmp, O::Greater),
Ordering::MinExclusive => matches!(cmp, O::Greater),
Ordering::MaxExclusive => matches!(cmp, O::Less),
};
if ok {
Ok(())
} else {
Err(FacetViolation::new(match op {
Ordering::MinInclusive => "minInclusive",
Ordering::MaxInclusive => "maxInclusive",
Ordering::MinExclusive => "minExclusive",
Ordering::MaxExclusive => "maxExclusive",
}, format!("value {value:?} fails bound {bound:?}")))
}
}
fn check_total_digits(limit: u32, _v: &Value, lex: &str) -> Result<(), FacetViolation> {
let count = lex.chars().filter(|c| c.is_ascii_digit()).count();
let stripped = lex.trim_start_matches(['+', '-']);
let no_lead_zero: String = if let Some(dot) = stripped.find('.') {
let (int_part, frac_part) = stripped.split_at(dot);
let int_no_lead = int_part.trim_start_matches('0');
let int_no_lead = if int_no_lead.is_empty() { "" } else { int_no_lead };
format!("{int_no_lead}{frac_part}")
.chars().filter(|c| c.is_ascii_digit()).collect()
} else {
stripped.trim_start_matches('0')
.chars().filter(|c| c.is_ascii_digit()).collect()
};
let actual = no_lead_zero.len().max(if count == 0 { 0 } else { 1 });
if actual as u32 <= limit {
Ok(())
} else {
Err(FacetViolation::new("totalDigits",
format!("expected at most {limit} digits, got {actual}")))
}
}
fn check_fraction_digits(limit: u32, _v: &Value, lex: &str) -> Result<(), FacetViolation> {
let actual = match lex.find('.') {
None => 0,
Some(dot) => lex[dot + 1..].chars().filter(|c| c.is_ascii_digit()).count(),
};
if actual as u32 <= limit {
Ok(())
} else {
Err(FacetViolation::new("fractionDigits",
format!("expected at most {limit} fraction digits, got {actual}")))
}
}
fn length_unit(v: &Value, lex: &str) -> usize {
match v {
Value::Bytes(b) => b.len(),
_ => lex.chars().count(),
}
}
fn check_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
let actual = length_unit(v, lex);
if actual == n {
Ok(())
} else {
Err(FacetViolation::new("length",
format!("expected length {n}, got {actual}")))
}
}
fn check_min_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
let actual = length_unit(v, lex);
if actual >= n {
Ok(())
} else {
Err(FacetViolation::new("minLength",
format!("expected at least {n}, got {actual}")))
}
}
fn check_max_length(n: usize, v: &Value, lex: &str) -> Result<(), FacetViolation> {
let actual = length_unit(v, lex);
if actual <= n {
Ok(())
} else {
Err(FacetViolation::new("maxLength",
format!("expected at most {n}, got {actual}")))
}
}
pub fn compare_values(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
use Value::*;
match (a, b) {
(DateTime(x), DateTime(y)) => Some(x.cmp(y)),
(Date(x), Date(y)) => date_cmp(x, y),
(Time(x), Time(y)) => time_cmp(x, y),
(GYearMonth(x), GYearMonth(y)) => g_year_month_cmp(x, y),
(GYear(x), GYear(y)) => Some(x.year.cmp(&y.year)),
(GMonthDay(x), GMonthDay(y)) => Some((x.month, x.day).cmp(&(y.month, y.day))),
(GDay(x), GDay(y)) => Some(x.day.cmp(&y.day)),
(GMonth(x), GMonth(y)) => Some(x.month.cmp(&y.month)),
(Duration(x), Duration(y)) => duration_cmp(x, y),
(Int(x), Int(y)) => Some(x.cmp(y)),
(Decimal(x), Decimal(y)) => Some(x.cmp(y)),
(Decimal(x), Int(y)) => Some(x.cmp(&rust_decimal::Decimal::from(*y))),
(Int(x), Decimal(y)) => Some(rust_decimal::Decimal::from(*x).cmp(y)),
(Float(x), Float(y)) => x.partial_cmp(y),
(Double(x), Double(y)) => x.partial_cmp(y),
_ => None,
}
}
fn date_cmp(a: &super::datetime::XsdDate, b: &super::datetime::XsdDate)
-> Option<std::cmp::Ordering>
{
let to_dt = |d: &super::datetime::XsdDate| super::datetime::XsdDateTime {
year: d.year, month: d.month, day: d.day,
hour: 0, minute: 0, second: 0, nanos: 0,
tz_min: d.tz_min,
};
Some(to_dt(a).cmp(&to_dt(b)))
}
fn time_cmp(a: &super::datetime::XsdTime, b: &super::datetime::XsdTime)
-> Option<std::cmp::Ordering>
{
let to_utc_seconds = |t: &super::datetime::XsdTime| -> i64 {
let raw = (t.hour as i64) * 3600 + (t.minute as i64) * 60 + (t.second as i64);
raw - (t.tz_min.unwrap_or(0) as i64) * 60
};
match (a.tz_min, b.tz_min) {
(Some(_), Some(_)) | (None, None) => {
let ord = to_utc_seconds(a).cmp(&to_utc_seconds(b));
if ord == std::cmp::Ordering::Equal {
Some(a.nanos.cmp(&b.nanos))
} else {
Some(ord)
}
}
_ => None,
}
}
fn g_year_month_cmp(a: &super::datetime::XsdGYearMonth, b: &super::datetime::XsdGYearMonth)
-> Option<std::cmp::Ordering>
{
Some((a.year, a.month).cmp(&(b.year, b.month)))
}
fn duration_cmp(a: &super::datetime::XsdDuration, b: &super::datetime::XsdDuration)
-> Option<std::cmp::Ordering>
{
use std::cmp::Ordering::*;
let m = a.months.cmp(&b.months);
let s = (a.seconds as i128 * 1_000_000_000 + a.nanos as i128)
.cmp(&(b.seconds as i128 * 1_000_000_000 + b.nanos as i128));
match (m, s) {
(Equal, s) => Some(s),
(m, Equal) => Some(m),
(Less, Less) => Some(Less),
(Greater, Greater) => Some(Greater),
_ => {
const SECS_PER_MONTH: i128 = 30 * 86_400;
let an = a.months as i128 * SECS_PER_MONTH * 1_000_000_000
+ a.seconds as i128 * 1_000_000_000
+ a.nanos as i128;
let bn = b.months as i128 * SECS_PER_MONTH * 1_000_000_000
+ b.seconds as i128 * 1_000_000_000
+ b.nanos as i128;
Some(an.cmp(&bn))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::types::{BuiltinType, SimpleType};
fn with_facets(b: BuiltinType, fs: Vec<Facet>) -> SimpleType {
let mut t = SimpleType::of_builtin(b);
for f in fs { t.facets.push(f); }
t
}
#[test]
fn length_facet_passes_and_fails() {
let t = with_facets(BuiltinType::String, vec![Facet::Length(3)]);
assert!(t.validate("abc").is_ok());
assert!(t.validate("abcd").is_err());
assert!(t.validate("ab").is_err());
}
#[test]
fn min_max_length_combo() {
let t = with_facets(BuiltinType::String, vec![
Facet::MinLength(2), Facet::MaxLength(4),
]);
assert!(t.validate("ab").is_ok());
assert!(t.validate("abcd").is_ok());
assert!(t.validate("a").is_err());
assert!(t.validate("abcde").is_err());
}
#[test]
fn enumeration_facet() {
let t = with_facets(BuiltinType::Token, vec![
Facet::Enumeration(vec!["red".into(), "green".into(), "blue".into()]),
]);
assert!(t.validate("red").is_ok());
assert!(t.validate("yellow").is_err());
}
#[test]
fn pattern_facet_matches() {
let p = super::super::regex::Pattern::compile(r"\d{3}-\d{4}").unwrap();
let t = with_facets(BuiltinType::String, vec![Facet::Pattern(p)]);
assert!(t.validate("555-1234").is_ok());
assert!(t.validate("12-3456").is_err());
}
#[test]
fn length_uses_unicode_codepoints_not_bytes() {
let t = with_facets(BuiltinType::String, vec![Facet::Length(2)]);
assert!(t.validate("中文").is_ok());
assert!(t.validate("ab").is_ok());
}
}