diff --git a/core/include/vf/Json.h b/core/include/vf/Json.h new file mode 100644 index 0000000..c9d0bf8 --- /dev/null +++ b/core/include/vf/Json.h @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace vf { + +enum class JsonType : uint8_t { Null, Bool, Number, String, Array, Object }; + +// Deep enough for any authored document, shallow enough that the recursive +// descent parser cannot run the stack out before it reports the error. +constexpr int jsonDefaultMaxDepth = 64; + +// One JSON value. Arrays and objects hold JsonValue by value, so a parsed +// document is one self-contained object with no shared ownership. +// +// Object members keep insertion order: a document written back out lists its +// keys exactly as they were read, which is what makes the round trip byte +// stable. Equality is order sensitive for the same reason. +class JsonValue { +public: + using Member = std::pair; + + JsonValue() = default; + + static JsonValue makeNull(); + static JsonValue makeBool(bool value); + static JsonValue makeNumber(double value); + static JsonValue makeInt(long long value); + static JsonValue makeString(std::string value); + static JsonValue makeArray(); + static JsonValue makeObject(); + + JsonType kind() const { return type; } + bool isNull() const { return type == JsonType::Null; } + bool isBool() const { return type == JsonType::Bool; } + bool isNumber() const { return type == JsonType::Number; } + bool isString() const { return type == JsonType::String; } + bool isArray() const { return type == JsonType::Array; } + bool isObject() const { return type == JsonType::Object; } + + // True when the number was written without a fraction or exponent and fits + // in a 64 bit integer, so it can be read back with no precision loss. + bool isInteger() const { return type == JsonType::Number && integral; } + + bool boolValue() const { return boolPayload; } + double numberValue() const; + long long intValue() const; + const std::string& stringValue() const { return stringPayload; } + + const std::vector& elements() const { return arrayPayload; } + std::vector& elements() { return arrayPayload; } + const std::vector& members() const { return objectPayload; } + std::vector& members() { return objectPayload; } + + // Element count for arrays, member count for objects, 0 for scalars. + std::size_t size() const; + bool empty() const { return size() == 0u; } + + void push(JsonValue value); + // Overwrites an existing member in place, keeping its position. + void set(const std::string& key, JsonValue value); + bool erase(const std::string& key); + + const JsonValue* find(const std::string& key) const; + JsonValue* find(const std::string& key); + bool has(const std::string& key) const { return find(key) != nullptr; } + const JsonValue* at(std::size_t index) const; + + // Typed reads that fall back instead of failing, for optional fields. + bool boolOr(bool fallback) const; + double numberOr(double fallback) const; + long long intOr(long long fallback) const; + std::string stringOr(const std::string& fallback) const; + + bool boolOr(const std::string& key, bool fallback) const; + double numberOr(const std::string& key, double fallback) const; + long long intOr(const std::string& key, long long fallback) const; + std::string stringOr(const std::string& key, const std::string& fallback) const; + + bool operator==(const JsonValue& other) const; + bool operator!=(const JsonValue& other) const { return !(*this == other); } + +private: + JsonType type = JsonType::Null; + bool boolPayload = false; + bool integral = false; + double numberPayload = 0.0; + long long integerPayload = 0; + std::string stringPayload; + std::vector arrayPayload; + std::vector objectPayload; +}; + +// Where a document stopped parsing. Line and column are 1 based and count +// bytes, and point at the offending character rather than past it. +struct JsonParseError { + int line = 0; + int column = 0; + std::string message; + + bool present() const { return line > 0; } + // "line:column: message", for logs and for DataPackError. + std::string text() const; +}; + +struct JsonParseResult { + bool ok = false; + JsonValue root; + JsonParseError error; +}; + +// Strict RFC 8259: no comments, no trailing commas, no unquoted keys, one root +// value plus trailing whitespace, control characters and malformed UTF-8 +// rejected inside strings. Nesting deeper than maxDepth is an error, not a +// crash. Never throws and never aborts on malformed input. +JsonParseResult parseJson(const std::string& text, int maxDepth = jsonDefaultMaxDepth); + +// Serializer. indentWidth 0 writes the compact form (no spaces at all), any +// positive width writes one member or element per line. +class JsonWriter { +public: + int indentWidth = 0; + // Emit every code point above ASCII as \uXXXX, surrogate pairs included, so + // the output stays pure ASCII whatever went in. + bool escapeNonAscii = true; + + std::string write(const JsonValue& value) const; + + static std::string compact(const JsonValue& value); + static std::string indented(const JsonValue& value, int indentWidth = 2); + +private: + void writeValue(const JsonValue& value, int depth, std::string& out) const; + void writeString(const std::string& value, std::string& out) const; +}; + +// RFC 6901 pointer: "" is the whole document, "/foo/0/bar" walks into it, "~1" +// stands for "/" and "~0" for "~". An array token must be a plain decimal index +// with no leading zeros. +class JsonPointer { +public: + JsonPointer() = default; + explicit JsonPointer(const std::string& pointer); + + bool valid() const { return wellFormed; } + const std::vector& tokens() const { return parts; } + // The escaped text form, which round trips through the constructor. + std::string text() const; + + const JsonValue* resolve(const JsonValue& root) const; + JsonValue* resolve(JsonValue& root) const; + + static const JsonValue* find(const JsonValue& root, const std::string& pointer); + +private: + std::vector parts; + bool wellFormed = true; +}; + +} // namespace vf