Skip to main content

yog_data/schema/
typed_values.rs

1//! The schemas of typed values: references, tags, resources, texts, images, colors.
2use super::{PhantomData, Reader, Ref, Res, Schema, Shape, Value, ValueRef, fmt, typed};
3
4// ── Typed values ────────────────────────────────────────────────────────────
5
6/// A kind of content: `item`, `block`, ... — a mod declares its own with
7/// [`kind!`](crate::kind).
8pub trait Kind {
9    const NAME: &'static str;
10}
11
12/// One kind or several (`(Item, Block)`): what a reference field takes.
13pub trait Kinds {
14    const NAMES: &'static [&'static str];
15}
16
17impl<K: Kind> Kinds for K {
18    const NAMES: &'static [&'static str] = &[K::NAME];
19}
20
21impl<A: Kind, B: Kind> Kinds for (A, B) {
22    const NAMES: &'static [&'static str] = &[A::NAME, B::NAME];
23}
24
25impl<A: Kind, B: Kind, C: Kind> Kinds for (A, B, C) {
26    const NAMES: &'static [&'static str] = &[A::NAME, B::NAME, C::NAME];
27}
28
29/// Declares kinds of content: `yog_data::kind!(Pattern = "pattern");`.
30#[macro_export]
31macro_rules! kind {
32    ($($name:ident = $kind:expr),* $(,)?) => {$(
33        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
34        pub struct $name;
35        impl $crate::Kind for $name {
36            const NAME: &'static str = $kind;
37        }
38    )*};
39}
40
41/// The kinds every Yog format knows.
42pub mod kinds {
43    crate::kind! {
44        Item = yog_ids::kind::ITEM,
45        Block = yog_ids::kind::BLOCK,
46        Entity = yog_ids::kind::ENTITY,
47        Recipe = yog_ids::kind::RECIPE,
48        Entry = yog_ids::kind::ENTRY,
49        Category = yog_ids::kind::CATEGORY,
50        Fluid = yog_ids::kind::FLUID,
51        Biome = yog_ids::kind::BIOME,
52        Dimension = yog_ids::kind::DIMENSION,
53        Texture = yog_ids::kind::TEXTURE,
54        Video = yog_ids::kind::VIDEO,
55        Material = yog_ids::kind::MATERIAL,
56        Advancement = yog_ids::kind::ADVANCEMENT,
57    }
58}
59
60/// The id of content of kind `K`: written `@item:minecraft:stick` or, in a
61/// field that can only be an item, just `minecraft:stick` (`stick`).
62pub struct Id<K> {
63    pub id: String,
64    kind: PhantomData<K>,
65}
66
67pub type ItemId = Id<kinds::Item>;
68pub type BlockId = Id<kinds::Block>;
69pub type EntityId = Id<kinds::Entity>;
70pub type RecipeId = Id<kinds::Recipe>;
71
72impl<K> Id<K> {
73    pub fn new(id: impl Into<String>) -> Self {
74        Self {
75            id: id.into(),
76            kind: PhantomData,
77        }
78    }
79}
80
81impl<K> Clone for Id<K> {
82    fn clone(&self) -> Self {
83        Self::new(self.id.clone())
84    }
85}
86impl<K> PartialEq for Id<K> {
87    fn eq(&self, o: &Self) -> bool {
88        self.id == o.id
89    }
90}
91impl<K> Eq for Id<K> {}
92impl<K> std::hash::Hash for Id<K> {
93    fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
94        self.id.hash(h);
95    }
96}
97impl<K: Kind> fmt::Debug for Id<K> {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "@{}:{}", K::NAME, self.id)
100    }
101}
102impl<K> fmt::Display for Id<K> {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str(&self.id)
105    }
106}
107
108/// `v` as a reference to content of one of `kinds` (any kind if empty),
109/// written `@kind:id` — a bare id is an error that says how to write it.
110pub fn read_ref(v: &ValueRef, cx: &mut Reader, kinds: &[&str]) -> Option<Ref> {
111    let wanted = || {
112        kinds
113            .iter()
114            .map(|k| format!("`@{k}:`"))
115            .collect::<Vec<_>>()
116            .join(" or ")
117    };
118    let r = match v {
119        ValueRef::Ref { kind, id } => Ref::new(*kind, *id),
120        ValueRef::Str(s) if s.starts_with('@') => match typed::classify(s) {
121            Ok(Value::Ref(r)) => r,
122            Ok(other) => {
123                cx.problem(format!(
124                    "expected a reference ({}), found {}",
125                    if kinds.is_empty() {
126                        "`@kind:id`".into()
127                    } else {
128                        wanted()
129                    },
130                    other.kind_name()
131                ));
132                return None;
133            }
134            Err(e) => {
135                cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
136                return None;
137            }
138        },
139        ValueRef::Str(s) => {
140            let kind = kinds.first().copied().unwrap_or(yog_ids::kind::ITEM);
141            cx.problem_of(
142                yog_ids::data_problem::NOT_A_REFERENCE,
143                format!("`{s}`: name it by a reference — `@{kind}:{s}`"),
144            );
145            return None;
146        }
147        other => {
148            cx.expected("a reference `@kind:id`", other);
149            return None;
150        }
151    };
152    if !kinds.is_empty() && !kinds.contains(&r.kind.as_str()) {
153        cx.problem_of(
154            yog_ids::data_problem::WRONG_KIND,
155            format!(
156                "`@{}:{}` is a reference to {} — this takes {}",
157                r.kind,
158                r.id,
159                r.kind,
160                wanted()
161            ),
162        );
163        return None;
164    }
165    cx.note_ref(r.clone());
166    Some(r)
167}
168
169fn read_tag(v: &ValueRef, cx: &mut Reader, kind: &str) -> Option<Ref> {
170    let r = match v {
171        ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
172        ValueRef::Str(s) => match typed::classify(s) {
173            Ok(Value::Tag(r)) => r,
174            Ok(_) => {
175                cx.problem(format!("expected a tag `@tag:{kind}:id`, found {s:?}"));
176                return None;
177            }
178            Err(e) => {
179                cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
180                return None;
181            }
182        },
183        other => {
184            cx.expected("a tag", other);
185            return None;
186        }
187    };
188    if !kind.is_empty() && r.kind != kind {
189        cx.problem(format!(
190            "expected a tag of {kind}, found a tag of {}",
191            r.kind
192        ));
193        return None;
194    }
195    cx.note_tag(r.clone());
196    Some(r)
197}
198
199impl<K: Kind> Schema for Id<K> {
200    fn shape() -> Shape {
201        Shape::Ref(<K as Kinds>::NAMES)
202    }
203    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
204        read_ref(v, cx, <K as Kinds>::NAMES).map(|r| Id::new(r.id))
205    }
206    fn write(&self) -> Value {
207        Value::Ref(Ref::new(K::NAME, self.id.clone()))
208    }
209}
210
211impl Schema for Ref {
212    fn shape() -> Shape {
213        Shape::Ref(&[])
214    }
215    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
216        if let ValueRef::Str(s) = v
217            && !s.starts_with('@')
218        {
219            cx.problem(format!("expected a reference `@kind:id`, found {s:?}"));
220            return None;
221        }
222        read_ref(v, cx, &[])
223    }
224    fn write(&self) -> Value {
225        Value::Ref(self.clone())
226    }
227}
228
229/// Content of any kind or a tag of it: `@entity:minecraft:pig`,
230/// `@tag:entity:c:bosses` — a tag's entries, whose kind the tag says.
231#[derive(Debug, Clone, PartialEq)]
232pub enum AnyRefOrTag {
233    One(Ref),
234    Tag(Ref),
235}
236
237impl AnyRefOrTag {
238    /// The kind it names (`entity`).
239    pub fn kind(&self) -> &str {
240        match self {
241            AnyRefOrTag::One(r) | AnyRefOrTag::Tag(r) => &r.kind,
242        }
243    }
244}
245
246impl fmt::Display for AnyRefOrTag {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            AnyRefOrTag::One(r) => write!(f, "@{}:{}", r.kind, r.id),
250            AnyRefOrTag::Tag(r) => write!(f, "@tag:{}:{}", r.kind, r.id),
251        }
252    }
253}
254
255impl Schema for AnyRefOrTag {
256    fn shape() -> Shape {
257        Shape::RefOrTag(&[])
258    }
259    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
260        let is_tag = match v {
261            ValueRef::Tag { .. } => true,
262            ValueRef::Str(s) => s.starts_with("@tag:"),
263            _ => false,
264        };
265        if is_tag {
266            let r = match v {
267                ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
268                ValueRef::Str(s) => match typed::classify(s) {
269                    Ok(Value::Tag(r)) => r,
270                    Ok(_) => {
271                        cx.problem(format!("expected a tag `@tag:kind:id`, found {s:?}"));
272                        return None;
273                    }
274                    Err(e) => {
275                        cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
276                        return None;
277                    }
278                },
279                _ => return None,
280            };
281            return Some(AnyRefOrTag::Tag(r));
282        }
283        if let ValueRef::Str(s) = v
284            && !s.starts_with('@')
285        {
286            cx.problem(format!(
287                "`{s}`: write `@<kind>:{s}` or a tag `@tag:<kind>:{s}`"
288            ));
289            return None;
290        }
291        read_ref(v, cx, &[]).map(AnyRefOrTag::One)
292    }
293    fn write(&self) -> Value {
294        match self {
295            AnyRefOrTag::One(r) => Value::Ref(r.clone()),
296            AnyRefOrTag::Tag(r) => Value::Tag(r.clone()),
297        }
298    }
299}
300
301/// A tag of content of kind `K`: `@tag:item:minecraft:planks`.
302pub struct TagId<K> {
303    pub id: String,
304    kind: PhantomData<K>,
305}
306
307impl<K> TagId<K> {
308    pub fn new(id: impl Into<String>) -> Self {
309        Self {
310            id: id.into(),
311            kind: PhantomData,
312        }
313    }
314}
315impl<K> Clone for TagId<K> {
316    fn clone(&self) -> Self {
317        Self::new(self.id.clone())
318    }
319}
320impl<K> PartialEq for TagId<K> {
321    fn eq(&self, o: &Self) -> bool {
322        self.id == o.id
323    }
324}
325impl<K: Kind> fmt::Debug for TagId<K> {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        write!(f, "@tag:{}:{}", K::NAME, self.id)
328    }
329}
330
331impl<K: Kind> Schema for TagId<K> {
332    fn shape() -> Shape {
333        Shape::Tag(<K as Kinds>::NAMES)
334    }
335    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
336        read_tag(v, cx, K::NAME).map(|r| TagId::new(r.id))
337    }
338    fn write(&self) -> Value {
339        Value::Tag(Ref::new(K::NAME, self.id.clone()))
340    }
341}
342
343/// Content of kind `K` or a tag of it — a recipe ingredient.
344#[derive(Debug, Clone, PartialEq)]
345pub enum OneOrTag<K: Kind> {
346    One(Id<K>),
347    Tag(TagId<K>),
348}
349
350/// An item or an item tag: `"minecraft:stick"`, `"@tag:item:minecraft:planks"`.
351pub type ItemIdOrTag = OneOrTag<kinds::Item>;
352
353impl<K: Kind> Schema for OneOrTag<K> {
354    fn shape() -> Shape {
355        Shape::RefOrTag(<K as Kinds>::NAMES)
356    }
357    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
358        if let ValueRef::Str(s) = v
359            && let Some(tag) = s.strip_prefix('#')
360        {
361            cx.problem(format!("`{s}`: write a tag as `@tag:{}:{tag}`", K::NAME));
362            return None;
363        }
364        let is_tag = match v {
365            ValueRef::Tag { .. } => true,
366            ValueRef::Str(s) => s.starts_with("@tag:"),
367            _ => false,
368        };
369        if is_tag {
370            TagId::read(v, cx).map(OneOrTag::Tag)
371        } else {
372            Id::read(v, cx).map(OneOrTag::One)
373        }
374    }
375    fn write(&self) -> Value {
376        match self {
377            OneOrTag::One(i) => i.write(),
378            OneOrTag::Tag(t) => t.write(),
379        }
380    }
381}
382
383impl Schema for Res {
384    fn shape() -> Shape {
385        Shape::Res
386    }
387    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
388        let r = match v {
389            ValueRef::Res { set, key } => Res {
390                set: set.to_string(),
391                key: key.to_string(),
392            },
393            ValueRef::Str(s) => match typed::classify(s) {
394                Ok(Value::Res(r)) => r,
395                Ok(_) => {
396                    cx.problem(format!("expected a resource `@Set.key`, found {s:?}"));
397                    return None;
398                }
399                Err(e) => {
400                    cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
401                    return None;
402                }
403            },
404            other => {
405                cx.expected("a resource `@Set.key`", other);
406                return None;
407            }
408        };
409        cx.note_res(r.clone());
410        Some(r)
411    }
412    fn write(&self) -> Value {
413        Value::Res(self.clone())
414    }
415}
416
417/// A text a player reads: Yog text as written, a `.yres` text, the name
418/// of some content (`@item:minecraft:stick` shows "Stick"), or any
419/// translation key (`@lang:gui.done`).
420#[derive(Debug, Clone, PartialEq)]
421pub enum Text {
422    Literal(String),
423    Res(Res),
424    NameOf(Ref),
425    Lang(String),
426}
427
428impl Default for Text {
429    fn default() -> Self {
430        Text::Literal(String::new())
431    }
432}
433
434impl Schema for Text {
435    fn shape() -> Shape {
436        Shape::Text
437    }
438    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
439        let typed = match v {
440            // Words as they are, the usual case: copied once.
441            ValueRef::Str(s) if !s.starts_with('@') => return Some(Text::Literal(s.to_string())),
442            ValueRef::Text(t) => return Some(Text::Literal(t.to_string())),
443            ValueRef::Lang(k) => return Some(Text::Lang(k.to_string())),
444            ValueRef::Str(s) => typed::classify(s).map_err(|e| cx.problem(e)).ok()?,
445            ValueRef::Res { .. } | ValueRef::Ref { .. } => v.to_value(),
446            other => {
447                cx.expected("a text", other);
448                return None;
449            }
450        };
451        match typed {
452            Value::Str(s) => Some(Text::Literal(s)),
453            Value::Res(r) => {
454                cx.note_res(r.clone());
455                Some(Text::Res(r))
456            }
457            Value::Ref(r) => {
458                cx.note_ref(r.clone());
459                Some(Text::NameOf(r))
460            }
461            Value::Lang(k) => Some(Text::Lang(k)),
462            other => {
463                cx.problem(format!("expected a text, found {}", other.kind_name()));
464                None
465            }
466        }
467    }
468    fn write(&self) -> Value {
469        match self {
470            Text::Literal(s) => Value::Text(s.clone()),
471            Text::Res(r) => Value::Res(r.clone()),
472            Text::NameOf(r) => Value::Ref(r.clone()),
473            Text::Lang(k) => Value::Lang(k.clone()),
474        }
475    }
476}
477
478/// An image: a source (`sprite:button`, `mymod:gui/logo`, `item:minecraft:stick`)
479/// or a `.yres` image.
480#[derive(Debug, Clone, PartialEq)]
481pub enum Image {
482    Source(String),
483    Res(Res),
484}
485
486impl Schema for Image {
487    fn shape() -> Shape {
488        Shape::Image
489    }
490    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
491        match v {
492            ValueRef::Image(s) => Some(Image::Source(s.to_string())),
493            ValueRef::Res { .. } => Res::read(v, cx).map(Image::Res),
494            ValueRef::Str(s) if !s.starts_with('@') => Some(Image::Source(s.to_string())),
495            ValueRef::Str(s) => match typed::classify(s) {
496                Ok(Value::Image(src)) => Some(Image::Source(src)),
497                Ok(Value::Res(r)) => {
498                    cx.note_res(r.clone());
499                    Some(Image::Res(r))
500                }
501                Ok(Value::Str(s)) => Some(Image::Source(s)),
502                Ok(other) => {
503                    cx.problem(format!("expected an image, found {}", other.kind_name()));
504                    None
505                }
506                Err(e) => {
507                    cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
508                    None
509                }
510            },
511            other => {
512                cx.expected("an image", other);
513                None
514            }
515        }
516    }
517    fn write(&self) -> Value {
518        match self {
519            Image::Source(s) => Value::Image(s.clone()),
520            Image::Res(r) => Value::Res(r.clone()),
521        }
522    }
523}
524
525/// A color: `#RRGGBB`, `#AARRGGBB`, or a `.yres` color.
526#[derive(Debug, Clone, PartialEq)]
527pub enum Color {
528    Argb(u32),
529    Res(Res),
530}
531
532impl Schema for Color {
533    fn shape() -> Shape {
534        Shape::Color
535    }
536    fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
537        match v {
538            ValueRef::Color(c) => Some(Color::Argb(*c)),
539            ValueRef::Res { .. } => Res::read(v, cx).map(Color::Res),
540            ValueRef::Str(s) if s.starts_with('#') => {
541                typed::parse_color(s).map(Color::Argb).or_else(|| {
542                    cx.problem(format!("`{s}` isn't a color (#RRGGBB or #AARRGGBB)"));
543                    None
544                })
545            }
546            ValueRef::Str(s) if s.starts_with('@') => Res::read(v, cx).map(Color::Res),
547            other => {
548                cx.expected("a color (#RRGGBB or #AARRGGBB)", other);
549                None
550            }
551        }
552    }
553    fn write(&self) -> Value {
554        match self {
555            Color::Argb(c) => Value::Color(*c),
556            Color::Res(r) => Value::Res(r.clone()),
557        }
558    }
559}