use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
use serde_json::{Map, Value};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mismatch {
pub at: String,
pub expected: String,
}
pub type Verdict = Result<(), Mismatch>;
type Check = Rc<dyn Fn(&str, &Value) -> Verdict>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Datum {
Bytes(Option<usize>),
Int { signed: bool, bits: u16 },
Other,
}
#[derive(Clone)]
pub struct Judgement {
check: Check,
members: Option<Vec<Member>>,
absent_ok: bool,
datum: Datum,
}
impl Judgement {
pub fn holds(&self, value: &Value) -> Verdict {
(self.check)("", value)
}
fn of(datum: Datum, check: impl Fn(&str, &Value) -> Verdict + 'static) -> Self {
Self { check: Rc::new(check), members: None, absent_ok: false, datum }
}
fn leaf(check: impl Fn(&str, &Value) -> Verdict + 'static) -> Self {
Self::of(Datum::Other, check)
}
}
#[derive(Clone)]
pub struct Member {
name: String,
shape: Judgement,
}
#[derive(Debug, Clone, Copy)]
pub struct Judge {
spelling: Spelling,
}
impl Judge {
pub fn new(spelling: Spelling) -> Self {
Self { spelling }
}
}
fn wrong(at: &str, expected: impl Into<String>) -> Verdict {
Err(Mismatch { at: if at.is_empty() { ".".into() } else { at.into() }, expected: expected.into() })
}
fn below(at: &str, name: &str) -> String {
if at.is_empty() { name.into() } else { format!("{at}.{name}") }
}
fn hex_bytes(text: &str) -> Option<usize> {
let digits = text.strip_prefix("0x")?;
(digits.len() % 2 == 0 && digits.chars().all(|c| c.is_ascii_hexdigit())).then_some(digits.len() / 2)
}
impl Sorts for Judge {
type Ty = Judgement;
type Field = Vec<Member>;
}
impl ShapeAlg for Judge {
fn truth(&self) -> Judgement {
Judgement::leaf(|at, v| if v.is_boolean() { Ok(()) } else { wrong(at, "a boolean") })
}
fn unit(&self) -> Judgement {
Judgement::leaf(|at, v| if v.is_null() { Ok(()) } else { wrong(at, "null") })
}
fn text(&self) -> Judgement {
Judgement::leaf(|at, v| if v.is_string() { Ok(()) } else { wrong(at, "text") })
}
fn literal(&self, text: &str) -> Judgement {
let expected = text.to_owned();
Judgement::leaf(move |at, v| {
if v.as_str() == Some(expected.as_str()) { Ok(()) } else { wrong(at, format!("\"{expected}\"")) }
})
}
fn name_word(&self, words: Words<'_>) -> Judgement {
let spelled = self.spelling.spell(words);
Judgement::leaf(
move |at, v| {
if v.as_str() == Some(spelled.as_str()) { Ok(()) } else { wrong(at, format!("\"{spelled}\"")) }
},
)
}
fn int(&self, signed: bool, bits: u16) -> Judgement {
Judgement::of(Datum::Int { signed, bits }, move |at, v| {
let fits = match v {
Value::Number(n) if signed => n.as_i64().is_some(),
Value::Number(n) => n.as_u64().is_some(),
_ => false,
};
if fits { Ok(()) } else { wrong(at, format!("an integer of {bits} bits")) }
})
}
fn float(&self, bits: u16) -> Judgement {
Judgement::leaf(move |at, v| if v.is_number() { Ok(()) } else { wrong(at, format!("a number of {bits} bits")) })
}
fn bytes(&self, len: Option<usize>) -> Judgement {
Judgement::of(Datum::Bytes(len), |at, _| wrong(at, "bytes, with no writing stated"))
}
fn hex(&self, item: Judgement) -> Judgement {
match item.datum {
Datum::Bytes(len) => Judgement::of(item.datum, move |at, v| match v.as_str().and_then(hex_bytes) {
Some(found) if len.is_none_or(|len| len == found) => Ok(()),
_ => wrong(
at,
match len {
Some(len) => format!("hexadecimal text of {len} bytes"),
None => "hexadecimal text".into(),
},
),
}),
_ => Judgement::of(item.datum, |at, v| {
let quantity = v.as_str().is_some_and(|text| {
text.strip_prefix("0x").is_some_and(|d| !d.is_empty() && d.chars().all(|c| c.is_ascii_hexdigit()))
});
if quantity { Ok(()) } else { wrong(at, "a hexadecimal quantity") }
}),
}
}
fn decimal(&self, item: Judgement) -> Judgement {
Judgement::of(item.datum, |at, v| {
let digits = v.as_str().is_some_and(|t| !t.is_empty() && t.chars().all(|c| c.is_ascii_digit()));
if digits { Ok(()) } else { wrong(at, "decimal digits in text") }
})
}
fn base64(&self, item: Judgement) -> Judgement {
Judgement::of(item.datum, |at, v| if v.is_string() { Ok(()) } else { wrong(at, "base64 text") })
}
fn opt(&self, item: Judgement) -> Judgement {
let inner = item.check.clone();
let mut shape = Judgement::of(item.datum, move |at, v| if v.is_null() { Ok(()) } else { inner(at, v) });
shape.absent_ok = true;
shape
}
fn seq(&self, item: Judgement) -> Judgement {
let inner = item.check.clone();
Judgement::leaf(move |at, v| match v.as_array() {
Some(items) => items.iter().enumerate().try_for_each(|(i, item)| inner(&below(at, &i.to_string()), item)),
None => wrong(at, "a sequence"),
})
}
fn map(&self, _key: Judgement, value: Judgement) -> Judgement {
let inner = value.check.clone();
Judgement::leaf(move |at, v| match v.as_object() {
Some(entries) => entries.iter().try_for_each(|(k, v)| inner(&below(at, k), v)),
None => wrong(at, "an association"),
})
}
fn product(&self, fields: Vec<Vec<Member>>) -> Judgement {
let members: Vec<Member> = fields.into_iter().flatten().collect();
let described = members.clone();
let mut shape = Judgement::leaf(move |at, v| judge_product(at, v, &described));
shape.members = Some(members);
shape
}
fn choice(&self, alternatives: Vec<Judgement>) -> Judgement {
let checks: Vec<Check> = alternatives.iter().map(|a| a.check.clone()).collect();
Judgement::leaf(move |at, v| {
if checks.iter().any(|check| check(at, v).is_ok()) {
Ok(())
} else {
wrong(at, format!("one of {} alternatives", checks.len()))
}
})
}
fn named(&self, _words: Words, body: Judgement) -> Judgement {
body
}
fn reference(&self, words: Words<'_>) -> Judgement {
let _ = words;
Judgement::leaf(|_, _| Ok(()))
}
}
fn judge_product(at: &str, value: &Value, members: &[Member]) -> Verdict {
let Some(entries) = value.as_object() else {
return wrong(at, "an object");
};
for member in members {
match entries.get(&member.name) {
Some(found) => (member.shape.check)(&below(at, &member.name), found)?,
None if member.shape.absent_ok => (),
None => return wrong(&below(at, &member.name), "a member that is present"),
}
}
undescribed(entries, members)
.map_or(Ok(()), |name| wrong(&below(at, &name), "no member, since the shape describes none here"))
}
fn undescribed(entries: &Map<String, Value>, members: &[Member]) -> Option<String> {
entries.keys().find(|key| !members.iter().any(|m| &&m.name == key)).cloned()
}
impl FieldAlg for Judge {
fn field(&self, words: Words, shape: Judgement) -> Vec<Member> {
vec![Member { name: self.spelling.spell(words), shape }]
}
fn merge(&self, shape: Judgement) -> Vec<Member> {
shape.members.clone().unwrap_or_else(|| {
vec![Member { name: String::new(), shape: Judgement::leaf(|at, _| wrong(at, "a product, to merge")) }]
})
}
}