130 lines
5.5 KiB
Text
130 lines
5.5 KiB
Text
|
|
// 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;
|
||
|
|
};
|
||
|
|
}
|