eq2go/old/common/json_parser.hpp
2025-08-06 19:00:30 -05:00

308 lines
8.8 KiB
C++

// Copyright (c) 2024 MIT License All rights reserved
#pragma once
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#include <string>
#include <string_view>
#include <cctype>
class JsonParser
{
public:
// Constructor that loads and parses JSON from specified file
explicit JsonParser(const std::string_view filename);
// Retrieves value for given dot-separated path, returns empty string if not found
std::string getValue(const std::string_view path) const
{
std::string lower_path{path};
std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(), ::tolower);
if (auto it = values.find(lower_path); it != values.end()) {
return it->second;
}
return "";
}
// Converts string to unsigned char with range validation
static bool convertStringToUnsignedChar(const std::string_view str, unsigned char& result);
// Converts string to unsigned short with range validation
static bool convertStringToUnsignedShort(const std::string_view str, unsigned short& result);
// Converts string to unsigned int with range validation
static bool convertStringToUnsignedInt(const std::string_view str, unsigned int& result);
// Converts string to unsigned long with range validation
static bool convertStringToUnsignedLong(const std::string_view str, unsigned long& result);
// Returns whether the JSON file was successfully loaded and parsed
constexpr bool IsLoaded() const noexcept { return is_loaded; }
private:
std::map<std::string, std::string> values; // Flat map of dot-separated paths to string values
bool is_loaded{false}; // Success flag for file loading and parsing
// Recursively parses JSON object and builds flat key-value map with dot notation
void parseObject(const std::string& json, size_t& pos, const std::string& path);
// Skips whitespace characters in JSON string starting from given position
size_t skipWhitespace(const std::string& json, size_t pos) const noexcept;
// Parses JSON string literal and returns unescaped content
std::string parseString(const std::string& json, size_t& pos);
// Parses JSON number and returns string representation
std::string parseNumber(const std::string& json, size_t& pos);
// Parses JSON value (string, number, object) and stores in values map
void parseValue(const std::string& json, size_t& pos, const std::string& key);
// Helper template for string to unsigned integer conversion with type safety
template<typename T>
static bool convertStringToUnsigned(const std::string_view str, T& result);
};
// Constructor implementation - loads and parses JSON file
JsonParser::JsonParser(const std::string_view filename)
{
std::ifstream file{std::string{filename}};
if (!file.is_open()) {
std::cerr << "Error: Cannot open JSON file: " << filename << std::endl;
return;
}
std::ostringstream buffer;
buffer << file.rdbuf();
std::string json_content = buffer.str();
try {
size_t pos = 0;
parseObject(json_content, pos, "");
is_loaded = true;
} catch (const std::exception& e) {
std::cerr << "Error parsing JSON: " << e.what() << std::endl;
}
}
// Template implementation for safe string to unsigned conversion
template<typename T>
bool JsonParser::convertStringToUnsigned(const std::string_view str, T& result)
{
try {
unsigned long ul = std::stoul(std::string{str});
if (ul > std::numeric_limits<T>::max()) {
return false;
}
result = static_cast<T>(ul);
return true;
} catch (const std::invalid_argument&) {
return false;
} catch (const std::out_of_range&) {
return false;
}
}
// Converts string to unsigned char with validation
bool JsonParser::convertStringToUnsignedChar(const std::string_view str, unsigned char& result)
{
return convertStringToUnsigned(str, result);
}
// Converts string to unsigned short with validation
bool JsonParser::convertStringToUnsignedShort(const std::string_view str, unsigned short& result)
{
return convertStringToUnsigned(str, result);
}
// Converts string to unsigned int with validation
bool JsonParser::convertStringToUnsignedInt(const std::string_view str, unsigned int& result)
{
return convertStringToUnsigned(str, result);
}
// Converts string to unsigned long with validation
bool JsonParser::convertStringToUnsignedLong(const std::string_view str, unsigned long& result)
{
return convertStringToUnsigned(str, result);
}
// Skips whitespace characters and returns next non-whitespace position
size_t JsonParser::skipWhitespace(const std::string& json, size_t pos) const noexcept
{
while (pos < json.length() && std::isspace(json[pos])) {
++pos;
}
return pos;
}
// Parses JSON string literal, handling escape sequences
std::string JsonParser::parseString(const std::string& json, size_t& pos)
{
if (json[pos] != '"') {
throw std::runtime_error("Expected '\"' at start of string");
}
++pos; // Skip opening quote
std::string result;
while (pos < json.length() && json[pos] != '"') {
if (json[pos] == '\\') {
++pos;
if (pos >= json.length()) {
throw std::runtime_error("Unterminated escape sequence");
}
// Handle basic escape sequences
switch (json[pos]) {
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;
default: result += json[pos]; break;
}
} else {
result += json[pos];
}
++pos;
}
if (pos >= json.length() || json[pos] != '"') {
throw std::runtime_error("Unterminated string");
}
++pos; // Skip closing quote
return result;
}
// Parses JSON number and returns string representation
std::string JsonParser::parseNumber(const std::string& json, size_t& pos)
{
size_t start = pos;
// Handle optional minus sign
if (json[pos] == '-') {
++pos;
}
// Parse digits
while (pos < json.length() && std::isdigit(json[pos])) {
++pos;
}
// Handle decimal point and fractional part
if (pos < json.length() && json[pos] == '.') {
++pos;
while (pos < json.length() && std::isdigit(json[pos])) {
++pos;
}
}
// Handle exponent
if (pos < json.length() && (json[pos] == 'e' || json[pos] == 'E')) {
++pos;
if (pos < json.length() && (json[pos] == '+' || json[pos] == '-')) {
++pos;
}
while (pos < json.length() && std::isdigit(json[pos])) {
++pos;
}
}
return json.substr(start, pos - start);
}
// Parses JSON value and stores it in the values map
void JsonParser::parseValue(const std::string& json, size_t& pos, const std::string& key)
{
pos = skipWhitespace(json, pos);
if (pos >= json.length()) {
throw std::runtime_error("Unexpected end of JSON");
}
if (json[pos] == '"') {
// String value - store directly
std::string value = parseString(json, pos);
std::string lower_key = key;
std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(), ::tolower);
values[lower_key] = value;
} else if (json[pos] == '{') {
// Nested object - recurse with current key as path prefix
parseObject(json, pos, key);
} else if (std::isdigit(json[pos]) || json[pos] == '-') {
// Number value - store as string
std::string value = parseNumber(json, pos);
std::string lower_key = key;
std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(), ::tolower);
values[lower_key] = value;
} else {
// Skip other JSON types (true, false, null, arrays)
while (pos < json.length() && json[pos] != ',' && json[pos] != '}') {
++pos;
}
}
}
// Recursively parses JSON object and builds flat key-value structure
void JsonParser::parseObject(const std::string& json, size_t& pos, const std::string& path)
{
pos = skipWhitespace(json, pos);
if (pos >= json.length() || json[pos] != '{') {
throw std::runtime_error("Expected '{' at start of object");
}
++pos; // Skip opening brace
pos = skipWhitespace(json, pos);
// Handle empty object
if (pos < json.length() && json[pos] == '}') {
++pos;
return;
}
while (pos < json.length()) {
pos = skipWhitespace(json, pos);
// Parse key
if (json[pos] != '"') {
throw std::runtime_error("Expected string key in object");
}
std::string key = parseString(json, pos);
pos = skipWhitespace(json, pos);
// Expect colon
if (pos >= json.length() || json[pos] != ':') {
throw std::runtime_error("Expected ':' after object key");
}
++pos;
// Build full path with dot notation
std::string full_key = path.empty() ? key : path + "." + key;
// Parse value
parseValue(json, pos, full_key);
pos = skipWhitespace(json, pos);
// Check for continuation or end
if (pos < json.length() && json[pos] == ',') {
++pos; // Skip comma
continue;
} else if (pos < json.length() && json[pos] == '}') {
++pos; // Skip closing brace
break;
} else {
throw std::runtime_error("Expected ',' or '}' in object");
}
}
}