Crafter.Debug/interfaces/Crafter.Debug.Core-Value.cppm
Jorijn van der Graaf 89a12bcd61
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>
2026-11-01 01:31:58 +01:00

130 lines
5.5 KiB
C++

// 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;
};
}