sots-engine/tests/game_design/json_min.h

236 lines
8.2 KiB
C++

// A minimal JSON reader for the test tools (reads the owner's designs JSON).
// Header-only, no dependencies, no exceptions: parse() returns false and an
// error message on malformed input. Numbers are kept as double plus the text.
#pragma once
#include <cstdlib>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace json_min {
struct Value {
enum class Kind { Null, Bool, Number, String, Array, Object };
Kind kind = Kind::Null;
bool b = false;
double num = 0;
std::string text; // string value or number text
std::vector<Value> items; // array
std::vector<std::pair<std::string, Value>> members; // object, file order
bool is_null() const { return kind == Kind::Null; }
bool is_string() const { return kind == Kind::String; }
bool is_number() const { return kind == Kind::Number; }
bool is_array() const { return kind == Kind::Array; }
bool is_object() const { return kind == Kind::Object; }
bool is_bool() const { return kind == Kind::Bool; }
const Value* get(std::string_view key) const {
for (const auto& m : members)
if (m.first == key) return &m.second;
return nullptr;
}
const Value& operator[](std::string_view key) const {
static const Value null;
const Value* v = get(key);
return v ? *v : null;
}
const std::string& str() const { return text; }
int as_int() const { return static_cast<int>(num); }
bool as_bool() const { return kind == Kind::Bool ? b : (kind == Kind::Number ? num != 0 : false); }
};
class Parser {
public:
explicit Parser(std::string_view s) : s_(s) {}
bool parse(Value& out, std::string& err) {
skip();
if (!value(out, err)) return false;
skip();
if (pos_ != s_.size()) return fail(err, "trailing characters");
return true;
}
private:
std::string_view s_;
std::size_t pos_ = 0;
bool fail(std::string& err, const char* what) {
err = std::string(what) + " at offset " + std::to_string(pos_);
return false;
}
void skip() {
while (pos_ < s_.size() && (s_[pos_] == ' ' || s_[pos_] == '\n' || s_[pos_] == '\r' || s_[pos_] == '\t')) ++pos_;
}
bool literal(const char* lit) {
std::string_view l(lit);
if (s_.substr(pos_, l.size()) != l) return false;
pos_ += l.size();
return true;
}
static void put_utf8(std::string& out, unsigned cp) {
if (cp < 0x80) {
out += static_cast<char>(cp);
} else if (cp < 0x800) {
out += static_cast<char>(0xC0 | (cp >> 6));
out += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
out += static_cast<char>(0xE0 | (cp >> 12));
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out += static_cast<char>(0x80 | (cp & 0x3F));
} else {
out += static_cast<char>(0xF0 | (cp >> 18));
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out += static_cast<char>(0x80 | (cp & 0x3F));
}
}
bool hex4(unsigned& cp) {
if (pos_ + 4 > s_.size()) return false;
cp = 0;
for (int i = 0; i < 4; ++i) {
char c = s_[pos_++];
cp <<= 4;
if (c >= '0' && c <= '9') cp |= static_cast<unsigned>(c - '0');
else if (c >= 'a' && c <= 'f') cp |= static_cast<unsigned>(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') cp |= static_cast<unsigned>(c - 'A' + 10);
else return false;
}
return true;
}
bool string(std::string& out, std::string& err) {
if (pos_ >= s_.size() || s_[pos_] != '"') return fail(err, "expected string");
++pos_;
while (pos_ < s_.size()) {
char c = s_[pos_++];
if (c == '"') return true;
if (c != '\\') {
out += c;
continue;
}
if (pos_ >= s_.size()) break;
char e = s_[pos_++];
switch (e) {
case '"': out += '"'; break;
case '\\': out += '\\'; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'n': out += '\n'; break;
case 'r': out += '\r'; break;
case 't': out += '\t'; break;
case 'u': {
unsigned cp;
if (!hex4(cp)) return fail(err, "bad \\u escape");
if (cp >= 0xD800 && cp < 0xDC00 && literal("\\u")) {
unsigned lo;
if (!hex4(lo)) return fail(err, "bad surrogate");
cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
}
put_utf8(out, cp);
break;
}
default: return fail(err, "bad escape");
}
}
return fail(err, "unterminated string");
}
bool value(Value& v, std::string& err) {
if (pos_ >= s_.size()) return fail(err, "unexpected end");
char c = s_[pos_];
if (c == '{') {
++pos_;
v.kind = Value::Kind::Object;
skip();
if (literal("}")) return true;
for (;;) {
skip();
std::string key;
if (!string(key, err)) return false;
skip();
if (!literal(":")) return fail(err, "expected ':'");
skip();
Value m;
if (!value(m, err)) return false;
v.members.emplace_back(std::move(key), std::move(m));
skip();
if (literal(",")) continue;
if (literal("}")) return true;
return fail(err, "expected ',' or '}'");
}
}
if (c == '[') {
++pos_;
v.kind = Value::Kind::Array;
skip();
if (literal("]")) return true;
for (;;) {
skip();
Value item;
if (!value(item, err)) return false;
v.items.push_back(std::move(item));
skip();
if (literal(",")) continue;
if (literal("]")) return true;
return fail(err, "expected ',' or ']'");
}
}
if (c == '"') {
v.kind = Value::Kind::String;
return string(v.text, err);
}
if (literal("null")) return true;
if (literal("true")) {
v.kind = Value::Kind::Bool;
v.b = true;
return true;
}
if (literal("false")) {
v.kind = Value::Kind::Bool;
v.b = false;
return true;
}
std::size_t start = pos_;
while (pos_ < s_.size()) {
char d = s_[pos_];
if ((d >= '0' && d <= '9') || d == '-' || d == '+' || d == '.' || d == 'e' || d == 'E') ++pos_;
else break;
}
if (start == pos_) return fail(err, "unexpected character");
v.kind = Value::Kind::Number;
v.text = std::string(s_.substr(start, pos_ - start));
v.num = std::strtod(v.text.c_str(), nullptr);
return true;
}
};
inline bool parse(std::string_view text, Value& out, std::string& err) { return Parser(text).parse(out, err); }
// Writer helpers for the dump tool.
inline std::string quote(std::string_view s) {
std::string out = "\"";
for (unsigned char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20) {
char buf[8];
std::snprintf(buf, sizeof buf, "\\u%04x", c);
out += buf;
} else {
out += static_cast<char>(c);
}
}
}
return out + "\"";
}
} // namespace json_min