Skip to main content

yog_data/schema/
reader.rs

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