299 lines
6.9 KiB
C++
299 lines
6.9 KiB
C++
#pragma once
|
|
|
|
#include <variant>
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <charconv>
|
|
#include <stdexcept>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
class JsonParser {
|
|
public:
|
|
struct JsonValue;
|
|
using JsonNull = std::monostate;
|
|
using JsonBool = bool;
|
|
using JsonNumber = double;
|
|
using JsonString = std::string;
|
|
using JsonArray = std::vector<JsonValue>;
|
|
using JsonObject = std::unordered_map<std::string, JsonValue>;
|
|
|
|
struct JsonValue {
|
|
std::variant<JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject> value;
|
|
|
|
template<typename T>
|
|
bool is() const { return std::holds_alternative<T>(value); }
|
|
|
|
template<typename T>
|
|
const T& as() const { return std::get<T>(value); }
|
|
|
|
template<typename T>
|
|
T& as() { return std::get<T>(value); }
|
|
};
|
|
|
|
private:
|
|
std::string_view input;
|
|
size_t pos = 0;
|
|
|
|
void skip_whitespace() {
|
|
while (pos < input.size() && (input[pos] == ' ' || input[pos] == '\t' ||
|
|
input[pos] == '\n' || input[pos] == '\r')) {
|
|
++pos;
|
|
}
|
|
}
|
|
|
|
char peek() const {
|
|
return pos < input.size() ? input[pos] : '\0';
|
|
}
|
|
|
|
char consume() {
|
|
return pos < input.size() ? input[pos++] : '\0';
|
|
}
|
|
|
|
void expect(char expected) {
|
|
if (consume() != expected) {
|
|
throw std::runtime_error("Expected '" + std::string(1, expected) + "'");
|
|
}
|
|
}
|
|
|
|
JsonValue parse_null() {
|
|
if (input.substr(pos, 4) == "null") {
|
|
pos += 4;
|
|
return JsonValue{JsonNull{}};
|
|
}
|
|
throw std::runtime_error("Invalid null");
|
|
}
|
|
|
|
JsonValue parse_bool() {
|
|
if (input.substr(pos, 4) == "true") {
|
|
pos += 4;
|
|
return JsonValue{true};
|
|
}
|
|
if (input.substr(pos, 5) == "false") {
|
|
pos += 5;
|
|
return JsonValue{false};
|
|
}
|
|
throw std::runtime_error("Invalid boolean");
|
|
}
|
|
|
|
JsonValue parse_number() {
|
|
size_t start = pos;
|
|
|
|
if (peek() == '-') consume();
|
|
|
|
if (peek() == '0') {
|
|
consume();
|
|
} else if (peek() >= '1' && peek() <= '9') {
|
|
consume();
|
|
while (peek() >= '0' && peek() <= '9') consume();
|
|
} else {
|
|
throw std::runtime_error("Invalid number");
|
|
}
|
|
|
|
if (peek() == '.') {
|
|
consume();
|
|
if (!(peek() >= '0' && peek() <= '9')) {
|
|
throw std::runtime_error("Invalid decimal");
|
|
}
|
|
while (peek() >= '0' && peek() <= '9') consume();
|
|
}
|
|
|
|
if (peek() == 'e' || peek() == 'E') {
|
|
consume();
|
|
if (peek() == '+' || peek() == '-') consume();
|
|
if (!(peek() >= '0' && peek() <= '9')) {
|
|
throw std::runtime_error("Invalid exponent");
|
|
}
|
|
while (peek() >= '0' && peek() <= '9') consume();
|
|
}
|
|
|
|
double result;
|
|
auto [ptr, ec] = std::from_chars(input.data() + start, input.data() + pos, result);
|
|
if (ec != std::errc{}) {
|
|
throw std::runtime_error("Number parsing failed");
|
|
}
|
|
|
|
return JsonValue{result};
|
|
}
|
|
|
|
std::string parse_string_content() {
|
|
std::string result;
|
|
result.reserve(32); // optimization for typical strings
|
|
|
|
while (pos < input.size() && peek() != '"') {
|
|
char c = consume();
|
|
if (c == '\\') {
|
|
if (pos >= input.size()) throw std::runtime_error("Unterminated escape");
|
|
char escaped = consume();
|
|
switch (escaped) {
|
|
case '"': result += '"'; break;
|
|
case '\\': result += '\\'; break;
|
|
case '/': result += '/'; break;
|
|
case 'b': result += '\b'; break;
|
|
case 'f': result += '\f'; break;
|
|
case 'n': result += '\n'; break;
|
|
case 'r': result += '\r'; break;
|
|
case 't': result += '\t'; break;
|
|
case 'u': {
|
|
if (pos + 4 > input.size()) throw std::runtime_error("Invalid unicode escape");
|
|
auto hex = input.substr(pos, 4);
|
|
pos += 4;
|
|
int codepoint = 0;
|
|
auto [ptr, ec] = std::from_chars(hex.data(), hex.data() + 4, codepoint, 16);
|
|
if (ec != std::errc{}) throw std::runtime_error("Invalid unicode hex");
|
|
|
|
// Simple UTF-8 encoding for BMP
|
|
if (codepoint < 0x80) {
|
|
result += static_cast<char>(codepoint);
|
|
} else if (codepoint < 0x800) {
|
|
result += static_cast<char>(0xC0 | (codepoint >> 6));
|
|
result += static_cast<char>(0x80 | (codepoint & 0x3F));
|
|
} else {
|
|
result += static_cast<char>(0xE0 | (codepoint >> 12));
|
|
result += static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
|
|
result += static_cast<char>(0x80 | (codepoint & 0x3F));
|
|
}
|
|
break;
|
|
}
|
|
default: throw std::runtime_error("Invalid escape sequence");
|
|
}
|
|
} else if (static_cast<unsigned char>(c) < 0x20) {
|
|
throw std::runtime_error("Unescaped control character");
|
|
} else {
|
|
result += c;
|
|
}
|
|
}
|
|
|
|
if (peek() != '"') throw std::runtime_error("Unterminated string");
|
|
return result;
|
|
}
|
|
|
|
JsonValue parse_string() {
|
|
expect('"');
|
|
auto content = parse_string_content();
|
|
expect('"');
|
|
return JsonValue{std::move(content)};
|
|
}
|
|
|
|
JsonValue parse_array() {
|
|
expect('[');
|
|
skip_whitespace();
|
|
|
|
JsonArray array;
|
|
|
|
if (peek() == ']') {
|
|
consume();
|
|
return JsonValue{std::move(array)};
|
|
}
|
|
|
|
while (true) {
|
|
array.push_back(parse_value());
|
|
skip_whitespace();
|
|
|
|
char next = peek();
|
|
if (next == ']') {
|
|
consume();
|
|
break;
|
|
} else if (next == ',') {
|
|
consume();
|
|
skip_whitespace();
|
|
} else {
|
|
throw std::runtime_error("Expected ',' or ']' in array");
|
|
}
|
|
}
|
|
|
|
return JsonValue{std::move(array)};
|
|
}
|
|
|
|
JsonValue parse_object() {
|
|
expect('{');
|
|
skip_whitespace();
|
|
|
|
JsonObject object;
|
|
|
|
if (peek() == '}') {
|
|
consume();
|
|
return JsonValue{std::move(object)};
|
|
}
|
|
|
|
while (true) {
|
|
if (peek() != '"') throw std::runtime_error("Expected string key");
|
|
|
|
expect('"');
|
|
auto key = parse_string_content();
|
|
expect('"');
|
|
|
|
skip_whitespace();
|
|
expect(':');
|
|
skip_whitespace();
|
|
|
|
auto value = parse_value();
|
|
object.emplace(std::move(key), std::move(value));
|
|
|
|
skip_whitespace();
|
|
|
|
char next = peek();
|
|
if (next == '}') {
|
|
consume();
|
|
break;
|
|
} else if (next == ',') {
|
|
consume();
|
|
skip_whitespace();
|
|
} else {
|
|
throw std::runtime_error("Expected ',' or '}' in object");
|
|
}
|
|
}
|
|
|
|
return JsonValue{std::move(object)};
|
|
}
|
|
|
|
JsonValue parse_value() {
|
|
skip_whitespace();
|
|
|
|
char c = peek();
|
|
switch (c) {
|
|
case 'n': return parse_null();
|
|
case 't':
|
|
case 'f': return parse_bool();
|
|
case '"': return parse_string();
|
|
case '[': return parse_array();
|
|
case '{': return parse_object();
|
|
case '-':
|
|
case '0': case '1': case '2': case '3': case '4':
|
|
case '5': case '6': case '7': case '8': case '9':
|
|
return parse_number();
|
|
default:
|
|
throw std::runtime_error("Unexpected character");
|
|
}
|
|
}
|
|
|
|
public:
|
|
JsonValue parse(std::string_view json) {
|
|
input = json;
|
|
pos = 0;
|
|
auto result = parse_value();
|
|
skip_whitespace();
|
|
if (pos < input.size()) {
|
|
throw std::runtime_error("Extra characters after JSON");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
JsonValue parse_file(const std::string& filepath) {
|
|
std::ifstream file(filepath);
|
|
if (!file.is_open()) {
|
|
throw std::runtime_error("Cannot open file: " + filepath);
|
|
}
|
|
|
|
std::ostringstream buffer;
|
|
buffer << file.rdbuf();
|
|
|
|
if (file.bad()) {
|
|
throw std::runtime_error("Error reading file: " + filepath);
|
|
}
|
|
|
|
return parse(buffer.str());
|
|
}
|
|
}; |