1use alux_shape::{FieldAlg, ShapeAlg, Sorts, Spelling, Words};
4use serde_json::{Map, Value};
5use std::rc::Rc;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Mismatch {
10 pub at: String,
12 pub expected: String,
14}
15
16pub type Verdict = Result<(), Mismatch>;
18
19type Check = Rc<dyn Fn(&str, &Value) -> Verdict>;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum Datum {
24 Bytes(Option<usize>),
25 Int { signed: bool, bits: u16 },
26 Other,
27}
28
29#[derive(Clone)]
31pub struct Judgement {
32 check: Check,
33 members: Option<Vec<Member>>,
35 absent_ok: bool,
37 datum: Datum,
38}
39
40impl Judgement {
41 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#[derive(Clone)]
61pub struct Member {
62 name: String,
63 shape: Judgement,
64}
65
66#[derive(Debug, Clone, Copy)]
68pub struct Judge {
69 spelling: Spelling,
70}
71
72impl Judge {
73 pub fn new(spelling: Spelling) -> Self {
75 Self { spelling }
76 }
77}
78
79fn 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
84fn below(at: &str, name: &str) -> String {
86 if at.is_empty() { name.into() } else { format!("{at}.{name}") }
87}
88
89fn 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 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 body
238 }
239
240 fn reference(&self, words: Words<'_>) -> Judgement {
241 let _ = words;
244
245 Judgement::leaf(|_, _| Ok(()))
246 }
247}
248
249fn 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
267fn 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 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}