78 lines
3.2 KiB
C++
78 lines
3.2 KiB
C++
|
|
// 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<std::uint8_t> slot;
|
||
|
|
float yaw = 0;
|
||
|
|
float pitch = 0;
|
||
|
|
std::vector<Vec3> 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;
|
||
|
|
}
|