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 Material = yog_ids::kind::MATERIAL,
55 Advancement = yog_ids::kind::ADVANCEMENT,
56 }
57}
58
59pub struct Id<K> {
62 pub id: String,
63 kind: PhantomData<K>,
64}
65
66pub type ItemId = Id<kinds::Item>;
67pub type BlockId = Id<kinds::Block>;
68pub type EntityId = Id<kinds::Entity>;
69pub type RecipeId = Id<kinds::Recipe>;
70
71impl<K> Id<K> {
72 pub fn new(id: impl Into<String>) -> Self {
73 Self {
74 id: id.into(),
75 kind: PhantomData,
76 }
77 }
78}
79
80impl<K> Clone for Id<K> {
81 fn clone(&self) -> Self {
82 Self::new(self.id.clone())
83 }
84}
85impl<K> PartialEq for Id<K> {
86 fn eq(&self, o: &Self) -> bool {
87 self.id == o.id
88 }
89}
90impl<K> Eq for Id<K> {}
91impl<K> std::hash::Hash for Id<K> {
92 fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
93 self.id.hash(h);
94 }
95}
96impl<K: Kind> fmt::Debug for Id<K> {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "@{}:{}", K::NAME, self.id)
99 }
100}
101impl<K> fmt::Display for Id<K> {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.write_str(&self.id)
104 }
105}
106
107pub fn read_ref(v: &ValueRef, cx: &mut Reader, kinds: &[&str]) -> Option<Ref> {
110 let wanted = || {
111 kinds
112 .iter()
113 .map(|k| format!("`@{k}:`"))
114 .collect::<Vec<_>>()
115 .join(" or ")
116 };
117 let r = match v {
118 ValueRef::Ref { kind, id } => Ref::new(*kind, *id),
119 ValueRef::Str(s) if s.starts_with('@') => match typed::classify(s) {
120 Ok(Value::Ref(r)) => r,
121 Ok(other) => {
122 cx.problem(format!(
123 "expected a reference ({}), found {}",
124 if kinds.is_empty() {
125 "`@kind:id`".into()
126 } else {
127 wanted()
128 },
129 other.kind_name()
130 ));
131 return None;
132 }
133 Err(e) => {
134 cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
135 return None;
136 }
137 },
138 ValueRef::Str(s) => {
139 let kind = kinds.first().copied().unwrap_or(yog_ids::kind::ITEM);
140 cx.problem_of(
141 yog_ids::data_problem::NOT_A_REFERENCE,
142 format!("`{s}`: name it by a reference — `@{kind}:{s}`"),
143 );
144 return None;
145 }
146 other => {
147 cx.expected("a reference `@kind:id`", other);
148 return None;
149 }
150 };
151 if !kinds.is_empty() && !kinds.contains(&r.kind.as_str()) {
152 cx.problem_of(
153 yog_ids::data_problem::WRONG_KIND,
154 format!(
155 "`@{}:{}` is a reference to {} — this takes {}",
156 r.kind,
157 r.id,
158 r.kind,
159 wanted()
160 ),
161 );
162 return None;
163 }
164 cx.note_ref(r.clone());
165 Some(r)
166}
167
168fn read_tag(v: &ValueRef, cx: &mut Reader, kind: &str) -> Option<Ref> {
169 let r = match v {
170 ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
171 ValueRef::Str(s) => match typed::classify(s) {
172 Ok(Value::Tag(r)) => r,
173 Ok(_) => {
174 cx.problem(format!("expected a tag `@tag:{kind}:id`, found {s:?}"));
175 return None;
176 }
177 Err(e) => {
178 cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
179 return None;
180 }
181 },
182 other => {
183 cx.expected("a tag", other);
184 return None;
185 }
186 };
187 if !kind.is_empty() && r.kind != kind {
188 cx.problem(format!(
189 "expected a tag of {kind}, found a tag of {}",
190 r.kind
191 ));
192 return None;
193 }
194 cx.note_tag(r.clone());
195 Some(r)
196}
197
198impl<K: Kind> Schema for Id<K> {
199 fn shape() -> Shape {
200 Shape::Ref(<K as Kinds>::NAMES)
201 }
202 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
203 read_ref(v, cx, <K as Kinds>::NAMES).map(|r| Id::new(r.id))
204 }
205 fn write(&self) -> Value {
206 Value::Ref(Ref::new(K::NAME, self.id.clone()))
207 }
208}
209
210impl Schema for Ref {
211 fn shape() -> Shape {
212 Shape::Ref(&[])
213 }
214 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
215 if let ValueRef::Str(s) = v
216 && !s.starts_with('@')
217 {
218 cx.problem(format!("expected a reference `@kind:id`, found {s:?}"));
219 return None;
220 }
221 read_ref(v, cx, &[])
222 }
223 fn write(&self) -> Value {
224 Value::Ref(self.clone())
225 }
226}
227
228#[derive(Debug, Clone, PartialEq)]
231pub enum AnyRefOrTag {
232 One(Ref),
233 Tag(Ref),
234}
235
236impl AnyRefOrTag {
237 pub fn kind(&self) -> &str {
239 match self {
240 AnyRefOrTag::One(r) | AnyRefOrTag::Tag(r) => &r.kind,
241 }
242 }
243}
244
245impl fmt::Display for AnyRefOrTag {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 match self {
248 AnyRefOrTag::One(r) => write!(f, "@{}:{}", r.kind, r.id),
249 AnyRefOrTag::Tag(r) => write!(f, "@tag:{}:{}", r.kind, r.id),
250 }
251 }
252}
253
254impl Schema for AnyRefOrTag {
255 fn shape() -> Shape {
256 Shape::RefOrTag(&[])
257 }
258 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
259 let is_tag = match v {
260 ValueRef::Tag { .. } => true,
261 ValueRef::Str(s) => s.starts_with("@tag:"),
262 _ => false,
263 };
264 if is_tag {
265 let r = match v {
266 ValueRef::Tag { kind, id } => Ref::new(*kind, *id),
267 ValueRef::Str(s) => match typed::classify(s) {
268 Ok(Value::Tag(r)) => r,
269 Ok(_) => {
270 cx.problem(format!("expected a tag `@tag:kind:id`, found {s:?}"));
271 return None;
272 }
273 Err(e) => {
274 cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
275 return None;
276 }
277 },
278 _ => return None,
279 };
280 return Some(AnyRefOrTag::Tag(r));
281 }
282 if let ValueRef::Str(s) = v
283 && !s.starts_with('@')
284 {
285 cx.problem(format!(
286 "`{s}`: write `@<kind>:{s}` or a tag `@tag:<kind>:{s}`"
287 ));
288 return None;
289 }
290 read_ref(v, cx, &[]).map(AnyRefOrTag::One)
291 }
292 fn write(&self) -> Value {
293 match self {
294 AnyRefOrTag::One(r) => Value::Ref(r.clone()),
295 AnyRefOrTag::Tag(r) => Value::Tag(r.clone()),
296 }
297 }
298}
299
300pub struct TagId<K> {
302 pub id: String,
303 kind: PhantomData<K>,
304}
305
306impl<K> TagId<K> {
307 pub fn new(id: impl Into<String>) -> Self {
308 Self {
309 id: id.into(),
310 kind: PhantomData,
311 }
312 }
313}
314impl<K> Clone for TagId<K> {
315 fn clone(&self) -> Self {
316 Self::new(self.id.clone())
317 }
318}
319impl<K> PartialEq for TagId<K> {
320 fn eq(&self, o: &Self) -> bool {
321 self.id == o.id
322 }
323}
324impl<K: Kind> fmt::Debug for TagId<K> {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "@tag:{}:{}", K::NAME, self.id)
327 }
328}
329
330impl<K: Kind> Schema for TagId<K> {
331 fn shape() -> Shape {
332 Shape::Tag(<K as Kinds>::NAMES)
333 }
334 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
335 read_tag(v, cx, K::NAME).map(|r| TagId::new(r.id))
336 }
337 fn write(&self) -> Value {
338 Value::Tag(Ref::new(K::NAME, self.id.clone()))
339 }
340}
341
342#[derive(Debug, Clone, PartialEq)]
344pub enum OneOrTag<K: Kind> {
345 One(Id<K>),
346 Tag(TagId<K>),
347}
348
349pub type ItemIdOrTag = OneOrTag<kinds::Item>;
351
352impl<K: Kind> Schema for OneOrTag<K> {
353 fn shape() -> Shape {
354 Shape::RefOrTag(<K as Kinds>::NAMES)
355 }
356 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
357 if let ValueRef::Str(s) = v
358 && let Some(tag) = s.strip_prefix('#')
359 {
360 cx.problem(format!("`{s}`: write a tag as `@tag:{}:{tag}`", K::NAME));
361 return None;
362 }
363 let is_tag = match v {
364 ValueRef::Tag { .. } => true,
365 ValueRef::Str(s) => s.starts_with("@tag:"),
366 _ => false,
367 };
368 if is_tag {
369 TagId::read(v, cx).map(OneOrTag::Tag)
370 } else {
371 Id::read(v, cx).map(OneOrTag::One)
372 }
373 }
374 fn write(&self) -> Value {
375 match self {
376 OneOrTag::One(i) => i.write(),
377 OneOrTag::Tag(t) => t.write(),
378 }
379 }
380}
381
382impl Schema for Res {
383 fn shape() -> Shape {
384 Shape::Res
385 }
386 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
387 let r = match v {
388 ValueRef::Res { set, key } => Res {
389 set: set.to_string(),
390 key: key.to_string(),
391 },
392 ValueRef::Str(s) => match typed::classify(s) {
393 Ok(Value::Res(r)) => r,
394 Ok(_) => {
395 cx.problem(format!("expected a resource `@Set.key`, found {s:?}"));
396 return None;
397 }
398 Err(e) => {
399 cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
400 return None;
401 }
402 },
403 other => {
404 cx.expected("a resource `@Set.key`", other);
405 return None;
406 }
407 };
408 cx.note_res(r.clone());
409 Some(r)
410 }
411 fn write(&self) -> Value {
412 Value::Res(self.clone())
413 }
414}
415
416#[derive(Debug, Clone, PartialEq)]
420pub enum Text {
421 Literal(String),
422 Res(Res),
423 NameOf(Ref),
424 Lang(String),
425}
426
427impl Default for Text {
428 fn default() -> Self {
429 Text::Literal(String::new())
430 }
431}
432
433impl Schema for Text {
434 fn shape() -> Shape {
435 Shape::Text
436 }
437 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
438 let typed = match v {
439 ValueRef::Str(s) if !s.starts_with('@') => return Some(Text::Literal(s.to_string())),
441 ValueRef::Text(t) => return Some(Text::Literal(t.to_string())),
442 ValueRef::Lang(k) => return Some(Text::Lang(k.to_string())),
443 ValueRef::Str(s) => typed::classify(s).map_err(|e| cx.problem(e)).ok()?,
444 ValueRef::Res { .. } | ValueRef::Ref { .. } => v.to_value(),
445 other => {
446 cx.expected("a text", other);
447 return None;
448 }
449 };
450 match typed {
451 Value::Str(s) => Some(Text::Literal(s)),
452 Value::Res(r) => {
453 cx.note_res(r.clone());
454 Some(Text::Res(r))
455 }
456 Value::Ref(r) => {
457 cx.note_ref(r.clone());
458 Some(Text::NameOf(r))
459 }
460 Value::Lang(k) => Some(Text::Lang(k)),
461 other => {
462 cx.problem(format!("expected a text, found {}", other.kind_name()));
463 None
464 }
465 }
466 }
467 fn write(&self) -> Value {
468 match self {
469 Text::Literal(s) => Value::Text(s.clone()),
470 Text::Res(r) => Value::Res(r.clone()),
471 Text::NameOf(r) => Value::Ref(r.clone()),
472 Text::Lang(k) => Value::Lang(k.clone()),
473 }
474 }
475}
476
477#[derive(Debug, Clone, PartialEq)]
480pub enum Image {
481 Source(String),
482 Res(Res),
483}
484
485impl Schema for Image {
486 fn shape() -> Shape {
487 Shape::Image
488 }
489 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
490 match v {
491 ValueRef::Image(s) => Some(Image::Source(s.to_string())),
492 ValueRef::Res { .. } => Res::read(v, cx).map(Image::Res),
493 ValueRef::Str(s) if !s.starts_with('@') => Some(Image::Source(s.to_string())),
494 ValueRef::Str(s) => match typed::classify(s) {
495 Ok(Value::Image(src)) => Some(Image::Source(src)),
496 Ok(Value::Res(r)) => {
497 cx.note_res(r.clone());
498 Some(Image::Res(r))
499 }
500 Ok(Value::Str(s)) => Some(Image::Source(s)),
501 Ok(other) => {
502 cx.problem(format!("expected an image, found {}", other.kind_name()));
503 None
504 }
505 Err(e) => {
506 cx.problem_of(yog_ids::data_problem::BAD_TYPED, e);
507 None
508 }
509 },
510 other => {
511 cx.expected("an image", other);
512 None
513 }
514 }
515 }
516 fn write(&self) -> Value {
517 match self {
518 Image::Source(s) => Value::Image(s.clone()),
519 Image::Res(r) => Value::Res(r.clone()),
520 }
521 }
522}
523
524#[derive(Debug, Clone, PartialEq)]
526pub enum Color {
527 Argb(u32),
528 Res(Res),
529}
530
531impl Schema for Color {
532 fn shape() -> Shape {
533 Shape::Color
534 }
535 fn read(v: &ValueRef<'_>, cx: &mut Reader) -> Option<Self> {
536 match v {
537 ValueRef::Color(c) => Some(Color::Argb(*c)),
538 ValueRef::Res { .. } => Res::read(v, cx).map(Color::Res),
539 ValueRef::Str(s) if s.starts_with('#') => {
540 typed::parse_color(s).map(Color::Argb).or_else(|| {
541 cx.problem(format!("`{s}` isn't a color (#RRGGBB or #AARRGGBB)"));
542 None
543 })
544 }
545 ValueRef::Str(s) if s.starts_with('@') => Res::read(v, cx).map(Color::Res),
546 other => {
547 cx.expected("a color (#RRGGBB or #AARRGGBB)", other);
548 None
549 }
550 }
551 }
552 fn write(&self) -> Value {
553 match self {
554 Color::Argb(c) => Value::Color(*c),
555 Color::Res(r) => Value::Res(r.clone()),
556 }
557 }
558}