feat(core): Value, reflection, codecs and results, shared by app and client
Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb2c06c32e
commit
89a12bcd61
13 changed files with 1951 additions and 11 deletions
67
tests/ShouldDescribeDefaultsAndRequired/main.cpp
Normal file
67
tests/ShouldDescribeDefaultsAndRequired/main.cpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Describe gives `crafter-debug help` what an agent needs to call a command
|
||||
// right the first time: each field's name and type, its default taken from
|
||||
// the member initializer, Required fields marked with no default, a narrow
|
||||
// integer's range, an enum's names, a nested aggregate's own fields. A type
|
||||
// that contains itself is described a few levels deep instead of forever.
|
||||
import std;
|
||||
import Crafter.Debug.Core;
|
||||
using namespace Crafter::Debug;
|
||||
|
||||
namespace Game {
|
||||
enum class Mode : std::uint8_t { Idle, Build };
|
||||
|
||||
struct Vec3 {
|
||||
float x = 0;
|
||||
float y = 1;
|
||||
float z = 0;
|
||||
};
|
||||
|
||||
struct Aim {
|
||||
Required<std::uint8_t> slot;
|
||||
float yaw = 45;
|
||||
Vec3 at;
|
||||
std::optional<std::string> label;
|
||||
Mode mode = Mode::Build;
|
||||
std::vector<std::int32_t> ids;
|
||||
};
|
||||
|
||||
struct Node {
|
||||
std::vector<Node> children;
|
||||
};
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::int32_t failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const Value description = Describe<Game::Aim>();
|
||||
std::println("{}", description.DumpPretty());
|
||||
Check(description.Find("type")->AsString() == "object" && description.Find("name")->AsString().ends_with("Aim"), "an aggregate describes as a named object");
|
||||
const std::span<const Value> fields = description.Find("fields")->Items();
|
||||
Check(fields.size() == 6, "one entry per field");
|
||||
const auto field = [&](std::string_view name) -> const Value& {
|
||||
for (const Value& f : fields) {
|
||||
if (f.Find("name")->AsString() == name) return f;
|
||||
}
|
||||
throw std::runtime_error(std::format("no field {}", name));
|
||||
};
|
||||
Check(field("slot").Find("required") != nullptr && field("slot").Find("default") == nullptr, "a Required field is marked required, with no default");
|
||||
Check(field("slot").Find("type")->Find("maximum")->AsInteger() == 255, "a narrow integer carries its range");
|
||||
Check(field("yaw").Find("default")->AsNumber() == 45, "a default comes from the member initializer");
|
||||
Check(field("at").Find("type")->Find("fields")->Size() == 3 && field("at").Find("default")->Find("y")->AsNumber() == 1, "a nested aggregate describes its fields and default");
|
||||
Check(field("label").Find("type")->Find("nullable") != nullptr && field("label").Find("default")->IsNull(), "an optional is nullable and defaults to null");
|
||||
Check(field("mode").Find("type")->Find("values")->Size() == 2 && field("mode").Find("default")->AsString() == "Build", "an enum lists its names");
|
||||
Check(field("ids").Find("type")->Find("items")->Find("type")->AsString() == "integer", "a vector describes its items");
|
||||
Check(!Describe<Game::Node>().Dump().empty(), "a type that contains itself is described without recursing forever");
|
||||
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue