Files

165 lines
6.0 KiB
C++

#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
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<std::string, JsonValue>;
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<JsonValue>& elements() const { return arrayPayload; }
std::vector<JsonValue>& elements() { return arrayPayload; }
const std::vector<Member>& members() const { return objectPayload; }
std::vector<Member>& 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<JsonValue> arrayPayload;
std::vector<Member> 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<std::string>& 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<std::string> parts;
bool wellFormed = true;
};
} // namespace vf