use yog_data::schema::Found;
use yog_data::{
Color, Id, Image, ItemIdOrTag, Ref, Res, Schema, TagId, Text, Value, binary, from_text,
from_value, kinds, text,
};
#[derive(Schema, Debug, Clone, PartialEq)]
enum Mode {
Shaped,
Shapeless,
#[data(rename = "smelt")]
Smelting,
}
#[derive(Schema, Debug, Clone, PartialEq)]
struct Recipe {
result: Id<kinds::Item>,
#[data(default = "one")]
count: u32,
mode: Mode,
ingredients: Vec<ItemIdOrTag>,
}
fn one() -> u32 {
1
}
#[derive(Schema, Debug, Clone, PartialEq)]
struct Spotlight {
item: Id<kinds::Item>,
#[data(default)]
text: Option<Text>,
}
#[derive(Schema, Debug, Clone, PartialEq)]
#[data(tag = "type")]
enum Page {
Text {
text: Text,
title: Option<Text>,
},
Spotlight(Spotlight),
Recipe(Recipe),
Empty,
}
#[derive(Schema, Debug, Clone, PartialEq)]
struct Entry {
name: Text,
icon: Image,
#[data(rename = "color")]
tint: Option<Color>,
pages: Vec<Page>,
#[data(skip)]
read_count: u32,
pos: [i32; 2],
}
const BOOK: &str = r##"{
// An entry.
name: "@Strings.entry.title",
icon: "@image:item:minecraft:book",
color: "#8d6acc",
pos: [0, -2],
pages: [
{ type: "text", text: "@@everyone {b|Hello}" },
{ type: "spotlight", item: "minecraft:stick", text: "@item:minecraft:stick" },
{ type: "recipe", result: "@item:gm:ruby", mode: "smelt", ingredients: ["@tag:item:minecraft:planks", "stick"], $note: "meta" },
{ type: "empty" },
],
}"##;
#[test]
fn reads_a_document_and_its_references() {
let read = from_text::<Entry>(BOOK).unwrap_or_else(|e| panic!("{e}"));
let e = read.value;
assert_eq!(
e.name,
Text::Res(Res {
set: "Strings".into(),
key: "entry.title".into()
})
);
assert_eq!(e.icon, Image::Source("item:minecraft:book".into()));
assert_eq!(e.tint, Some(Color::Argb(0xFF8D6ACC)));
assert_eq!(e.pos, [0, -2]);
assert_eq!(
e.pages[0],
Page::Text {
text: Text::Literal("@everyone {b|Hello}".into()),
title: None
}
);
assert_eq!(
e.pages[1],
Page::Spotlight(Spotlight {
item: Id::new("minecraft:stick"),
text: Some(Text::NameOf(Ref::new("item", "minecraft:stick")))
})
);
let Page::Recipe(r) = &e.pages[2] else {
panic!()
};
assert_eq!((r.count, &r.mode), (1, &Mode::Smelting));
assert_eq!(
r.ingredients,
[
ItemIdOrTag::Tag(TagId::new("minecraft:planks")),
ItemIdOrTag::One(Id::new("minecraft:stick"))
]
);
assert_eq!(e.pages[3], Page::Empty);
let refs: Vec<String> = read
.refs
.iter()
.map(|Found { value, path }| format!("{path} {}", value.id))
.collect();
assert_eq!(
refs,
[
"pages[1].item minecraft:stick",
"pages[1].text minecraft:stick",
"pages[2].result gm:ruby",
"pages[2].ingredients[1] minecraft:stick"
]
);
assert_eq!(read.tags[0].path.to_string(), "pages[2].ingredients[0]");
assert_eq!(read.resources[0].value.key, "entry.title");
}
#[test]
fn every_problem_is_reported_with_its_place() {
let text = r##"{
name: 5,
icon: "@item:minecraft:book",
pos: [1],
colour: "#fff",
pages: [
{ type: "txt", text: "x" },
{ type: "spotlight", item: "@block:minecraft:stone", txt: "y" },
{ type: "recipe", result: "Gm:Ruby", count: -1, mode: "boil", ingredients: ["@tag:block:minecraft:logs"] },
{ text: "no type" },
],
}"##;
let errors = from_text::<Entry>(text).unwrap_err().to_string();
let expected = [
"2:9: name: expected a text, found 5",
"3:9: icon: expected an image, found a reference",
"4:8: pos: expected 2 items, found 1",
"5:3: colour: unknown field `colour` — did you mean `color`?",
"7:13: pages[0].type: unknown `txt` — did you mean `text`?",
"8:32: pages[1].item: expected item, found block `minecraft:stone`",
"8:58: pages[1].txt: unknown field `txt` — did you mean `text`?",
"9:31: pages[2].result: `Gm:Ruby`: `Gm:Ruby` isn't an id (lower case `namespace:path`)",
"9:49: pages[2].count: -1 is out of range (0..=4294967295)",
"9:59: pages[2].mode: unknown `boil` (expected `shaped`, `shapeless`, `smelt`)",
"9:81: pages[2].ingredients[0]: expected a tag of item, found a tag of block",
"10:5: pages[3]: missing `type` (`text`, `spotlight`, `recipe`, `empty`)",
];
assert_eq!(errors.lines().collect::<Vec<_>>(), expected, "\n{errors}");
}
#[test]
fn missing_fields_and_syntax_errors() {
let e = from_text::<Spotlight>("{ text: \"x\" }")
.unwrap_err()
.to_string();
assert_eq!(e, "1:1: missing `item`");
let e = from_text::<Spotlight>("{ item: \"x\",, }")
.unwrap_err()
.to_string();
assert_eq!(e, "1:13: expected a key");
}
#[test]
fn writes_back_what_it_read() {
let entry = from_text::<Entry>(BOOK).unwrap().value;
let written = entry.write();
let again = from_value::<Entry>(&written)
.unwrap_or_else(|p| panic!("{p:?}"))
.value;
assert_eq!(again, entry);
let txt = text::to_string_pretty(&written);
assert!(
txt.contains("\"type\": \"spotlight\",\n \"item\": \"@item:minecraft:stick\""),
"{txt}"
);
assert!(txt.contains("\"@@everyone {b|Hello}\""), "{txt}");
assert!(!txt.contains("read_count"));
assert_eq!(from_text::<Entry>(&txt).unwrap().value, entry);
let bytes = binary::encode(&written);
let decoded = binary::decode(&bytes).unwrap();
assert!(matches!(decoded.get("name"), Some(Value::Res(_))));
assert_eq!(from_value::<Entry>(&decoded).unwrap().value, entry);
}
#[test]
fn shapes_describe_the_format() {
let d = Entry::shape().describe();
let defs = d.get("defs").unwrap();
let page = defs.get("Page").unwrap();
assert_eq!(page.get("tag"), Some(&Value::from("type")));
let variants = page.get("variants").unwrap().as_list().unwrap();
assert_eq!(
variants
.iter()
.map(|v| v.get("name").unwrap().as_str().unwrap())
.collect::<Vec<_>>(),
["text", "spotlight", "recipe", "empty"]
);
assert_eq!(variants[0].get("doc"), Some(&Value::from("Text, wrapped.")));
let spot_fields = variants[1].get("fields").unwrap().as_list().unwrap();
assert_eq!(
spot_fields[0].get("shape").unwrap().get("of"),
Some(&Value::from("item"))
);
assert_eq!(spot_fields[1].get("required"), Some(&Value::Bool(false)));
let fields = variants[2].get("fields").unwrap().as_list().unwrap();
assert_eq!(fields[0].get("doc"), Some(&Value::from("What it makes.")));
assert_eq!(fields[1].get("required"), Some(&Value::Bool(false)));
assert_eq!(
fields[3]
.get("shape")
.unwrap()
.get("of")
.unwrap()
.get("kind"),
Some(&Value::from("ref_or_tag"))
);
}