Skip to main content

alux_shape_json/
judge.rs

1//! Judging a JSON value against a shape.
2
3use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
4use serde_json::{Map, Value};
5use std::rc::Rc;
6
7/// What a shape found where it expected something else.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Mismatch {
10    /// Where in the value the disagreement is, as a dotted path.
11    pub at: String,
12    /// What the shape describes there.
13    pub expected: String,
14}
15
16/// Answers whether a value is described.
17pub type Verdict = Result<(), Mismatch>;
18
19type Check = Rc<dyn Fn(&str, &Value) -> Verdict>;
20
21/// What a leaf is, which is what decides how a writing modifier reads it.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum Datum {
24    Bytes(Option<usize>),
25    Int { signed: bool, bits: u16 },
26    Other,
27}
28
29/// A shape, as this interpretation carries one: a check, plus what a modifier needs to know about it.
30#[derive(Clone)]
31pub struct Judgement {
32    check: Check,
33    /// The members of a product, kept so that merging one into another is expressible.
34    members: Option<Vec<Member>>,
35    /// Whether the value may be absent altogether, rather than merely null.
36    absent_ok: bool,
37    datum: Datum,
38}
39
40impl Judgement {
41    /// Decides a value, reporting the first disagreement.
42    ///
43    /// # Errors
44    ///
45    /// Answers with the mismatch when the value is not the one this shape describes.
46    pub fn holds(&self, value: &Value) -> Verdict {
47        (self.check)("", value)
48    }
49
50    fn of(datum: Datum, check: impl Fn(&str, &Value) -> Verdict + 'static) -> Self {
51        Self { check: Rc::new(check), members: None, absent_ok: false, datum }
52    }
53
54    fn leaf(check: impl Fn(&str, &Value) -> Verdict + 'static) -> Self {
55        Self::of(Datum::Other, check)
56    }
57}
58
59/// One member of a product, ready to be looked for in an object.
60#[derive(Clone)]
61pub struct Member {
62    name: String,
63    shape: Judgement,
64}
65
66/// Judges JSON against a shape, spelling names as the surface spells them.
67#[derive(Debug, Clone, Copy)]
68pub struct Judge {
69    spelling: Spelling,
70}
71
72impl Judge {
73    /// Judges values whose names are spelled this way.
74    pub fn new(spelling: Spelling) -> Self {
75        Self { spelling }
76    }
77}
78
79/// Reports what was expected where.
80fn wrong(at: &str, expected: impl Into<String>) -> Verdict {
81    Err(Mismatch { at: if at.is_empty() { ".".into() } else { at.into() }, expected: expected.into() })
82}
83
84/// Extends a path with a member's name.
85fn below(at: &str, name: &str) -> String {
86    if at.is_empty() { name.into() } else { format!("{at}.{name}") }
87}
88
89/// Reads a hexadecimal string, answering with the bytes it states.
90fn hex_bytes(text: &str) -> Option<usize> {
91    let digits = text.strip_prefix("0x")?;
92
93    (digits.len() % 2 == 0 && digits.chars().all(|c| c.is_ascii_hexdigit())).then_some(digits.len() / 2)
94}
95
96impl Sorts for Judge {
97    type Ty = Judgement;
98    type Field = Vec<Member>;
99}
100
101impl ShapeAlg for Judge {
102    fn truth(&self) -> Judgement {
103        Judgement::leaf(|at, v| if v.is_boolean() { Ok(()) } else { wrong(at, "a boolean") })
104    }
105
106    fn unit(&self) -> Judgement {
107        Judgement::leaf(|at, v| if v.is_null() { Ok(()) } else { wrong(at, "null") })
108    }
109
110    fn text(&self) -> Judgement {
111        Judgement::leaf(|at, v| if v.is_string() { Ok(()) } else { wrong(at, "text") })
112    }
113
114    fn literal(&self, text: &str) -> Judgement {
115        let expected = text.to_owned();
116
117        Judgement::leaf(move |at, v| {
118            if v.as_str() == Some(expected.as_str()) { Ok(()) } else { wrong(at, format!("\"{expected}\"")) }
119        })
120    }
121
122    fn name_word(&self, words: Words<'_>) -> Judgement {
123        let spelled = self.spelling.spell(words);
124
125        Judgement::leaf(
126            move |at, v| {
127                if v.as_str() == Some(spelled.as_str()) { Ok(()) } else { wrong(at, format!("\"{spelled}\"")) }
128            },
129        )
130    }
131
132    fn int(&self, signed: bool, bits: u16) -> Judgement {
133        Judgement::of(Datum::Int { signed, bits }, move |at, v| {
134            let fits = match v {
135                Value::Number(n) if signed => n.as_i64().is_some(),
136                Value::Number(n) => n.as_u64().is_some(),
137                _ => false,
138            };
139
140            if fits { Ok(()) } else { wrong(at, format!("an integer of {bits} bits")) }
141        })
142    }
143
144    fn float(&self, bits: u16) -> Judgement {
145        Judgement::leaf(move |at, v| if v.is_number() { Ok(()) } else { wrong(at, format!("a number of {bits} bits")) })
146    }
147
148    fn bytes(&self, len: Option<usize>) -> Judgement {
149        // Bytes alone say nothing about how they are written, and JSON has no form for them. A
150        // writing modifier is what makes them readable, so bare bytes describe no value.
151        Judgement::of(Datum::Bytes(len), |at, _| wrong(at, "bytes, with no writing stated"))
152    }
153
154    fn hex(&self, item: Judgement) -> Judgement {
155        match item.datum {
156            Datum::Bytes(len) => Judgement::of(item.datum, move |at, v| match v.as_str().and_then(hex_bytes) {
157                Some(found) if len.is_none_or(|len| len == found) => Ok(()),
158                _ => wrong(
159                    at,
160                    match len {
161                        Some(len) => format!("hexadecimal text of {len} bytes"),
162                        None => "hexadecimal text".into(),
163                    },
164                ),
165            }),
166            _ => Judgement::of(item.datum, |at, v| {
167                let quantity = v.as_str().is_some_and(|text| {
168                    text.strip_prefix("0x").is_some_and(|d| !d.is_empty() && d.chars().all(|c| c.is_ascii_hexdigit()))
169                });
170
171                if quantity { Ok(()) } else { wrong(at, "a hexadecimal quantity") }
172            }),
173        }
174    }
175
176    fn decimal(&self, item: Judgement) -> Judgement {
177        Judgement::of(item.datum, |at, v| {
178            let digits = v.as_str().is_some_and(|t| !t.is_empty() && t.chars().all(|c| c.is_ascii_digit()));
179
180            if digits { Ok(()) } else { wrong(at, "decimal digits in text") }
181        })
182    }
183
184    fn base64(&self, item: Judgement) -> Judgement {
185        Judgement::of(item.datum, |at, v| if v.is_string() { Ok(()) } else { wrong(at, "base64 text") })
186    }
187
188    fn opt(&self, item: Judgement) -> Judgement {
189        let inner = item.check.clone();
190        let mut shape = Judgement::of(item.datum, move |at, v| if v.is_null() { Ok(()) } else { inner(at, v) });
191        shape.absent_ok = true;
192
193        shape
194    }
195
196    fn seq(&self, item: Judgement) -> Judgement {
197        let inner = item.check.clone();
198
199        Judgement::leaf(move |at, v| match v.as_array() {
200            Some(items) => items.iter().enumerate().try_for_each(|(i, item)| inner(&below(at, &i.to_string()), item)),
201            None => wrong(at, "a sequence"),
202        })
203    }
204
205    fn map(&self, _key: Judgement, value: Judgement) -> Judgement {
206        let inner = value.check.clone();
207
208        Judgement::leaf(move |at, v| match v.as_object() {
209            Some(entries) => entries.iter().try_for_each(|(k, v)| inner(&below(at, k), v)),
210            None => wrong(at, "an association"),
211        })
212    }
213
214    fn product(&self, fields: Vec<Vec<Member>>) -> Judgement {
215        let members: Vec<Member> = fields.into_iter().flatten().collect();
216        let described = members.clone();
217        let mut shape = Judgement::leaf(move |at, v| judge_product(at, v, &described));
218        shape.members = Some(members);
219
220        shape
221    }
222
223    fn choice(&self, alternatives: Vec<Judgement>) -> Judgement {
224        let checks: Vec<Check> = alternatives.iter().map(|a| a.check.clone()).collect();
225
226        Judgement::leaf(move |at, v| {
227            if checks.iter().any(|check| check(at, v).is_ok()) {
228                Ok(())
229            } else {
230                wrong(at, format!("one of {} alternatives", checks.len()))
231            }
232        })
233    }
234
235    fn named(&self, _words: Words, body: Judgement) -> Judgement {
236        // A name is an identity, and identity does not decide a value.
237        body
238    }
239
240    fn reference(&self, words: Words<'_>) -> Judgement {
241        // Resolving a name needs the whole term, which a fold does not hold. Until a term is read
242        // back from its written form, a reference describes anything.
243        let _ = words;
244
245        Judgement::leaf(|_, _| Ok(()))
246    }
247}
248
249/// Decides an object against the members described for it, in both directions.
250fn judge_product(at: &str, value: &Value, members: &[Member]) -> Verdict {
251    let Some(entries) = value.as_object() else {
252        return wrong(at, "an object");
253    };
254
255    for member in members {
256        match entries.get(&member.name) {
257            Some(found) => (member.shape.check)(&below(at, &member.name), found)?,
258            None if member.shape.absent_ok => (),
259            None => return wrong(&below(at, &member.name), "a member that is present"),
260        }
261    }
262
263    undescribed(entries, members)
264        .map_or(Ok(()), |name| wrong(&below(at, &name), "no member, since the shape describes none here"))
265}
266
267/// Names a key the shape does not describe, if the value carries one.
268fn undescribed(entries: &Map<String, Value>, members: &[Member]) -> Option<String> {
269    entries.keys().find(|key| !members.iter().any(|m| &&m.name == key)).cloned()
270}
271
272impl FieldAlg for Judge {
273    fn field(&self, words: Words, shape: Judgement) -> Vec<Member> {
274        vec![Member { name: self.spelling.spell(words), shape }]
275    }
276
277    fn merge(&self, shape: Judgement) -> Vec<Member> {
278        // Merging answers with the members of the product merged in, so a merge of anything else
279        // contributes a member no value can satisfy.
280        shape.members.clone().unwrap_or_else(|| {
281            vec![Member { name: String::new(), shape: Judgement::leaf(|at, _| wrong(at, "a product, to merge")) }]
282        })
283    }
284}