Schema model: catalog, field types, references, checker and line writer shared by tools and validator
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
ce15eccb0f
27 files changed
+1038
No files matched your search
@@ -0,0 +1,65 @@
|
||||
namespace Cthangover.Mods.Schemas.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Merging schema files of several mods: the later mod replaces an action or command
|
||||
/// whole and a kind's field by key, and adds fields of its own; broken files are reported.
|
||||
/// </summary>
|
||||
public class SchemaCatalogTests
|
||||
{
|
||||
private const string First = "{\"Actions\":[{\"Id\":\"x.do\",\"Params\":[{\"Key\":\"a\"}]}],\"Kinds\":[{\"Kind\":\"item\",\"Name\":\"k/item\",\"Folder\":\"items\",\"Fields\":[{\"Key\":\"Name\",\"Type\":\"locale\"},{\"Key\":\"Cost\",\"Type\":\"text\"}]}]}";
|
||||
private const string Second = "{\"actions\":[{\"id\":\"x.do\",\"params\":[{\"key\":\"b\",\"type\":\"int\"}]}],\"kinds\":[{\"kind\":\"ITEM\",\"fields\":[{\"key\":\"cost\",\"type\":\"int\"},{\"key\":\"Equip\"}]}],\"commands\":[{\"id\":\"check\",\"arrow\":\"k/arrow\"}]}";
|
||||
|
||||
[ModTest("catalog/later_mod_wins_and_fields_merge")]
|
||||
static void LaterModWinsAndFieldsMerge(ModTestContext ctx)
|
||||
{
|
||||
var catalog = SchemaCatalog.Build(new List<(string, string, string)> { ("mod_a", "schemas/a.json", First), ("mod_b", "schemas/b.json", Second) });
|
||||
|
||||
ModAssert.Equal(0, catalog.Errors.Count, "both files parse (keys in any case)");
|
||||
var action = catalog.Actions["X.DO"];
|
||||
ModAssert.Equal("mod_b", action.SourceMod, "the later action replaced the earlier");
|
||||
ModAssert.Equal(1, action.Params.Count, "replaced whole, not merged");
|
||||
ModAssert.Equal("b", action.Params[0].Key, "the later parameter");
|
||||
|
||||
var item = catalog.Kinds["item"];
|
||||
ModAssert.Equal("k/item", item.Name, "a kind keeps the name nobody replaced");
|
||||
ModAssert.Equal("items", item.Folder, "and the folder");
|
||||
ModAssert.Equal(3, item.Fields.Count, "Name, Cost, Equip");
|
||||
ModAssert.Equal("Name", item.Fields[0].Key, "order kept");
|
||||
ModAssert.Equal("int", item.Fields[1].Type, "a field with the same key is replaced in place");
|
||||
ModAssert.Equal("mod_b", item.Fields[1].SourceMod, "and remembers who described it");
|
||||
ModAssert.Equal("Equip", item.Fields[2].Key, "a new field is appended");
|
||||
ModAssert.Equal("mod_a", item.Fields[0].SourceMod, "the untouched field keeps its source");
|
||||
ModAssert.NotNull(catalog.Commands["check"], "commands are read too");
|
||||
}
|
||||
|
||||
[ModTest("catalog/broken_files_are_reported")]
|
||||
static void BrokenFilesAreReported(ModTestContext ctx)
|
||||
{
|
||||
var catalog = SchemaCatalog.Build(new List<(string, string, string)> { ("mod_a", "schemas/a.json", "{ not json"), ("mod_b", "schemas/b.json", First) });
|
||||
|
||||
ModAssert.Equal(1, catalog.Errors.Count, "one broken file");
|
||||
ModAssert.True(catalog.Errors[0].StartsWith("mod_a:schemas/a.json"), "named by mod and path");
|
||||
ModAssert.NotNull(catalog.Actions["x.do"], "the good file still counts");
|
||||
}
|
||||
|
||||
[ModTest("catalog/loaded_mods_describe_their_actions_and_kinds")]
|
||||
static void LoadedModsDescribe(ModTestContext ctx)
|
||||
{
|
||||
var catalog = SchemaCatalog.Current;
|
||||
ModAssert.Equal(0, catalog.Errors.Count, "every shipped schema parses: " + string.Join("; ", catalog.Errors));
|
||||
foreach (var id in new[] { "stat.add", "stat.exp", "equip.put", "item.add", "rpg.battle", "trade.open" })
|
||||
ModAssert.True(catalog.Actions.ContainsKey(id), $"action {id} is described");
|
||||
foreach (var id in new[] { "check", "when", "text", "switch_scene" })
|
||||
ModAssert.True(catalog.Commands.ContainsKey(id), $"command {id} is described");
|
||||
var item = catalog.FieldsOf("item").Select(f => f.Key).ToList();
|
||||
ModAssert.True(item.Contains("Cost") && item.Contains("Equip") && item.Contains("Traits"), "items: the tools' own fields and the equipment mod's merged");
|
||||
var character = catalog.FieldsOf("character").Select(f => f.Key).ToList();
|
||||
ModAssert.True(character.Contains("Health") && character.Contains("Archetype") && character.Contains("Stats"), "characters: builtin and stats fields merged");
|
||||
ModAssert.True(catalog.FieldsOf("action").Any(f => f.Key == "Values" && f.Of.Any(o => o.Key == "Formula")), "battle formulas describe action values");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Cthangover.Mods.Schemas.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
|
||||
/// <summary>What the validator and the palette report about values.</summary>
|
||||
public class SchemaCheckerTests
|
||||
{
|
||||
private static readonly List<FieldSchema> Fields = new()
|
||||
{
|
||||
new() { Key = "id", Type = "ref", Kind = "item", Required = true },
|
||||
new() { Key = "count", Type = "int" },
|
||||
new() { Key = "rate", Type = "number" },
|
||||
new() { Key = "mode", Type = "choice", Options = new List<string> { "fast", "slow" } },
|
||||
new() { Key = "on", Type = "bool" },
|
||||
new() { Key = "who", Type = "who" },
|
||||
new() { Key = "enemies", Type = "ref_list", Kind = "character" },
|
||||
};
|
||||
|
||||
private static bool Exists(string kind, string id) => kind == "item" && id == "sword" || kind == "character" && id is "hero" or "wolf";
|
||||
|
||||
private static List<string> Codes(Dictionary<string, string> named) => SchemaChecker.CheckNamed("t", Fields, named, Exists).Select(i => (i.IsError ? "E:" : "W:") + i.Code).ToList();
|
||||
|
||||
[ModTest("checker/clean_values_pass")]
|
||||
static void CleanValuesPass(ModTestContext ctx)
|
||||
{
|
||||
var codes = Codes(new Dictionary<string, string> { ["id"] = "sword", ["count"] = "-2", ["rate"] = "0.5", ["mode"] = "FAST", ["on"] = "1", ["who"] = "party", ["enemies"] = "hero, wolf" });
|
||||
ModAssert.Equal(0, codes.Count, "no issue: " + string.Join(",", codes));
|
||||
}
|
||||
|
||||
[ModTest("checker/each_problem_is_named")]
|
||||
static void EachProblemIsNamed(ModTestContext ctx)
|
||||
{
|
||||
ModAssert.True(Codes(new Dictionary<string, string>()).SequenceEqual(new[] { "E:SCHEMA_MISSING_PARAM" }), "missing required");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "sword", ["nope"] = "1" }).SequenceEqual(new[] { "W:SCHEMA_UNKNOWN_PARAM" }), "unknown parameter");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "axe" }).SequenceEqual(new[] { "E:SCHEMA_UNKNOWN_REF" }), "unknown entity");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "sword", ["count"] = "1.5" }).SequenceEqual(new[] { "E:SCHEMA_BAD_VALUE" }), "not a whole number");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "sword", ["mode"] = "medium" }).SequenceEqual(new[] { "W:SCHEMA_BAD_VALUE" }), "outside the options");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "sword", ["who"] = "ghost" }).SequenceEqual(new[] { "E:SCHEMA_UNKNOWN_REF" }), "an unknown character as who");
|
||||
ModAssert.True(Codes(new Dictionary<string, string> { ["id"] = "sword", ["enemies"] = "wolf,bear" }).SequenceEqual(new[] { "E:SCHEMA_UNKNOWN_REF" }), "one unknown in a list");
|
||||
}
|
||||
|
||||
[ModTest("checker/no_callback_skips_references")]
|
||||
static void NoCallbackSkipsReferences(ModTestContext ctx)
|
||||
{
|
||||
var issues = SchemaChecker.CheckNamed("t", Fields, new Dictionary<string, string> { ["id"] = "anything" }, null);
|
||||
ModAssert.Equal(0, issues.Count, "references are not checked without a callback");
|
||||
var positional = SchemaChecker.CheckPositional("t", Fields.Take(2).ToList(), new List<string> { "sword", "x" }, Exists);
|
||||
ModAssert.Equal(1, positional.Count, "positional values are checked by index");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Cthangover.Mods.Schemas.Tests
|
||||
{
|
||||
using System.Text.Json.Nodes;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
|
||||
/// <summary>Form text and JSON nodes convert into the shapes the engine's readers expect.</summary>
|
||||
public class SchemaJsonTests
|
||||
{
|
||||
[ModTest("json/typed_nodes")]
|
||||
static void TypedNodes(ModTestContext ctx)
|
||||
{
|
||||
ModAssert.Equal("5", SchemaJson.ToNode(new FieldSchema { Type = "int" }, "5").ToJsonString(), "int");
|
||||
ModAssert.Equal("2.5", SchemaJson.ToNode(new FieldSchema { Type = "number" }, "2.5").ToJsonString(), "number");
|
||||
ModAssert.Equal("true", SchemaJson.ToNode(new FieldSchema { Type = "bool" }, "1").ToJsonString(), "bool");
|
||||
ModAssert.Equal("[\"a\",\"b\"]", SchemaJson.ToNode(new FieldSchema { Type = "ref_list" }, "a, b").ToJsonString(), "list of ids");
|
||||
ModAssert.Equal("str + 1", SchemaJson.ToNode(new FieldSchema { Type = "formula" }, " str + 1 ").GetValue<string>(), "formula text, trimmed");
|
||||
ModAssert.Null(SchemaJson.ToNode(new FieldSchema { Type = "int" }, " "), "empty removes the key");
|
||||
ModAssert.Equal("7", SchemaJson.NumberOrText("7").ToJsonString(), "a map value that is a number");
|
||||
ModAssert.Equal("6 + level", SchemaJson.NumberOrText("6 + level").GetValue<string>(), "a map value that is a formula");
|
||||
}
|
||||
|
||||
[ModTest("json/texts_for_the_form")]
|
||||
static void TextsForTheForm(ModTestContext ctx)
|
||||
{
|
||||
var node = JsonNode.Parse("{\"a\": 3, \"b\": true, \"c\": [\"x\", \"y\"], \"d\": \"t\", \"e\": {\"k\": 1}}");
|
||||
ModAssert.Equal("3", SchemaJson.ToText(node["a"]), "number");
|
||||
ModAssert.Equal("1", SchemaJson.ToText(node["b"]), "true");
|
||||
ModAssert.Equal("x,y", SchemaJson.ToText(node["c"]), "array of strings");
|
||||
ModAssert.Equal("t", SchemaJson.ToText(node["d"]), "string");
|
||||
ModAssert.Equal("{\"k\":1}", SchemaJson.ToText(node["e"]), "object as JSON");
|
||||
ModAssert.Equal("", SchemaJson.ToText(null), "absent");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Cthangover.Mods.Schemas.Tests
|
||||
{
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
|
||||
/// <summary>Which entity references a value makes, by its field type.</summary>
|
||||
public class SchemaReferencesTests
|
||||
{
|
||||
private static string Refs(FieldSchema field, string value)
|
||||
=> string.Join(";", SchemaReferences.Of(field, value).Select(r => r.Kind + ":" + r.Id));
|
||||
|
||||
[ModTest("references/by_type")]
|
||||
static void ByType(ModTestContext ctx)
|
||||
{
|
||||
ModAssert.Equal("item:sword", Refs(new FieldSchema { Type = "ref", Kind = "item" }, " sword "), "ref");
|
||||
ModAssert.Equal("character:a;character:b", Refs(new FieldSchema { Type = "ref_list", Kind = "character" }, "a, b,"), "ref_list");
|
||||
ModAssert.Equal("character:hero", Refs(new FieldSchema { Type = "who" }, "hero"), "who names a character");
|
||||
ModAssert.Equal("", Refs(new FieldSchema { Type = "who" }, "party"), "party is no reference");
|
||||
ModAssert.Equal("", Refs(new FieldSchema { Type = "who" }, "Player"), "player is no reference");
|
||||
ModAssert.Equal("scene:town", Refs(new FieldSchema { Type = "scene" }, "town"), "scene");
|
||||
ModAssert.Equal("", Refs(new FieldSchema { Type = "text" }, "anything"), "text makes none");
|
||||
ModAssert.Equal("", Refs(new FieldSchema { Type = "ref" }, "x"), "a ref without a kind makes none");
|
||||
ModAssert.Equal("", Refs(new FieldSchema { Type = "ref", Kind = "item" }, "${picked}"), "a run-time value makes none");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user