// 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 requires (!std::same_as && !std::same_as) Value(T value) { if constexpr (std::is_unsigned_v && sizeof(T) >= sizeof(std::int64_t)) { if (value > static_cast(std::numeric_limits::max())) { kind = ValueKind::Number; number = static_cast(value); return; } } kind = ValueKind::Integer; integer = static_cast(value); } Value(double value) : kind(ValueKind::Number), number(value) {} Value(float value) : Value(static_cast(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 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 Items() const; std::span 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 items; // Object members as two parallel lists: a member type holding a Value // could not be complete inside Value's own definition. std::vector keys; std::vector values; void DumpTo(std::string& out, std::uint32_t indent, std::uint32_t depth) const; }; }