Skip to main content

yog_data/schema/
reader.rs

1//! Reading a value against a schema: its problems, with their paths.
2use crate::Diagnostic;
3
4use super::{MapRef, Path, Ref, Res, Schema, Seg, Text, ValueRef};
5
6// ── Reading ─────────────────────────────────────────────────────────────────
7
8/// Where a reference was found.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Found<T> {
11    pub value: T,
12    pub path: Path,
13}
14
15/// The state of reading a value: where it is, the problems so far, and the
16/// references met (for the build to check against the content there is).
17#[derive(Default)]
18pub struct Reader<'r> {
19    /// Where it is, as steps: a field's name stays `&'static` until a
20    /// problem or a reference needs the whole [`Path`].
21    pub(super) path: Vec<Step>,
22    /// Don't keep the references, tags and resources met (who only wants the value).
23    pub(super) value_only: bool,
24    /// What the tool makes of references and texts (none: kept as written).
25    pub(super) resolver: Option<&'r mut dyn Resolve>,
26    pub problems: Vec<Diagnostic>,
27    pub refs: Vec<Found<Ref>>,
28    pub tags: Vec<Found<Ref>>,
29    pub resources: Vec<Found<Res>>,
30    /// Keys a struct being read doesn't report as unknown (an enum's tag).
31    pub(super) allowed: Vec<&'static str>,
32}
33
34/// A step of [`Reader`]'s path.
35pub(super) enum Step {
36    Seg(Seg),
37    Field(&'static str),
38}
39
40/// A note for whoever edits the file — every object may have one.
41pub const COMMENT_KEY: &str = "comment";
42/// What a source-only key starts with: `$…` (a note on an entry: `$<key>`), `_…`.
43pub const META_PREFIX: char = '$';
44pub const PRIVATE_PREFIX: char = '_';
45
46/// Source-only keys every object may have: `$…`, `_…`, `comment`.
47pub fn is_metadata_key(key: &str) -> bool {
48    key.starts_with(META_PREFIX) || key.starts_with(PRIVATE_PREFIX) || key == COMMENT_KEY
49}
50
51/// What a tool makes of the references and texts it reads: `yog build`
52/// checks each reference against the content there is and turns texts
53/// into translation keys; the IDE shows them.
54pub trait Resolve {
55    /// A reference: the id to keep, or why it's wrong.
56    fn reference(&mut self, r: &Ref, _path: &Path) -> Result<String, String> {
57        Ok(r.id.clone())
58    }
59
60    /// A text: what to keep (its words, a translation key, ...), or why it's wrong.
61    fn text(&mut self, t: &Text, _path: &Path) -> Result<String, String> {
62        Ok(match t {
63            Text::Literal(s) => s.clone(),
64            Text::Res(r) => r.to_string(),
65            Text::NameOf(r) => format!("@{}:{}", r.kind, r.id),
66            Text::Lang(k) => format!("@lang:{k}"),
67        })
68    }
69
70    /// The words of a `.yres` text, if known (to check what's in them).
71    fn words(&mut self, _r: &Res) -> Option<String> {
72        None
73    }
74}
75
76impl<'r> Reader<'r> {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// A reader handing references and texts to `resolver`.
82    pub fn with_resolver(resolver: &'r mut dyn Resolve) -> Self {
83        Self {
84            resolver: Some(resolver),
85            ..Self::default()
86        }
87    }
88
89    /// `r` resolved (a problem if it's wrong); its id without a resolver.
90    pub fn resolve_ref(&mut self, r: &Ref) -> Option<String> {
91        let result = match &mut self.resolver {
92            Some(res) => res.reference(r, &Self::built(&self.path)),
93            None => Ok(r.id.clone()),
94        };
95        result.map_err(|e| self.problem(e)).ok()
96    }
97
98    /// `t` resolved (a problem if it's wrong).
99    pub fn resolve_text(&mut self, t: &Text) -> Option<String> {
100        struct Keep;
101        impl Resolve for Keep {}
102        let result = match &mut self.resolver {
103            Some(res) => res.text(t, &Self::built(&self.path)),
104            None => Keep.text(t, &Path::root()),
105        };
106        result.map_err(|e| self.problem(e)).ok()
107    }
108
109    /// [`Reader::resolve_text`], taking `t`: without a resolver its words
110    /// move into the result instead of being copied.
111    pub fn take_text(&mut self, t: Text) -> Option<String> {
112        match (t, self.resolver.is_some()) {
113            (Text::Literal(s), false) => Some(s),
114            (t, _) => self.resolve_text(&t),
115        }
116    }
117
118    /// [`Reader::resolve_ref`], taking `r`: without a resolver the id moves.
119    pub fn take_ref(&mut self, r: Ref) -> Option<String> {
120        if self.resolver.is_none() {
121            return Some(r.id);
122        }
123        self.resolve_ref(&r)
124    }
125
126    /// Whether references are kept for the caller ([`read_value`] doesn't).
127    pub fn keeps_found(&self) -> bool {
128        !self.value_only
129    }
130
131    /// Whether references and texts go through a resolver (else kept as written).
132    pub fn resolves(&self) -> bool {
133        self.resolver.is_some()
134    }
135
136    /// The words of `.yres` text `r`, if the resolver knows them.
137    pub fn words(&mut self, r: &Res) -> Option<String> {
138        self.resolver.as_mut()?.words(r)
139    }
140
141    /// Where the reader is.
142    pub fn path(&self) -> Path {
143        Self::built(&self.path)
144    }
145
146    fn built(steps: &[Step]) -> Path {
147        let mut p = Path::root();
148        for s in steps {
149            p.push(match s {
150                Step::Seg(s) => s.clone(),
151                Step::Field(k) => Seg::Key((*k).to_string()),
152            });
153        }
154        p
155    }
156
157    /// Reports a problem with the value being read.
158    pub fn problem(&mut self, message: impl Into<String>) {
159        self.problem_of(yog_ids::data_problem::INVALID, message);
160    }
161
162    /// Reports a problem of kind `code` (`yog_ids::data_problem`) with the value being read.
163    pub fn problem_of(&mut self, code: &'static str, message: impl Into<String>) {
164        let found = Diagnostic::error(code, message.into()).at_path(self.path());
165        self.problems.push(found);
166    }
167
168    /// "expected X, found Y".
169    pub fn expected(&mut self, what: &str, found: &ValueRef) {
170        self.problem_of(
171            yog_ids::data_problem::EXPECTED,
172            format!("expected {what}, found {}", found_text(found)),
173        );
174    }
175
176    /// Runs `f` one step deeper.
177    pub fn at<T>(&mut self, seg: Seg, f: impl FnOnce(&mut Self) -> T) -> T {
178        self.path.push(Step::Seg(seg));
179        let r = f(self);
180        self.path.pop();
181        r
182    }
183
184    /// Runs `f` in field `key` (no allocation for the step).
185    fn in_field<T>(&mut self, key: &'static str, f: impl FnOnce(&mut Self) -> T) -> T {
186        self.path.push(Step::Field(key));
187        let r = f(self);
188        self.path.pop();
189        r
190    }
191
192    pub fn note_ref(&mut self, r: Ref) {
193        if self.value_only {
194            return;
195        }
196        let path = self.path();
197        self.refs.push(Found { value: r, path });
198    }
199
200    pub fn note_tag(&mut self, r: Ref) {
201        if self.value_only {
202            return;
203        }
204        let path = self.path();
205        self.tags.push(Found { value: r, path });
206    }
207
208    pub fn note_res(&mut self, r: Res) {
209        if self.value_only {
210            return;
211        }
212        let path = self.path();
213        self.resources.push(Found { value: r, path });
214    }
215
216    /// `v` as an object, or a problem.
217    pub fn map<'v>(&mut self, v: &ValueRef<'v>) -> Option<MapRef<'v>> {
218        match v {
219            ValueRef::Map(m) => Some(*m),
220            other => {
221                self.expected("an object", other);
222                None
223            }
224        }
225    }
226
227    /// Reports keys of `map` that aren't `known` (nor metadata, nor allowed by an enclosing enum).
228    pub fn unknown_keys(&mut self, map: MapRef, known: &[&str]) {
229        for key in map.keys() {
230            if known.contains(&key) || is_metadata_key(key) || self.allowed.contains(&key) {
231                continue;
232            }
233            let hint = closest(key, known)
234                .map(|k| format!(" — did you mean `{k}`?"))
235                .unwrap_or_else(|| {
236                    if known.is_empty() {
237                        String::new()
238                    } else {
239                        format!(
240                            " (expected {})",
241                            known
242                                .iter()
243                                .map(|k| format!("`{k}`"))
244                                .collect::<Vec<_>>()
245                                .join(", ")
246                        )
247                    }
248                });
249            let mut found = Diagnostic::error(
250                yog_ids::data_problem::UNKNOWN_FIELD,
251                format!("unknown field `{key}`{hint}"),
252            )
253            .at_path(self.path().key(key));
254            found.on_key = true;
255            self.problems.push(found);
256        }
257    }
258
259    /// Reports a name that isn't one of `known` (an enum's variant).
260    pub fn unknown_name(&mut self, name: &str, known: &[&str]) {
261        let hint = match closest(name, known) {
262            Some(k) => format!(" — did you mean `{k}`?"),
263            None => format!(
264                " (expected {})",
265                known
266                    .iter()
267                    .map(|k| format!("`{k}`"))
268                    .collect::<Vec<_>>()
269                    .join(", ")
270            ),
271        };
272        self.problem(format!("unknown `{name}`{hint}"));
273    }
274
275    /// Field `key` of `map`: read, or its absent value, or a problem.
276    pub fn field<T: Schema>(&mut self, map: MapRef, key: &'static str) -> Option<T> {
277        match map.get(key) {
278            Some(v) => self.at(Seg::Key(key.to_string()), |cx| T::read(v, cx)),
279            None => T::absent().or_else(|| {
280                self.problem_of(
281                    yog_ids::data_problem::MISSING_FIELD,
282                    format!("missing `{key}`"),
283                );
284                None
285            }),
286        }
287    }
288
289    /// Field `key` of `map`, `default()` when it isn't there.
290    pub fn field_or<T: Schema>(
291        &mut self,
292        map: MapRef,
293        key: &'static str,
294        default: impl FnOnce() -> T,
295    ) -> Option<T> {
296        match map.get(key) {
297            Some(v) => self.at(Seg::Key(key.to_string()), |cx| T::read(v, cx)),
298            None => Some(default()),
299        }
300    }
301
302    /// Field `key` of `map` through codec `W`.
303    pub fn field_with<W: crate::with::With<F>, F>(
304        &mut self,
305        map: MapRef,
306        key: &'static str,
307    ) -> Option<F> {
308        match map.get(key) {
309            Some(v) => self.in_field(key, |cx| W::read(v, cx)),
310            None => W::absent().or_else(|| {
311                self.problem_of(
312                    yog_ids::data_problem::MISSING_FIELD,
313                    format!("missing `{key}`"),
314                );
315                None
316            }),
317        }
318    }
319
320    /// Field `key` of `map` through codec `W`, `default()` when it isn't there.
321    pub fn field_or_with<W: crate::with::With<F>, F>(
322        &mut self,
323        map: MapRef,
324        key: &'static str,
325        default: impl FnOnce() -> F,
326    ) -> Option<F> {
327        match map.get(key) {
328            Some(v) => self.in_field(key, |cx| W::read(v, cx)),
329            None => Some(default()),
330        }
331    }
332
333    /// Reads `v` as `T` with `key` allowed in it — the tag of the enum variant `T` is.
334    pub fn with_allowed<T>(&mut self, key: &'static str, f: impl FnOnce(&mut Self) -> T) -> T {
335        self.allowed.push(key);
336        let r = f(self);
337        self.allowed.pop();
338        r
339    }
340
341    /// Reads with every one of `keys` allowed (the fields of a struct read
342    /// from the same object — see [`read_both`]).
343    pub fn with_allowed_all<T>(
344        &mut self,
345        keys: &[&'static str],
346        f: impl FnOnce(&mut Self) -> T,
347    ) -> T {
348        let before = self.allowed.len();
349        self.allowed.extend_from_slice(keys);
350        let r = f(self);
351        self.allowed.truncate(before);
352        r
353    }
354
355    /// Reads a struct's own keys with no allowance inherited from outside.
356    pub fn own_keys<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
357        let saved = std::mem::take(&mut self.allowed);
358        let r = f(self);
359        self.allowed = saved;
360        r
361    }
362}
363
364fn found_text(v: &ValueRef) -> String {
365    match v {
366        ValueRef::Str(s) if s.chars().count() <= 40 => format!("{s:?}"),
367        ValueRef::Int(n) => n.to_string(),
368        ValueRef::Float(f) => f.to_string(),
369        ValueRef::Bool(b) => b.to_string(),
370        other => other.kind_name().to_string(),
371    }
372}
373
374/// The `known` name closest to `key`, if it's close enough to be a typo
375/// (`txt` → `text`, `img` → `image`, `colr` → `color`).
376fn closest<'k>(key: &str, known: &[&'k str]) -> Option<&'k str> {
377    let lower = key.to_ascii_lowercase();
378    known
379        .iter()
380        .map(|k| (edit_distance(&lower, k), *k))
381        .filter(|(d, k)| *d <= if k.len() <= 3 { 1 } else { k.len() / 2 })
382        .min_by_key(|(d, _)| *d)
383        .map(|(_, k)| k)
384}
385
386fn edit_distance(a: &str, b: &str) -> usize {
387    let b: Vec<char> = b.chars().collect();
388    let mut prev: Vec<usize> = (0..=b.len()).collect();
389    for (i, ca) in a.chars().enumerate() {
390        let mut cur = vec![i + 1; b.len() + 1];
391        for (j, cb) in b.iter().enumerate() {
392            cur[j + 1] = (prev[j] + usize::from(ca != *cb))
393                .min(prev[j + 1] + 1)
394                .min(cur[j] + 1);
395        }
396        prev = cur;
397    }
398    prev[b.len()]
399}
400
401/// What reading a document gives: the value and the references in it.
402#[derive(Debug)]
403pub struct Read<T> {
404    pub value: T,
405    pub refs: Vec<Found<Ref>>,
406    pub tags: Vec<Found<Ref>>,
407    pub resources: Vec<Found<Res>>,
408}
409
410/// The numbers a value holds — what `#[data(range = …)]` checks.
411pub trait Numbers {
412    fn numbers(&self, out: &mut Vec<f64>);
413}
414
415macro_rules! numbers {
416    ($($t:ty),*) => {$(
417        impl Numbers for $t {
418            fn numbers(&self, out: &mut Vec<f64>) {
419                out.push(*self as f64);
420            }
421        }
422    )*};
423}
424numbers!(i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64);
425
426impl<T: Numbers> Numbers for Option<T> {
427    fn numbers(&self, out: &mut Vec<f64>) {
428        if let Some(x) = self {
429            x.numbers(out);
430        }
431    }
432}
433
434impl<T: Numbers> Numbers for Vec<T> {
435    fn numbers(&self, out: &mut Vec<f64>) {
436        self.iter().for_each(|x| x.numbers(out));
437    }
438}
439
440impl<T: Numbers, const N: usize> Numbers for [T; N] {
441    fn numbers(&self, out: &mut Vec<f64>) {
442        self.iter().for_each(|x| x.numbers(out));
443    }
444}
445
446impl Reader<'_> {
447    /// `value` (field `key`) checked against `lo..=hi`; `None` (a problem) if it's out.
448    pub fn in_range<T: Numbers>(
449        &mut self,
450        key: &str,
451        value: Option<T>,
452        lo: f64,
453        hi: f64,
454    ) -> Option<T> {
455        let v = value?;
456        let mut ns = Vec::new();
457        v.numbers(&mut ns);
458        match ns.into_iter().find(|n| !(lo..=hi).contains(n)) {
459            None => Some(v),
460            Some(n) => {
461                let show = |x: f64| {
462                    if x.fract() == 0.0 {
463                        format!("{}", x as i64)
464                    } else {
465                        x.to_string()
466                    }
467                };
468                let bad = show(n);
469                self.at(Seg::Key(key.to_string()), |cx| {
470                    cx.problem_of(
471                        yog_ids::data_problem::OUT_OF_RANGE,
472                        format!("{bad} is out of range ({}..={})", show(lo), show(hi)),
473                    )
474                });
475                None
476            }
477        }
478    }
479}