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
130
interfaces/Crafter.Debug.Core-Value.cppm
Normal file
130
interfaces/Crafter.Debug.Core-Value.cppm
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// A JSON value: what travels between an app and crafter-debug, and what
|
||||
// command arguments, results, watches and events are encoded to. Its own type
|
||||
// rather than a JSON library's, so no consumer interface ever sees one, and
|
||||
// small enough to read in one sitting. Objects keep their members in the
|
||||
// order they were set, which is the order an agent reads them in.
|
||||
//
|
||||
// Numbers keep the distinction JSON text does not: an Integer is a whole
|
||||
// number that fits an int64, anything else is a Number (a double). Integer 1
|
||||
// and Number 1.0 still compare equal. Non-finite doubles have no JSON
|
||||
// spelling, and a debugger must not hide a NaN, so Dump writes them as the
|
||||
// strings "NaN", "Infinity" and "-Infinity", which Codec reads back.
|
||||
export module Crafter.Debug.Core:Value;
|
||||
import std;
|
||||
|
||||
export namespace Crafter::Debug {
|
||||
enum class ValueKind : std::uint8_t {
|
||||
Null,
|
||||
Bool,
|
||||
Integer,
|
||||
Number,
|
||||
String,
|
||||
Array,
|
||||
Object,
|
||||
};
|
||||
|
||||
// "null", "boolean", "integer", "number", "string", "array" or "object":
|
||||
// how errors name a kind.
|
||||
std::string_view KindName(ValueKind kind);
|
||||
|
||||
// Thrown by Value::Parse: what is wrong, and the byte offset in the text.
|
||||
class ParseError : public std::runtime_error {
|
||||
public:
|
||||
std::size_t offset;
|
||||
ParseError(std::size_t offset, std::string_view what);
|
||||
};
|
||||
|
||||
class Value {
|
||||
public:
|
||||
Value() = default;
|
||||
Value(std::nullptr_t) {}
|
||||
Value(bool value) : kind(ValueKind::Bool), boolean(value) {}
|
||||
// Any integer type but bool and char. An unsigned value above the
|
||||
// int64 range becomes a Number, which is exact up to 2^53.
|
||||
template<std::integral T>
|
||||
requires (!std::same_as<T, bool> && !std::same_as<T, char>)
|
||||
Value(T value) {
|
||||
if constexpr (std::is_unsigned_v<T> && sizeof(T) >= sizeof(std::int64_t)) {
|
||||
if (value > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
|
||||
kind = ValueKind::Number;
|
||||
number = static_cast<double>(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
kind = ValueKind::Integer;
|
||||
integer = static_cast<std::int64_t>(value);
|
||||
}
|
||||
Value(double value) : kind(ValueKind::Number), number(value) {}
|
||||
Value(float value) : Value(static_cast<double>(value)) {}
|
||||
Value(std::string value) : kind(ValueKind::String), text(std::move(value)) {}
|
||||
Value(std::string_view value) : Value(std::string(value)) {}
|
||||
// String literals; without this, `Value("x")` would pick the bool
|
||||
// constructor, a standard conversion beating a user-defined one.
|
||||
Value(const char* value) : Value(std::string(value)) {}
|
||||
|
||||
static Value MakeArray(std::vector<Value> items = {});
|
||||
static Value MakeObject();
|
||||
|
||||
ValueKind Kind() const { return kind; }
|
||||
bool IsNull() const { return kind == ValueKind::Null; }
|
||||
bool IsBool() const { return kind == ValueKind::Bool; }
|
||||
bool IsInteger() const { return kind == ValueKind::Integer; }
|
||||
// Integer or Number.
|
||||
bool IsNumber() const { return kind == ValueKind::Integer || kind == ValueKind::Number; }
|
||||
bool IsString() const { return kind == ValueKind::String; }
|
||||
bool IsArray() const { return kind == ValueKind::Array; }
|
||||
bool IsObject() const { return kind == ValueKind::Object; }
|
||||
|
||||
// Each throws std::logic_error for a value of another kind; check the
|
||||
// kind first. AsNumber accepts an Integer too.
|
||||
bool AsBool() const;
|
||||
std::int64_t AsInteger() const;
|
||||
double AsNumber() const;
|
||||
const std::string& AsString() const;
|
||||
|
||||
// Array items. Push turns a null value into an array first.
|
||||
std::span<const Value> Items() const;
|
||||
std::span<Value> Items();
|
||||
void Push(Value item);
|
||||
|
||||
// Object members, in the order they were first set. Set replaces the
|
||||
// value of an existing key in place, and turns a null value into an
|
||||
// object first, as operator[] does when the key is missing.
|
||||
const Value* Find(std::string_view key) const;
|
||||
Value* Find(std::string_view key);
|
||||
Value& Set(std::string_view key, Value value);
|
||||
Value& operator[](std::string_view key);
|
||||
bool Erase(std::string_view key);
|
||||
auto Members() const { return std::views::zip(keys, values); }
|
||||
|
||||
// Array items or object members; 0 for anything else.
|
||||
std::size_t Size() const;
|
||||
|
||||
// JSON text. Parse throws ParseError; a duplicate key keeps the last
|
||||
// value. Dump writes compact text, DumpPretty indents.
|
||||
static Value Parse(std::string_view json);
|
||||
std::string Dump() const;
|
||||
std::string DumpPretty(std::uint32_t indent = 2) const;
|
||||
|
||||
// Numbers compare by value across Integer and Number; objects compare
|
||||
// their members whatever their order.
|
||||
friend bool operator==(const Value& a, const Value& b);
|
||||
|
||||
private:
|
||||
ValueKind kind = ValueKind::Null;
|
||||
bool boolean = false;
|
||||
std::int64_t integer = 0;
|
||||
double number = 0;
|
||||
std::string text;
|
||||
std::vector<Value> items;
|
||||
// Object members as two parallel lists: a member type holding a Value
|
||||
// could not be complete inside Value's own definition.
|
||||
std::vector<std::string> keys;
|
||||
std::vector<Value> values;
|
||||
|
||||
void DumpTo(std::string& out, std::uint32_t indent, std::uint32_t depth) const;
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue