Skip to main content

yog_data/
schema.rs

1//! Schemas: a format is a Rust type implementing [`Schema`] (usually
2//! `#[derive(Schema)]`). One schema reads and writes the format, checks it
3//! with the path of every problem, collects its references for the build to
4//! check, and describes itself ([`Shape`]) for Yog-IDLE's editors and the
5//! docs.
6
7use std::collections::{BTreeMap, HashMap};
8use std::fmt;
9use std::marker::PhantomData;
10
11use crate::text::{self, Doc, LineCol};
12use crate::typed;
13use crate::value::{Map, Path, Ref, Res, Seg, Value};
14
15/// A Yog Data format.
16pub trait Schema: Sized {
17    /// What the format looks like.
18    fn shape() -> Shape;
19
20    /// Reads `v`. Problems go to `cx` at its current path; `None` when the
21    /// value couldn't be read (the problem is in `cx`).
22    fn read(v: &Value, cx: &mut Reader) -> Option<Self>;
23
24    /// The value, as the format writes it.
25    fn write(&self) -> Value;
26
27    /// The value of a field that isn't there — `None`: the field is required.
28    fn absent() -> Option<Self> {
29        None
30    }
31
32    /// Whether a struct leaves this field out when writing it.
33    fn skip(&self) -> bool {
34        false
35    }
36}
37
38// ── Shapes ──────────────────────────────────────────────────────────────────
39
40/// A shape, built on demand — so shapes of recursive types are fine.
41pub type ShapeFn = fn() -> Shape;
42
43/// What a format looks like: drives Yog-IDLE's property grids and pickers,
44/// completion, hover docs and the docs' field tables.
45#[derive(Debug, Clone)]
46pub enum Shape {
47    /// Any value.
48    Any,
49    Bool,
50    Int {
51        min: i64,
52        max: i64,
53    },
54    Float,
55    /// Plain text.
56    Str,
57    /// Yog text, a `@Set.key` resource, or `@kind:id` (that content's name).
58    Text,
59    /// An image source or a `@Set.key` image resource.
60    Image,
61    /// `#RRGGBB`/`#AARRGGBB` or a `@Set.key` color resource.
62    Color,
63    /// A mod resource, `@Set.key`.
64    Res,
65    /// Content of a kind (`""`: any kind, written `@kind:id`).
66    Ref(&'static str),
67    /// A tag of content of a kind.
68    Tag(&'static str),
69    /// Content of a kind or a tag of it.
70    RefOrTag(&'static str),
71    List(ShapeFn),
72    /// An object of any keys, every value this shape.
73    Map(ShapeFn),
74    /// A value or null.
75    Optional(ShapeFn),
76    Struct(StructShape),
77    Enum(EnumShape),
78}
79
80#[derive(Debug, Clone)]
81pub struct StructShape {
82    pub name: &'static str,
83    pub doc: &'static str,
84    pub fields: Vec<Field>,
85}
86
87#[derive(Debug, Clone)]
88pub struct Field {
89    pub name: &'static str,
90    pub doc: &'static str,
91    pub shape: ShapeFn,
92    /// Must be written (no default).
93    pub required: bool,
94}
95
96#[derive(Debug, Clone)]
97pub struct EnumShape {
98    pub name: &'static str,
99    pub doc: &'static str,
100    /// The key naming the variant (`"type"`), `None` for an enum of names.
101    pub tag: Option<&'static str>,
102    pub variants: Vec<Variant>,
103}
104
105#[derive(Debug, Clone)]
106pub struct Variant {
107    pub name: &'static str,
108    pub doc: &'static str,
109    /// The variant's fields (none for a name).
110    pub fields: Vec<Field>,
111}
112
113impl Shape {
114    /// The shape as Yog Data, for tools in other languages (Yog-IDLE).
115    /// Structs and enums are written once in `defs` and referred to by name.
116    pub fn describe(&self) -> Value {
117        let mut defs = Map::new();
118        let root = self.describe_into(&mut defs);
119        let mut m = Map::new();
120        m.insert("root", root);
121        m.insert("defs", Value::Map(defs));
122        Value::Map(m)
123    }
124
125    fn describe_into(&self, defs: &mut Map) -> Value {
126        let kind = |k: &str| {
127            let mut m = Map::new();
128            m.insert("kind", Value::from(k));
129            m
130        };
131        let fields = |fs: &[Field], defs: &mut Map| -> Value {
132            Value::List(
133                fs.iter()
134                    .map(|f| {
135                        let mut m = Map::new();
136                        m.insert("name", Value::from(f.name));
137                        if !f.doc.is_empty() {
138                            m.insert("doc", Value::from(f.doc));
139                        }
140                        m.insert("shape", (f.shape)().describe_into(defs));
141                        m.insert("required", Value::Bool(f.required));
142                        Value::Map(m)
143                    })
144                    .collect(),
145            )
146        };
147        let m = match self {
148            Shape::Any => kind("any"),
149            Shape::Bool => kind("bool"),
150            Shape::Int { min, max } => {
151                let mut m = kind("int");
152                m.insert("min", Value::Int(*min));
153                m.insert("max", Value::Int(*max));
154                m
155            }
156            Shape::Float => kind("float"),
157            Shape::Str => kind("string"),
158            Shape::Text => kind("text"),
159            Shape::Image => kind("image"),
160            Shape::Color => kind("color"),
161            Shape::Res => kind("res"),
162            Shape::Ref(k) | Shape::Tag(k) | Shape::RefOrTag(k) => {
163                let mut m = kind(match self {
164                    Shape::Ref(_) => "ref",
165                    Shape::Tag(_) => "tag",
166                    _ => "ref_or_tag",
167                });
168                m.insert("of", Value::from(*k));
169                m
170            }
171            Shape::List(s) | Shape::Map(s) | Shape::Optional(s) => {
172                let mut m = kind(match self {
173                    Shape::List(_) => "list",
174                    Shape::Map(_) => "map",
175                    _ => "optional",
176                });
177                m.insert("of", s().describe_into(defs));
178                m
179            }
180            Shape::Struct(s) => {
181                if defs.get(s.name).is_none() {
182                    // Placeholder first: a recursive type finds itself defined.
183                    defs.insert(s.name, Value::Null);
184                    let mut d = kind("struct");
185                    if !s.doc.is_empty() {
186                        d.insert("doc", Value::from(s.doc));
187                    }
188                    d.insert("fields", fields(&s.fields, defs));
189                    defs.insert(s.name, Value::Map(d));
190                }
191                let mut m = kind("def");
192                m.insert("name", Value::from(s.name));
193                m
194            }
195            Shape::Enum(e) => {
196                if defs.get(e.name).is_none() {
197                    defs.insert(e.name, Value::Null);
198                    let mut d = kind("enum");
199                    if !e.doc.is_empty() {
200                        d.insert("doc", Value::from(e.doc));
201                    }
202                    if let Some(tag) = e.tag {
203                        d.insert("tag", Value::from(tag));
204                    }
205                    let variants = e
206                        .variants
207                        .iter()
208                        .map(|v| {
209                            let mut m = Map::new();
210                            m.insert("name", Value::from(v.name));
211                            if !v.doc.is_empty() {
212                                m.insert("doc", Value::from(v.doc));
213                            }
214                            if e.tag.is_some() {
215                                m.insert("fields", fields(&v.fields, defs));
216                            }
217                            Value::Map(m)
218                        })
219                        .collect();
220                    d.insert("variants", Value::List(variants));
221                    defs.insert(e.name, Value::Map(d));
222                }
223                let mut m = kind("def");
224                m.insert("name", Value::from(e.name));
225                m
226            }
227        };
228        Value::Map(m)
229    }
230}
231
232// ── Reading ─────────────────────────────────────────────────────────────────
233
234/// A problem found reading a value.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct Problem {
237    pub path: Path,
238    pub message: String,
239    /// About the key at `path` rather than its value (an unknown field).
240    pub at_key: bool,
241}
242
243/// Where a reference was found.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct Found<T> {
246    pub value: T,
247    pub path: Path,
248}
249
250/// The state of reading a value: where it is, the problems so far, and the
251/// references met (for the build to check against the content there is).
252#[derive(Debug, Default)]
253pub struct Reader {
254    path: Path,
255    pub problems: Vec<Problem>,
256    pub refs: Vec<Found<Ref>>,
257    pub tags: Vec<Found<Ref>>,
258    pub resources: Vec<Found<Res>>,
259    /// Keys a struct being read doesn't report as unknown (an enum's tag).
260    allowed: Vec<&'static str>,
261}
262
263/// Source-only keys every object may have: `$…`, `_…`, `comment`.
264pub fn is_metadata_key(key: &str) -> bool {
265    key.starts_with('$') || key.starts_with('_') || key == "comment"
266}
267
268impl Reader {
269    pub fn new() -> Self {
270        Self::default()
271    }
272
273    pub fn path(&self) -> &Path {
274        &self.path
275    }
276
277    /// Reports a problem with the value being read.
278    pub fn problem(&mut self, message: impl Into<String>) {
279        self.problems.push(Problem {
280            path: self.path.clone(),
281            message: message.into(),
282            at_key: false,
283        });
284    }
285
286    /// "expected X, found Y".
287    pub fn expected(&mut self, what: &str, found: &Value) {
288        self.problem(format!("expected {what}, found {}", found_text(found)));
289    }
290
291    /// Runs `f` one step deeper.
292    pub fn at<T>(&mut self, seg: Seg, f: impl FnOnce(&mut Self) -> T) -> T {
293        self.path.push(seg);
294        let r = f(self);
295        self.path.pop();
296        r
297    }
298
299    pub fn note_ref(&mut self, r: Ref) {
300        self.refs.push(Found {
301            value: r,
302            path: self.path.clone(),
303        });
304    }
305
306    pub fn note_tag(&mut self, r: Ref) {
307        self.tags.push(Found {
308            value: r,
309            path: self.path.clone(),
310        });
311    }
312
313    pub fn note_res(&mut self, r: Res) {
314        self.resources.push(Found {
315            value: r,
316            path: self.path.clone(),
317        });
318    }
319
320    /// `v` as an object, or a problem.
321    pub fn map<'v>(&mut self, v: &'v Value) -> Option<&'v Map> {
322        match v {
323            Value::Map(m) => Some(m),
324            other => {
325                self.expected("an object", other);
326                None
327            }
328        }
329    }
330
331    /// Reports keys of `map` that aren't `known` (nor metadata, nor allowed by an enclosing enum).
332    pub fn unknown_keys(&mut self, map: &Map, known: &[&str]) {
333        for key in map.keys() {
334            if known.contains(&key) || is_metadata_key(key) || self.allowed.contains(&key) {
335                continue;
336            }
337            let hint = closest(key, known)
338                .map(|k| format!(" — did you mean `{k}`?"))
339                .unwrap_or_else(|| {
340                    if known.is_empty() {
341                        String::new()
342                    } else {
343                        format!(
344                            " (expected {})",
345                            known
346                                .iter()
347                                .map(|k| format!("`{k}`"))
348                                .collect::<Vec<_>>()
349                                .join(", ")
350                        )
351                    }
352                });
353            self.problems.push(Problem {
354                path: self.path.key(key),
355                message: format!("unknown field `{key}`{hint}"),
356                at_key: true,
357            });
358        }
359    }
360
361    /// Reports a name that isn't one of `known` (an enum's variant).
362    pub fn unknown_name(&mut self, name: &str, known: &[&str]) {
363        let hint = match closest(name, known) {
364            Some(k) => format!(" — did you mean `{k}`?"),
365            None => format!(
366                " (expected {})",
367                known
368                    .iter()
369                    .map(|k| format!("`{k}`"))
370                    .collect::<Vec<_>>()
371                    .join(", ")
372            ),
373        };
374        self.problem(format!("unknown `{name}`{hint}"));
375    }
376
377    /// Field `key` of `map`: read, or its absent value, or a problem.
378    pub fn field<T: Schema>(&mut self, map: &Map, key: &'static str) -> Option<T> {
379        match map.get(key) {
380            Some(v) => self.at(Seg::Key(key.to_string()), |cx| T::read(v, cx)),
381            None => T::absent().or_else(|| {
382                self.problem(format!("missing `{key}`"));
383                None
384            }),
385        }
386    }
387
388    /// Field `key` of `map`, `default()` when it isn't there.
389    pub fn field_or<T: Schema>(
390        &mut self,
391        map: &Map,
392        key: &'static str,
393        default: impl FnOnce() -> T,
394    ) -> Option<T> {
395        match map.get(key) {
396            Some(v) => self.at(Seg::Key(key.to_string()), |cx| T::read(v, cx)),
397            None => Some(default()),
398        }
399    }
400
401    /// Reads `v` as `T` with `key` allowed in it — the tag of the enum variant `T` is.
402    pub fn with_allowed<T>(&mut self, key: &'static str, f: impl FnOnce(&mut Self) -> T) -> T {
403        self.allowed.push(key);
404        let r = f(self);
405        self.allowed.pop();
406        r
407    }
408
409    /// Reads a struct's own keys with no allowance inherited from outside.
410    pub fn own_keys<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
411        let saved = std::mem::take(&mut self.allowed);
412        let r = f(self);
413        self.allowed = saved;
414        r
415    }
416}
417
418fn found_text(v: &Value) -> String {
419    match v {
420        Value::Str(s) if s.chars().count() <= 40 => format!("{s:?}"),
421        Value::Int(n) => n.to_string(),
422        Value::Float(f) => f.to_string(),
423        Value::Bool(b) => b.to_string(),
424        other => other.kind_name().to_string(),
425    }
426}
427
428/// The `known` name closest to `key`, if it's close enough to be a typo
429/// (`txt` → `text`, `img` → `image`, `colour` → `color`).
430fn closest<'k>(key: &str, known: &[&'k str]) -> Option<&'k str> {
431    let lower = key.to_ascii_lowercase();
432    known
433        .iter()
434        .map(|k| (edit_distance(&lower, k), *k))
435        .filter(|(d, k)| *d <= if k.len() <= 3 { 1 } else { k.len() / 2 })
436        .min_by_key(|(d, _)| *d)
437        .map(|(_, k)| k)
438}
439
440fn edit_distance(a: &str, b: &str) -> usize {
441    let b: Vec<char> = b.chars().collect();
442    let mut prev: Vec<usize> = (0..=b.len()).collect();
443    for (i, ca) in a.chars().enumerate() {
444        let mut cur = vec![i + 1; b.len() + 1];
445        for (j, cb) in b.iter().enumerate() {
446            cur[j + 1] = (prev[j] + usize::from(ca != *cb))
447                .min(prev[j + 1] + 1)
448                .min(cur[j] + 1);
449        }
450        prev = cur;
451    }
452    prev[b.len()]
453}
454
455/// Problems found reading a document, with where each is.
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct Errors(pub Vec<Located>);
458
459/// A problem and its place in the source.
460#[derive(Debug, Clone, PartialEq, Eq)]
461pub struct Located {
462    pub at: Option<LineCol>,
463    pub path: Path,
464    pub message: String,
465}
466
467impl fmt::Display for Located {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        if let Some(at) = self.at {
470            write!(f, "{at}: ")?;
471        }
472        if !self.path.is_root() {
473            write!(f, "{}: ", self.path)?;
474        }
475        f.write_str(&self.message)
476    }
477}
478
479impl fmt::Display for Errors {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        for (i, e) in self.0.iter().enumerate() {
482            if i > 0 {
483                f.write_str("\n")?;
484            }
485            write!(f, "{e}")?;
486        }
487        Ok(())
488    }
489}
490
491impl std::error::Error for Errors {}
492
493/// What reading a document gives: the value and the references in it.
494#[derive(Debug)]
495pub struct Read<T> {
496    pub value: T,
497    pub refs: Vec<Found<Ref>>,
498    pub tags: Vec<Found<Ref>>,
499    pub resources: Vec<Found<Res>>,
500}
501
502/// Reads `v` as `T`: the value and its references, or every problem.
503pub fn from_value<T: Schema>(v: &Value) -> Result<Read<T>, Vec<Problem>> {
504    let mut cx = Reader::new();
505    let value = T::read(v, &mut cx);
506    match value {
507        Some(value) if cx.problems.is_empty() => Ok(Read {
508            value,
509            refs: cx.refs,
510            tags: cx.tags,
511            resources: cx.resources,
512        }),
513        _ => Err(cx.problems),
514    }
515}
516
517/// Parses `text` and reads it as `T`; problems come with their line and column.
518pub fn from_text<T: Schema>(text: &str) -> Result<Read<T>, Errors> {
519    let doc = text::parse(text).map_err(|e| {
520        Errors(vec![Located {
521            at: Some(e.at),
522            path: Path::root(),
523            message: e.message,
524        }])
525    })?;
526    from_doc(text, &doc)
527}
528
529/// Parses TOML `text` and reads it as `T`.
530#[cfg(feature = "toml")]
531pub fn from_toml<T: Schema>(text: &str) -> Result<Read<T>, Errors> {
532    let doc = crate::toml::parse(text).map_err(|e| {
533        Errors(vec![Located {
534            at: Some(e.at),
535            path: Path::root(),
536            message: e.message,
537        }])
538    })?;
539    from_doc(text, &doc)
540}
541
542/// Reads a parsed document as `T`; `text` is its source, for positions.
543pub fn from_doc<T: Schema>(text: &str, doc: &Doc) -> Result<Read<T>, Errors> {
544    from_value(&doc.value).map_err(|problems| {
545        let mut located: Vec<(u32, Located)> = problems
546            .into_iter()
547            .map(|p| {
548                let spans = doc.spans_at(&p.path);
549                let span = if p.at_key {
550                    spans.key.unwrap_or(spans.span)
551                } else {
552                    spans.span
553                };
554                (
555                    span.start,
556                    Located {
557                        at: Some(LineCol::of(text, span.start as usize)),
558                        path: p.path,
559                        message: p.message,
560                    },
561                )
562            })
563            .collect();
564        // In file order, as an editor lists them.
565        located.sort_by_key(|(offset, _)| *offset);
566        Errors(located.into_iter().map(|(_, l)| l).collect())
567    })
568}
569
570/// `value` as text, indented.
571pub fn to_text<T: Schema>(value: &T) -> String {
572    text::to_string_pretty(&value.write())
573}
574
575// ── Plain types ─────────────────────────────────────────────────────────────
576
577impl Schema for Value {
578    fn shape() -> Shape {
579        Shape::Any
580    }
581    fn read(v: &Value, _: &mut Reader) -> Option<Self> {
582        Some(v.clone())
583    }
584    fn write(&self) -> Value {
585        self.clone()
586    }
587}
588
589impl Schema for bool {
590    fn shape() -> Shape {
591        Shape::Bool
592    }
593    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
594        v.as_bool().or_else(|| {
595            cx.expected("true or false", v);
596            None
597        })
598    }
599    fn write(&self) -> Value {
600        Value::Bool(*self)
601    }
602}
603
604macro_rules! int_schema {
605    ($($t:ty),*) => {$(
606        impl Schema for $t {
607            fn shape() -> Shape {
608                Shape::Int { min: <$t>::MIN as i64, max: (<$t>::MAX as u64).min(i64::MAX as u64) as i64 }
609            }
610            fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
611                let Some(n) = v.as_i64() else {
612                    cx.expected("an integer", v);
613                    return None;
614                };
615                <$t>::try_from(n).ok().or_else(|| {
616                    cx.problem(format!("{n} is out of range ({}..={})", <$t>::MIN, <$t>::MAX));
617                    None
618                })
619            }
620            fn write(&self) -> Value {
621                Value::Int(*self as i64)
622            }
623        }
624    )*};
625}
626int_schema!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
627
628macro_rules! float_schema {
629    ($($t:ty),*) => {$(
630        impl Schema for $t {
631            fn shape() -> Shape {
632                Shape::Float
633            }
634            fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
635                v.as_f64().map(|f| f as $t).or_else(|| {
636                    cx.expected("a number", v);
637                    None
638                })
639            }
640            fn write(&self) -> Value {
641                Value::Float(*self as f64)
642            }
643        }
644    )*};
645}
646float_schema!(f32, f64);
647
648impl Schema for String {
649    fn shape() -> Shape {
650        Shape::Str
651    }
652    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
653        match v {
654            Value::Str(s) => match typed::classify(s) {
655                Ok(Value::Str(s)) => Some(s),
656                Ok(other) => {
657                    cx.problem(format!(
658                        "expected plain text, found {} (write `@@` for a literal `@`)",
659                        other.kind_name()
660                    ));
661                    None
662                }
663                Err(e) => {
664                    cx.problem(e);
665                    None
666                }
667            },
668            other => {
669                cx.expected("a string", other);
670                None
671            }
672        }
673    }
674    fn write(&self) -> Value {
675        Value::Str(self.clone())
676    }
677}
678
679impl<T: Schema> Schema for Option<T> {
680    fn shape() -> Shape {
681        Shape::Optional(T::shape)
682    }
683    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
684        if v.is_null() {
685            Some(None)
686        } else {
687            T::read(v, cx).map(Some)
688        }
689    }
690    fn write(&self) -> Value {
691        self.as_ref().map_or(Value::Null, T::write)
692    }
693    fn absent() -> Option<Self> {
694        Some(None)
695    }
696    fn skip(&self) -> bool {
697        self.is_none()
698    }
699}
700
701impl<T: Schema> Schema for Box<T> {
702    fn shape() -> Shape {
703        T::shape()
704    }
705    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
706        T::read(v, cx).map(Box::new)
707    }
708    fn write(&self) -> Value {
709        (**self).write()
710    }
711    fn absent() -> Option<Self> {
712        T::absent().map(Box::new)
713    }
714    fn skip(&self) -> bool {
715        (**self).skip()
716    }
717}
718
719impl<T: Schema> Schema for Vec<T> {
720    fn shape() -> Shape {
721        Shape::List(T::shape)
722    }
723    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
724        let Value::List(items) = v else {
725            cx.expected("a list", v);
726            return None;
727        };
728        let mut out = Vec::with_capacity(items.len());
729        let mut ok = true;
730        for (i, item) in items.iter().enumerate() {
731            match cx.at(Seg::Index(i), |cx| T::read(item, cx)) {
732                Some(x) => out.push(x),
733                None => ok = false,
734            }
735        }
736        ok.then_some(out)
737    }
738    fn write(&self) -> Value {
739        Value::List(self.iter().map(T::write).collect())
740    }
741}
742
743impl<T: Schema, const N: usize> Schema for [T; N] {
744    fn shape() -> Shape {
745        Shape::List(T::shape)
746    }
747    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
748        let items = Vec::<T>::read(v, cx)?;
749        let len = items.len();
750        items.try_into().ok().or_else(|| {
751            cx.problem(format!("expected {N} items, found {len}"));
752            None
753        })
754    }
755    fn write(&self) -> Value {
756        Value::List(self.iter().map(T::write).collect())
757    }
758}
759
760fn read_map<T: Schema>(v: &Value, cx: &mut Reader, mut put: impl FnMut(String, T)) -> Option<()> {
761    let map = cx.map(v)?;
762    let mut ok = true;
763    for (k, item) in map.iter() {
764        if is_metadata_key(k) {
765            continue;
766        }
767        match cx.at(Seg::Key(k.to_string()), |cx| T::read(item, cx)) {
768            Some(x) => put(k.to_string(), x),
769            None => ok = false,
770        }
771    }
772    ok.then_some(())
773}
774
775impl<T: Schema> Schema for BTreeMap<String, T> {
776    fn shape() -> Shape {
777        Shape::Map(T::shape)
778    }
779    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
780        let mut out = BTreeMap::new();
781        read_map(v, cx, |k, x| {
782            out.insert(k, x);
783        })?;
784        Some(out)
785    }
786    fn write(&self) -> Value {
787        Value::Map(self.iter().map(|(k, v)| (k.clone(), v.write())).collect())
788    }
789}
790
791impl<T: Schema> Schema for HashMap<String, T> {
792    fn shape() -> Shape {
793        Shape::Map(T::shape)
794    }
795    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
796        let mut out = HashMap::new();
797        read_map(v, cx, |k, x| {
798            out.insert(k, x);
799        })?;
800        Some(out)
801    }
802    fn write(&self) -> Value {
803        let mut keys: Vec<&String> = self.keys().collect();
804        keys.sort();
805        Value::Map(
806            keys.into_iter()
807                .map(|k| (k.clone(), self[k].write()))
808                .collect(),
809        )
810    }
811}
812
813/// An object kept in file order, values of one schema.
814#[derive(Debug, Clone, PartialEq)]
815pub struct Ordered<T>(pub Vec<(String, T)>);
816
817impl<T> Default for Ordered<T> {
818    fn default() -> Self {
819        Self(Vec::new())
820    }
821}
822
823impl<T: Schema> Schema for Ordered<T> {
824    fn shape() -> Shape {
825        Shape::Map(T::shape)
826    }
827    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
828        let mut out = Vec::new();
829        read_map(v, cx, |k, x| out.push((k, x)))?;
830        Some(Ordered(out))
831    }
832    fn write(&self) -> Value {
833        Value::Map(self.0.iter().map(|(k, v)| (k.clone(), v.write())).collect())
834    }
835}
836
837// ── Typed values ────────────────────────────────────────────────────────────
838
839/// A kind of content: `item`, `block`, ... — a mod declares its own with
840/// [`kind!`](crate::kind).
841pub trait Kind {
842    const NAME: &'static str;
843}
844
845/// Declares kinds of content: `yog_data::kind!(Pattern = "pattern");`.
846#[macro_export]
847macro_rules! kind {
848    ($($name:ident = $kind:literal),* $(,)?) => {$(
849        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
850        pub struct $name;
851        impl $crate::Kind for $name {
852            const NAME: &'static str = $kind;
853        }
854    )*};
855}
856
857/// The kinds every Yog format knows.
858pub mod kinds {
859    crate::kind! {
860        Item = "item",
861        Block = "block",
862        Entity = "entity",
863        Recipe = "recipe",
864        Entry = "entry",
865        Category = "category",
866        Fluid = "fluid",
867        Biome = "biome",
868        Dimension = "dimension",
869    }
870}
871
872/// The id of content of kind `K`: written `@item:minecraft:stick` or, in a
873/// field that can only be an item, just `minecraft:stick` (`stick`).
874pub struct Id<K> {
875    pub id: String,
876    kind: PhantomData<K>,
877}
878
879pub type ItemId = Id<kinds::Item>;
880pub type BlockId = Id<kinds::Block>;
881pub type EntityId = Id<kinds::Entity>;
882pub type RecipeId = Id<kinds::Recipe>;
883
884impl<K> Id<K> {
885    pub fn new(id: impl Into<String>) -> Self {
886        Self {
887            id: id.into(),
888            kind: PhantomData,
889        }
890    }
891}
892
893impl<K> Clone for Id<K> {
894    fn clone(&self) -> Self {
895        Self::new(self.id.clone())
896    }
897}
898impl<K> PartialEq for Id<K> {
899    fn eq(&self, o: &Self) -> bool {
900        self.id == o.id
901    }
902}
903impl<K> Eq for Id<K> {}
904impl<K> std::hash::Hash for Id<K> {
905    fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
906        self.id.hash(h);
907    }
908}
909impl<K: Kind> fmt::Debug for Id<K> {
910    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911        write!(f, "@{}:{}", K::NAME, self.id)
912    }
913}
914impl<K> fmt::Display for Id<K> {
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        f.write_str(&self.id)
917    }
918}
919
920/// `v` as content of kind `kind`: a reference of that kind, or (`bare`) a bare id.
921fn read_ref(v: &Value, cx: &mut Reader, kind: &str) -> Option<Ref> {
922    let r = match v {
923        Value::Ref(r) => r.clone(),
924        Value::Str(s) if s.starts_with('@') => match typed::classify(s) {
925            Ok(Value::Ref(r)) => r,
926            Ok(other) => {
927                cx.problem(format!(
928                    "expected {kind} `@{kind}:id`, found {}",
929                    other.kind_name()
930                ));
931                return None;
932            }
933            Err(e) => {
934                cx.problem(e);
935                return None;
936            }
937        },
938        Value::Str(s) => match typed::resource_id(s, s) {
939            Ok(id) => Ref {
940                kind: kind.to_string(),
941                id,
942            },
943            Err(e) => {
944                cx.problem(e);
945                return None;
946            }
947        },
948        other => {
949            cx.expected(&format!("an id of {kind}"), other);
950            return None;
951        }
952    };
953    if !kind.is_empty() && r.kind != kind {
954        cx.problem(format!("expected {kind}, found {} `{}`", r.kind, r.id));
955        return None;
956    }
957    cx.note_ref(r.clone());
958    Some(r)
959}
960
961fn read_tag(v: &Value, cx: &mut Reader, kind: &str) -> Option<Ref> {
962    let r = match v {
963        Value::Tag(r) => r.clone(),
964        Value::Str(s) => match typed::classify(s) {
965            Ok(Value::Tag(r)) => r,
966            Ok(_) => {
967                cx.problem(format!("expected a tag `@tag:{kind}:id`, found {s:?}"));
968                return None;
969            }
970            Err(e) => {
971                cx.problem(e);
972                return None;
973            }
974        },
975        other => {
976            cx.expected("a tag", other);
977            return None;
978        }
979    };
980    if !kind.is_empty() && r.kind != kind {
981        cx.problem(format!(
982            "expected a tag of {kind}, found a tag of {}",
983            r.kind
984        ));
985        return None;
986    }
987    cx.note_tag(r.clone());
988    Some(r)
989}
990
991impl<K: Kind> Schema for Id<K> {
992    fn shape() -> Shape {
993        Shape::Ref(K::NAME)
994    }
995    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
996        read_ref(v, cx, K::NAME).map(|r| Id::new(r.id))
997    }
998    fn write(&self) -> Value {
999        Value::Ref(Ref::new(K::NAME, self.id.clone()))
1000    }
1001}
1002
1003impl Schema for Ref {
1004    fn shape() -> Shape {
1005        Shape::Ref("")
1006    }
1007    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1008        if let Value::Str(s) = v
1009            && !s.starts_with('@')
1010        {
1011            cx.problem(format!("expected a reference `@kind:id`, found {s:?}"));
1012            return None;
1013        }
1014        read_ref(v, cx, "")
1015    }
1016    fn write(&self) -> Value {
1017        Value::Ref(self.clone())
1018    }
1019}
1020
1021/// A tag of content of kind `K`: `@tag:item:minecraft:planks`.
1022pub struct TagId<K> {
1023    pub id: String,
1024    kind: PhantomData<K>,
1025}
1026
1027impl<K> TagId<K> {
1028    pub fn new(id: impl Into<String>) -> Self {
1029        Self {
1030            id: id.into(),
1031            kind: PhantomData,
1032        }
1033    }
1034}
1035impl<K> Clone for TagId<K> {
1036    fn clone(&self) -> Self {
1037        Self::new(self.id.clone())
1038    }
1039}
1040impl<K> PartialEq for TagId<K> {
1041    fn eq(&self, o: &Self) -> bool {
1042        self.id == o.id
1043    }
1044}
1045impl<K: Kind> fmt::Debug for TagId<K> {
1046    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1047        write!(f, "@tag:{}:{}", K::NAME, self.id)
1048    }
1049}
1050
1051impl<K: Kind> Schema for TagId<K> {
1052    fn shape() -> Shape {
1053        Shape::Tag(K::NAME)
1054    }
1055    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1056        read_tag(v, cx, K::NAME).map(|r| TagId::new(r.id))
1057    }
1058    fn write(&self) -> Value {
1059        Value::Tag(Ref::new(K::NAME, self.id.clone()))
1060    }
1061}
1062
1063/// Content of kind `K` or a tag of it — a recipe ingredient.
1064#[derive(Debug, Clone, PartialEq)]
1065pub enum OneOrTag<K: Kind> {
1066    One(Id<K>),
1067    Tag(TagId<K>),
1068}
1069
1070/// An item or an item tag: `"minecraft:stick"`, `"@tag:item:minecraft:planks"`.
1071pub type ItemIdOrTag = OneOrTag<kinds::Item>;
1072
1073impl<K: Kind> Schema for OneOrTag<K> {
1074    fn shape() -> Shape {
1075        Shape::RefOrTag(K::NAME)
1076    }
1077    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1078        let is_tag = match v {
1079            Value::Tag(_) => true,
1080            Value::Str(s) => s.starts_with("@tag:"),
1081            _ => false,
1082        };
1083        if is_tag {
1084            TagId::read(v, cx).map(OneOrTag::Tag)
1085        } else {
1086            Id::read(v, cx).map(OneOrTag::One)
1087        }
1088    }
1089    fn write(&self) -> Value {
1090        match self {
1091            OneOrTag::One(i) => i.write(),
1092            OneOrTag::Tag(t) => t.write(),
1093        }
1094    }
1095}
1096
1097impl Schema for Res {
1098    fn shape() -> Shape {
1099        Shape::Res
1100    }
1101    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1102        let r = match v {
1103            Value::Res(r) => r.clone(),
1104            Value::Str(s) => match typed::classify(s) {
1105                Ok(Value::Res(r)) => r,
1106                Ok(_) => {
1107                    cx.problem(format!("expected a resource `@Set.key`, found {s:?}"));
1108                    return None;
1109                }
1110                Err(e) => {
1111                    cx.problem(e);
1112                    return None;
1113                }
1114            },
1115            other => {
1116                cx.expected("a resource `@Set.key`", other);
1117                return None;
1118            }
1119        };
1120        cx.note_res(r.clone());
1121        Some(r)
1122    }
1123    fn write(&self) -> Value {
1124        Value::Res(self.clone())
1125    }
1126}
1127
1128/// A text a player reads: Yog text as written, a `.yres` text, or the name
1129/// of some content (`@item:minecraft:stick` shows "Stick").
1130#[derive(Debug, Clone, PartialEq)]
1131pub enum Text {
1132    Literal(String),
1133    Res(Res),
1134    NameOf(Ref),
1135}
1136
1137impl Default for Text {
1138    fn default() -> Self {
1139        Text::Literal(String::new())
1140    }
1141}
1142
1143impl Schema for Text {
1144    fn shape() -> Shape {
1145        Shape::Text
1146    }
1147    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1148        let typed = match v {
1149            Value::Str(s) => typed::classify(s).map_err(|e| cx.problem(e)).ok()?,
1150            Value::Text(t) => return Some(Text::Literal(t.clone())),
1151            other => other.clone(),
1152        };
1153        match typed {
1154            Value::Str(s) => Some(Text::Literal(s)),
1155            Value::Res(r) => {
1156                cx.note_res(r.clone());
1157                Some(Text::Res(r))
1158            }
1159            Value::Ref(r) => {
1160                cx.note_ref(r.clone());
1161                Some(Text::NameOf(r))
1162            }
1163            other => {
1164                cx.expected("a text", &other);
1165                None
1166            }
1167        }
1168    }
1169    fn write(&self) -> Value {
1170        match self {
1171            Text::Literal(s) => Value::Text(s.clone()),
1172            Text::Res(r) => Value::Res(r.clone()),
1173            Text::NameOf(r) => Value::Ref(r.clone()),
1174        }
1175    }
1176}
1177
1178/// An image: a source (`sprite:button`, `mymod:gui/logo`, `item:minecraft:stick`)
1179/// or a `.yres` image.
1180#[derive(Debug, Clone, PartialEq)]
1181pub enum Image {
1182    Source(String),
1183    Res(Res),
1184}
1185
1186impl Schema for Image {
1187    fn shape() -> Shape {
1188        Shape::Image
1189    }
1190    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1191        match v {
1192            Value::Image(s) => Some(Image::Source(s.clone())),
1193            Value::Res(r) => {
1194                cx.note_res(r.clone());
1195                Some(Image::Res(r.clone()))
1196            }
1197            Value::Str(s) if !s.starts_with('@') => Some(Image::Source(s.clone())),
1198            Value::Str(s) => match typed::classify(s) {
1199                Ok(Value::Image(src)) => Some(Image::Source(src)),
1200                Ok(Value::Res(r)) => {
1201                    cx.note_res(r.clone());
1202                    Some(Image::Res(r))
1203                }
1204                Ok(Value::Str(s)) => Some(Image::Source(s)),
1205                Ok(other) => {
1206                    cx.expected("an image", &other);
1207                    None
1208                }
1209                Err(e) => {
1210                    cx.problem(e);
1211                    None
1212                }
1213            },
1214            other => {
1215                cx.expected("an image", other);
1216                None
1217            }
1218        }
1219    }
1220    fn write(&self) -> Value {
1221        match self {
1222            Image::Source(s) => Value::Image(s.clone()),
1223            Image::Res(r) => Value::Res(r.clone()),
1224        }
1225    }
1226}
1227
1228/// A color: `#RRGGBB`, `#AARRGGBB`, or a `.yres` color.
1229#[derive(Debug, Clone, PartialEq)]
1230pub enum Color {
1231    Argb(u32),
1232    Res(Res),
1233}
1234
1235impl Schema for Color {
1236    fn shape() -> Shape {
1237        Shape::Color
1238    }
1239    fn read(v: &Value, cx: &mut Reader) -> Option<Self> {
1240        match v {
1241            Value::Color(c) => Some(Color::Argb(*c)),
1242            Value::Res(r) => {
1243                cx.note_res(r.clone());
1244                Some(Color::Res(r.clone()))
1245            }
1246            Value::Str(s) if s.starts_with('#') => {
1247                typed::parse_color(s).map(Color::Argb).or_else(|| {
1248                    cx.problem(format!("`{s}` isn't a color (#RRGGBB or #AARRGGBB)"));
1249                    None
1250                })
1251            }
1252            Value::Str(s) if s.starts_with('@') => Res::read(v, cx).map(Color::Res),
1253            other => {
1254                cx.expected("a color (#RRGGBB or #AARRGGBB)", other);
1255                None
1256            }
1257        }
1258    }
1259    fn write(&self) -> Value {
1260        match self {
1261            Color::Argb(c) => Value::Color(*c),
1262            Color::Res(r) => Value::Res(r.clone()),
1263        }
1264    }
1265}