1use super::{PhantomData, Reader, Ref, Res, Schema, Shape, Value, ValueRef, fmt, typed};
3
4pub trait Kind {
9 const NAME: &'static str;
10}
11
12pub 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#[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
41pub 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
60pub 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
108pub 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#[derive(Debug, Clone, PartialEq)]
232pub enum AnyRefOrTag {
233 One(Ref),
234 Tag(Ref),
235}
236
237impl AnyRefOrTag {
238 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
301pub 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#[derive(Debug, Clone, PartialEq)]
345pub enum OneOrTag<K: Kind> {
346 One(Id<K>),
347 Tag(TagId<K>),
348}
349
350pub 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#[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 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#[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#[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}