// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A command call with several mistakes gets one reply that names each of // them: a missing Required field, a wrong type naming both kinds, a nested // path ("path[1].x"), an out-of-range integer with the range, and for an // unknown field or enum name the one the caller most likely meant. Decoding // never throws, so a bad call cannot take the app down. Guards the replies an // agent corrects its calls from. import std; import Crafter.Debug.Core; using namespace Crafter::Debug; namespace Game { enum class Mode : std::uint8_t { Idle, Build, Fight }; struct Vec3 { float x = 0; float y = 0; float z = 0; }; struct Aim { Required slot; float yaw = 0; float pitch = 0; std::vector path; Mode mode = Mode::Idle; }; } namespace { std::int32_t failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } bool Has(const DecodeContext& context, std::string_view path, std::string_view text) { return std::ranges::any_of(context.Issues(), [&](const DecodeIssue& issue) { return issue.path == path && issue.message.find(text) != std::string::npos; }); } } int main() { Game::Aim aim; DecodeContext context; Decode(Value::Parse(R"({"yw": 1, "pitch": "up", "path": [{"x": 1}, {"x": "no"}], "mode": "Fihgt"})"), aim, context); std::println("report: {}", context.Report()); Check(context.Issues().size() == 5, std::format("five mistakes, five issues ({})", context.Issues().size())); Check(Has(context, "slot", "required"), "a missing Required field is reported"); Check(Has(context, "pitch", "expected a number, got a string"), "a wrong type names both kinds"); Check(Has(context, "path[1].x", "expected a number"), "a nested path names the item and the member"); Check(Has(context, "mode", "did you mean 'Fight'"), "a mistyped enum name gets a suggestion"); Check(Has(context, "yw", "did you mean 'yaw'"), "an unknown field gets a suggestion"); Check(context.Report().find("slot: missing") != std::string::npos, "the report prefixes each issue with its path"); Game::Aim outOfRange; DecodeContext rangeContext; Decode(Value::Parse(R"({"slot": 300})"), outOfRange, rangeContext); Check(Has(rangeContext, "slot", "300 is out of range (0..255)"), "an out-of-range integer names the range"); Game::Aim notAnObject; DecodeContext objectContext; Decode(Value::Parse("[1]"), notAnObject, objectContext); Check(objectContext.Issues().size() == 1 && objectContext.Issues()[0].path.empty() && objectContext.Issues()[0].message == "expected an object, got an array", "a non-object is one issue at the root"); Game::Aim farOff; DecodeContext farContext; Decode(Value::Parse(R"({"slot": 1, "zzzzzz": 1})"), farOff, farContext); Check(Has(farContext, "zzzzzz", "unknown field (fields: slot, yaw, pitch, path, mode)"), "an unknown field with no close match lists the fields"); return failures == 0 ? 0 : 1; }