From eb5191a72278a669deb7ffcedcde4871561acbbb Mon Sep 17 00:00:00 2001 From: Vyn Date: Mon, 11 Nov 2024 16:36:37 +0100 Subject: [PATCH] first commit --- .gitignore | 4 + CMakeLists.txt | 36 +++++++ README.md | 44 +++++++++ include/rei-json/Array.h | 42 ++++++++ include/rei-json/Field.h | 70 +++++++++++++ include/rei-json/Object.h | 44 +++++++++ include/rei-json/json.h | 14 +++ src/Array.cpp | 115 ++++++++++++++++++++++ src/Field.cpp | 127 ++++++++++++++++++++++++ src/Object.cpp | 119 ++++++++++++++++++++++ src/parse.cpp | 202 ++++++++++++++++++++++++++++++++++++++ src/toString.cpp | 103 +++++++++++++++++++ tests/parsing.cpp | 50 ++++++++++ tests/stringify.cpp | 54 ++++++++++ tests/usage.cpp | 51 ++++++++++ 15 files changed, 1075 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 include/rei-json/Array.h create mode 100644 include/rei-json/Field.h create mode 100644 include/rei-json/Object.h create mode 100644 include/rei-json/json.h create mode 100644 src/Array.cpp create mode 100644 src/Field.cpp create mode 100644 src/Object.cpp create mode 100644 src/parse.cpp create mode 100644 src/toString.cpp create mode 100644 tests/parsing.cpp create mode 100644 tests/stringify.cpp create mode 100644 tests/usage.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca54cba --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +.cache +build-profiler.sh +build-run.sh diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ad286d8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.21) +project(rei-json LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(SLINT_FEATURE_RENDERER_SKIA ON) +set(SLINT_FEATURE_RENDERER_SOFTWARE ON) + +add_library(rei-json + src/parse.cpp + src/toString.cpp + src/Object.cpp + src/Array.cpp + src/Field.cpp +) +target_include_directories(rei-json PRIVATE "external") +target_include_directories(rei-json PRIVATE "include") + +Include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.4.0 # or a later release +) + +FetchContent_MakeAvailable(Catch2) + +add_executable(tests + tests/usage.cpp + tests/parsing.cpp + tests/stringify.cpp +) +target_include_directories(tests PRIVATE "include") +target_link_libraries(tests PRIVATE rei-json) +target_link_libraries(tests PRIVATE Catch2::Catch2WithMain) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1800ff3 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# rei-cpp +> [!warning] +> This is a work in progress, not stable and the API might change. + +A C++ JSON library. + +## Example + +```cpp +#include "rei-json/json.h" +#include + +int main() { + auto objectJson = rei::json::JsonObject{}; + objectJson + .set("keyPositiveNumber", 12) + .set("keyNegativeNumber", -13) + .set("keyBooleanTrue", true) + .set("keyBooleanFalse", false) + .set("keyString", "YEP") + .set("keyEmptyString", "") + .setNull("keyNull"); + + rei::json::JsonObject obj{}; + obj.set("keyNumberOnObject", 42); + + rei::json::JsonArray array{}; + array.push(42); + array.push("elemString"); + array.push(""); + array.push(true); + array.push(false); + array.pushNull(); + + objectJson.addObject("keyObject", std::move(obj)); + objectJson.addArray("keyArray", std::move(array)); + + auto newJsonString = rei::json::toString(objectJson); + std::println("{}\n", newJsonString); + + return 0; +} +``` + diff --git a/include/rei-json/Array.h b/include/rei-json/Array.h new file mode 100644 index 0000000..6452003 --- /dev/null +++ b/include/rei-json/Array.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include + +namespace rei::json { + class JsonObject; + class FieldValue; + class JsonArray { + + public: + + JsonArray(); + + JsonArray& push(int value); + JsonArray& push(bool value); + JsonArray& push(const std::string& value); + JsonArray& push(const std::string&& value); + JsonArray& push(const char * value); + JsonArray& pushObject(const JsonObject& object); + JsonArray& pushObject(const JsonObject&& object); + JsonArray& pushArray(const JsonArray& array); + JsonArray& pushArray(const JsonArray&& array); + JsonArray& pushNull(); + + int getNumber(int index); + bool getBool(int index); + std::string getString(int index); + JsonObject& getObject(int index); + JsonArray& getArray(int index); + bool hasNull(int index); + + void forEach(std::function); + size_t count() const; + + private: + std::vector elements; + }; + +} diff --git a/include/rei-json/Field.h b/include/rei-json/Field.h new file mode 100644 index 0000000..dfef543 --- /dev/null +++ b/include/rei-json/Field.h @@ -0,0 +1,70 @@ +#pragma once + +#include "Object.h" +#include "Array.h" +#include + +namespace rei::json { + + class JsonObject; + class JsonArray; + + enum FieldType { + Null, + Number, + Bool, + String, + Object, + Array + }; + + class FieldValue { + + public: + + FieldValue(const FieldValue& other) noexcept; + FieldValue& operator=(const FieldValue& other) noexcept; + FieldValue& operator=(FieldValue&& other) noexcept; + FieldValue(FieldValue&& other) noexcept; + + FieldValue(FieldType type); + FieldValue(int value); + FieldValue(bool value); + FieldValue(const std::string& value); + FieldValue(std::string&& value); + FieldValue(const JsonObject& value); + FieldValue(JsonObject&& value); + FieldValue(const JsonArray& value); + FieldValue(JsonArray&& value) noexcept; + + ~FieldValue(); + + inline int asNumber() { return number; } + inline bool asBool() { return boolean; } + inline std::string& asString() { return string; } + inline JsonObject& asObject() { return object; } + inline JsonArray& asArray() { return array; } + + inline bool isNumber() const { return type == FieldType::Number; } + inline bool isBool() const { return type == FieldType::Bool; } + inline bool isString() const { return type == FieldType::String; } + inline bool isObject() const { return type == FieldType::Object; } + inline bool isArray() const { return type == FieldType::Array; } + inline bool isNull() const { return type == FieldType::Null; } + + private: + + void _copyValues(const FieldValue& other); + void _moveValues(FieldValue&& other); + void _destroy(); + + FieldType type; + union { + int number; + bool boolean; + std::string string; + JsonObject object; + JsonArray array; + }; + }; +} diff --git a/include/rei-json/Object.h b/include/rei-json/Object.h new file mode 100644 index 0000000..7d7fd74 --- /dev/null +++ b/include/rei-json/Object.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +namespace rei::json { + + class JsonArray; + class FieldValue; + + class JsonObject { + public: + + JsonObject(); + + bool contains(const std::string& key) const; + + JsonObject& set(const std::string& key, int value); + JsonObject& set(const std::string& key, bool value); + JsonObject& set(const std::string& key, const std::string& value); + JsonObject& set(const std::string& key, const std::string&& value); + JsonObject& set(const std::string& key, const char * value); + JsonObject& setNull(const std::string& key); + JsonObject& addArray(const std::string& key, const JsonArray& array); + JsonObject& addArray(const std::string& key, JsonArray&& array); + JsonObject& addObject(const std::string& key, const JsonObject& object); + JsonObject& addObject(const std::string& key, JsonObject&& object); + + int getNumber(const std::string& key); + bool getBool(const std::string& key); + std::string getString(const std::string& key); + JsonObject& getObject(const std::string& key); + JsonArray& getArray(const std::string& key); + bool hasNull(const std::string& key); + + void forEach(std::function); + size_t count() const; + + private: + + std::map elements; + }; +} diff --git a/include/rei-json/json.h b/include/rei-json/json.h new file mode 100644 index 0000000..adecdca --- /dev/null +++ b/include/rei-json/json.h @@ -0,0 +1,14 @@ +#pragma once +#include "Object.h" +#include "Array.h" +#include "Field.h" +#include +#include +namespace rei::json { + class std::variant parse(const std::string& jsonStr); + + std::string toString(std::variant& element); + std::string toString(JsonObject& object); + std::string toString(JsonArray& object); + +} diff --git a/src/Array.cpp b/src/Array.cpp new file mode 100644 index 0000000..36d1ddb --- /dev/null +++ b/src/Array.cpp @@ -0,0 +1,115 @@ +#include "rei-json/Field.h" +#include "rei-json/Array.h" +#include "rei-json/Object.h" +#include +#include +#include +namespace rei::json { + + JsonArray::JsonArray() { + elements.reserve(1000); + } + + JsonArray& JsonArray::push(int value) { + elements.push_back(value); + return *this; + } + + JsonArray& JsonArray::push(bool value) { + elements.push_back(value); + return *this; + } + + JsonArray& JsonArray::push(const char * value) { + elements.push_back(std::string(value)); + return *this; + } + + JsonArray& JsonArray::push(const std::string& value) { + elements.push_back(value); + return *this; + } + + JsonArray& JsonArray::push(const std::string&& value) { + elements.push_back(std::move(value)); + return *this; + } + + JsonArray& JsonArray::pushNull() { + elements.push_back(std::move(FieldValue{FieldType::Null})); + return *this; + } + + JsonArray& JsonArray::pushObject(const JsonObject& object) { + elements.push_back(object); + return *this; + }; + + JsonArray& JsonArray::pushObject(const JsonObject&& object) { + elements.push_back(std::move(object)); + return *this; + }; + + JsonArray& JsonArray::pushArray(const JsonArray& array) { + elements.push_back(array); + return *this; + }; + + JsonArray& JsonArray::pushArray(const JsonArray&& array) { + elements.push_back(std::move(array)); + return *this; + }; + + int JsonArray::getNumber(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].asNumber();; + } + + bool JsonArray::getBool(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].asBool();; + } + + std::string JsonArray::getString(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].asString(); + } + + JsonObject& JsonArray::getObject(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].asObject(); + } + + JsonArray& JsonArray::getArray(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].asArray(); + } + + bool JsonArray::hasNull(int index) { + if (index >= elements.size()) { + throw std::logic_error(std::string("Trying to access non existing index: ") + std::to_string(index)); + } + return elements[index].isNull(); + } + + void JsonArray::forEach(std::function func) { + for (int i = 0; i < elements.size(); ++i) { + func(elements[i], i); + } + } + + size_t JsonArray::count() const { + return elements.size(); + } + +} diff --git a/src/Field.cpp b/src/Field.cpp new file mode 100644 index 0000000..5dfcc23 --- /dev/null +++ b/src/Field.cpp @@ -0,0 +1,127 @@ +#include "rei-json/Field.h" +#include +#include +#include +#include + +namespace rei::json { + + + + FieldValue::FieldValue(const FieldValue& other) noexcept { + //std::println("I'm being copied ! (constructor) {} {}", static_cast(other.type), other.number); + _copyValues(std::move(other)); + } + + FieldValue& FieldValue::operator=(const FieldValue& other) noexcept { + //std::println("I'm being copied ! (operator) {}", static_cast(other.type)); + _destroy(); + _copyValues(std::move(other)); + return *this; + } + + FieldValue& FieldValue::operator=(FieldValue&& other) noexcept { + //std::println("I'm being moved ! (operator) {} {}", static_cast(other.type), other.number); + _destroy(); + _moveValues(std::move(other)); + return *this; + } + + FieldValue::FieldValue(FieldValue&& other) noexcept { + //std::println("I'm being moved ! (constructor) {} {}", static_cast(other.type), other.number); + _moveValues(std::move(other)); + } + + void FieldValue::_copyValues(const FieldValue& other) { + switch (other.type) { + case rei::json::FieldType::Number: + number = other.number; + break; + case rei::json::FieldType::Bool: + boolean = other.boolean; + break; + case rei::json::FieldType::String: + new (&string) std::string(other.string); + break; + case rei::json::FieldType::Object: + new (&object) JsonObject(other.object); + break; + case rei::json::FieldType::Array: + new (&array) JsonArray(other.array); + break; + } + type = other.type; + } + void FieldValue::_moveValues(FieldValue&& other) { + switch (other.type) { + case rei::json::FieldType::Number: + number = other.number; + break; + case rei::json::FieldType::Bool: + boolean = other.boolean; + break; + case rei::json::FieldType::String: + new (&string) std::string(std::move(other.string)); + break; + case rei::json::FieldType::Object: + new (&object) JsonObject(std::move(other.object)); + break; + case rei::json::FieldType::Array: + new (&array) JsonArray(std::move(other.array)); + break; + } + type = std::move(other.type); + } + + FieldValue::FieldValue(FieldType type) : number(0), type(type) { + + } + + FieldValue::FieldValue(int value) : number(value), type(FieldType::Number) { + + } + + FieldValue::FieldValue(bool value) : boolean(value), type(FieldType::Bool) { + + } + + FieldValue::FieldValue(const std::string& value) : string(value), type(FieldType::String) { + + } + + FieldValue::FieldValue(std::string&& value) : string(std::move(value)), type(FieldType::String) { + + } + + FieldValue::FieldValue(const JsonObject& value) : object(value), type(FieldType::Object) { + + } + + FieldValue::FieldValue(JsonObject&& value) : object(std::move(value)), type(FieldType::Object) { + + } + + FieldValue::FieldValue(const JsonArray& value) : array(value), type(FieldType::Array) { + //std::println("Array ! copy"); + } + + FieldValue::FieldValue(JsonArray&& value) noexcept : array(std::move(value)), type(FieldType::Array) { + //std::println("Array ! move"); + } + + FieldValue::~FieldValue() { + _destroy(); + } + + void FieldValue::_destroy() { + if (isString()) { + (&string)->std::string::~string(); + } else if (isObject()) { + object.~JsonObject(); + } else if (isArray()) { + array.~JsonArray(); + } + } + + +} diff --git a/src/Object.cpp b/src/Object.cpp new file mode 100644 index 0000000..681ba48 --- /dev/null +++ b/src/Object.cpp @@ -0,0 +1,119 @@ +#include "rei-json/json.h" +#include "rei-json/Field.h" +#include "rei-json/Array.h" +#include +#include +#include +#include +namespace rei::json { + + JsonObject::JsonObject() { + + }; + + bool JsonObject::contains(const std::string& key) const { + return elements.contains(key); + } + + JsonObject& JsonObject::set(const std::string& key, int value) { + elements.insert_or_assign(key, value); + return *this; + } + + JsonObject& JsonObject::set(const std::string& key, bool value) { + elements.insert_or_assign(key, value); + return *this; + } + + JsonObject& JsonObject::set(const std::string& key, const std::string& value) { + elements.insert_or_assign(key, value); + return *this; + } + + JsonObject& JsonObject::set(const std::string& key, const std::string&& value) { + elements.insert_or_assign(key, std::move(value)); + return *this; + } + + JsonObject& JsonObject::setNull(const std::string& key) { + elements.insert_or_assign(key, std::move(FieldValue{FieldType::Null})); + return *this; + } + + JsonObject& JsonObject::set(const std::string& key, const char * value) { + elements.insert_or_assign(key, std::string(value)); + return *this; + } + + int JsonObject::getNumber(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).asNumber(); + } + + bool JsonObject::getBool(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).asBool(); + } + + std::string JsonObject::getString(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).asString(); + } + + JsonObject& JsonObject::getObject(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).asObject(); + } + + JsonArray& JsonObject::getArray(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).asArray(); + } + + bool JsonObject::hasNull(const std::string& key) { + if (!contains(key)) { + throw std::logic_error(std::string("Trying to access non existing key: ") + key); + } + return elements.at(key).isNull(); + } + + JsonObject& JsonObject::addArray(const std::string& key, const JsonArray& array) { + elements.insert_or_assign(key, array); + return *this; + } + + JsonObject& JsonObject::addArray(const std::string& key, JsonArray&& array) { + elements.insert_or_assign(key, std::move(array)); + return *this; + } + + JsonObject& JsonObject::addObject(const std::string& key, const JsonObject& object) { + elements.insert_or_assign(key, object); + return *this; + } + + JsonObject& JsonObject::addObject(const std::string& key, JsonObject&& object) { + elements.insert_or_assign(key, std::move(object)); + return *this; + } + + void JsonObject::forEach(std::function func) { + for (auto& element : elements) { + func(element.second, element.first); + } + } + + size_t JsonObject::count() const { + return elements.size(); + } +} diff --git a/src/parse.cpp b/src/parse.cpp new file mode 100644 index 0000000..7c457ab --- /dev/null +++ b/src/parse.cpp @@ -0,0 +1,202 @@ +#include "rei-json/Array.h" +#include "rei-json/Field.h" +#include "rei-json/json.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace rei::json { + + void goToNextDoubleQuote(const std::string& str, int* const i) { + while (str[*i] != '"') { + if (str[*i] == '\\') { + ++(*i); + } + ++(*i); + } + } + + void skipDigits(const std::string& str, int* const i) { + while (isdigit(str[*i]) || str[*i] == '-') { + ++(*i); + } + } + + void skipWhiteSpaces(const std::string& str, int* const i) { + while (str[*i] == ' ' || str[*i] == '\t' || str[*i] == '\n' || str[*i] == '\v') { + ++(*i); + } + } + + void parseObject(const std::string &str, int *i, JsonObject* object); + void parseArray(const std::string &str, int *i, JsonArray* object); + + void parseObjectField(const std::string &str, int *i, JsonObject* object) { + if (str[*i] != '"') { + throw "wrong json: expected opening quote"; + } + ++(*i); + const int keyStart = *i; + goToNextDoubleQuote(str, i); + const int keyLength = *i - keyStart; + const std::string key = str.substr(keyStart, keyLength); + ++(*i); + skipWhiteSpaces(str, i); + if (str[*i] != ':') { + throw "wrong json: expect 2 dots"; + } + ++(*i); + skipWhiteSpaces(str, i); + + if (str[*i] == '"') { + ++(*i); + const int valueStart = *i; + goToNextDoubleQuote(str, i); + const int valueLength = *i - valueStart; + const std::string valueStr = str.substr(valueStart, valueLength); + object->set(key, std::move(valueStr)); + ++(*i); + } else if (isdigit(str[*i]) || str[*i] == '-') { + const int valueStart = *i; + skipDigits(str, i); + const int valueLength = *i - valueStart; + const std::string valueStr = str.substr(valueStart, valueLength); + int value; + std::from_chars(str.c_str() + valueStart, str.c_str() + valueStart + valueLength, value); + object->set(key, value); + } else if (std::string_view{str}.substr(*i, 4) == "true") { + const int valueStart = *i; + *i += 4; + const int valueLength = *i - valueStart; + object->set(key, true); + } else if (std::string_view{str}.substr(*i, 5) == "false") { + const int valueStart = *i; + *i += 5; + const int valueLength = *i - valueStart; + object->set(key, false); + } else if (std::string_view{str}.substr(*i, 4) == "null") { + *i += 4; + object->setNull(key); + } else if (str[*i] == '{'){ + JsonObject newObject{}; + parseObject(str, i, &newObject); + object->addObject(key, std::move(newObject)); + } else if (str[*i] == '['){ + JsonArray newArray{}; + parseArray(str, i, &newArray); + object->addArray(key, std::move(newArray)); + } + } + + void parseArrayField(const std::string &str, int *i, JsonArray* array) { + skipWhiteSpaces(str, i); + if (str[*i] == '"') { + ++(*i); + const int valueStart = *i; + goToNextDoubleQuote(str, i); + const int valueLength = *i - valueStart; + const std::string valueStr = str.substr(valueStart, valueLength); + array->push(std::move(valueStr)); + ++(*i); + } else if (isdigit(str[*i])) { + const int valueStart = *i; + skipDigits(str, i); + const int valueLength = *i - valueStart; + const std::string valueStr = str.substr(valueStart, valueLength); + array->push(std::stoi(valueStr)); + } else if (std::string_view{str}.substr(*i, 4) == "true") { + const int valueStart = *i; + *i += 4; + const int valueLength = *i - valueStart; + array->push(true); + } else if (std::string_view{str}.substr(*i, 4) == "null") { + *i += 4; + array->pushNull(); + } else if (std::string_view{str}.substr(*i, 5) == "false") { + const int valueStart = *i; + *i += 5; + const int valueLength = *i - valueStart; + array->push(false); + } else if (str[*i] == '{'){ + JsonObject newObject{}; + parseObject(str, i, &newObject); + array->pushObject(std::move(newObject)); + } else if (str[*i] == '['){ + JsonArray newArray{}; + parseArray(str, i, &newArray); + array->pushArray(std::move(newArray)); + } + + } + + void parseArray(const std::string &str, int *i, JsonArray* object) { + if (str[*i] != '[') { + throw "wrong json"; + } + ++(*i); + skipWhiteSpaces(str, i); + while (str[*i] != ']') { + parseArrayField(str, i, object); + skipWhiteSpaces(str, i); + if (str[*i] == ',') { + ++(*i); + skipWhiteSpaces(str, i); + if (str[*i] == ']') { + throw "wrong json: extra comma on last array field"; + } + } else if (str[*i] != ']') { + throw "wrong json: expected array end"; + } + } + ++(*i); + } + + void parseObject(const std::string &str, int *i, JsonObject* object) { + if (str[*i] != '{') { + throw "wrong json"; + } + ++(*i); + skipWhiteSpaces(str, i); + while (str[*i] != '}') { + if (str[*i] != '"') { + throw "wrong json: expect opening key quote"; + } + parseObjectField(str, i, object); + skipWhiteSpaces(str, i); + if (str[*i] == ',') { + ++(*i); + skipWhiteSpaces(str, i); + if (str[*i] == '}') { + throw "wrong json: extra comma on last object field"; + } + } else if (str[*i] != '}') { + const std::string sstr = str.substr(*i - 1); + throw "wrong json: expected object end"; + } + } + ++(*i); + + } + + std::variant parse(const std::string& jsonStr) { + int i = 0; + skipWhiteSpaces(jsonStr, &i); + if (jsonStr[i] == '{') { + JsonObject object{}; + parseObject(jsonStr, &i, &object); + return std::move(object); + } else if (jsonStr[i] == '[') { + JsonArray array{}; + parseArray(jsonStr, &i, &array); + return std::move(array); + } + throw std::runtime_error("Bad json syntax"); + } +} diff --git a/src/toString.cpp b/src/toString.cpp new file mode 100644 index 0000000..b522c44 --- /dev/null +++ b/src/toString.cpp @@ -0,0 +1,103 @@ +#include "rei-json/Array.h" +#include "rei-json/Field.h" +#include "rei-json/Object.h" +#include "rei-json/json.h" +#include +#include +#include +#include +#include +namespace rei::json { + + void arrayToString(JsonArray& array, std::string& str, int currentIndentation, int indentIncrement); + + inline void objectToString(JsonObject& object, std::string& str, int currentIndentation, int indentIncrement) { + int elementCount = object.count(); + //std::string str = "{"; + str += "{"; + currentIndentation += indentIncrement; + std::string padding = "\n" + std::string(currentIndentation, ' '); + object.forEach([&](FieldValue& element, const std::string& key) { + str += padding + "\"" + key + "\": "; + if (element.isNumber()) { + str += std::to_string(element.asNumber()); + } else if (element.isBool()) { + str += element.asBool() ? "true" : "false"; + } else if (element.isNull()) { + str += "null"; + } else if (element.isString()) { + str += "\"" + element.asString() + "\""; + } else if (element.isObject()) { + objectToString(element.asObject(), str, currentIndentation, indentIncrement); + } else if (element.isArray()) { + arrayToString(element.asArray(), str, currentIndentation, indentIncrement); + } + elementCount--; + if (elementCount > 0) { + str += ","; + } + }); + currentIndentation -= indentIncrement; + if (object.count() > 0) { + str += "\n" + std::string(currentIndentation, ' '); + } + str += "}"; + //return str; + } + + inline void arrayToString(JsonArray& array, std::string& str, int currentIndentation, int indentIncrement) { + int elementCount = array.count(); + //std::string str = "["; + str += "["; + currentIndentation += indentIncrement; + std::string padding = "\n" + std::string(currentIndentation, ' '); + array.forEach([&](FieldValue& element, int index) { + str += padding; + if (element.isNumber()) { + str += std::to_string(element.asNumber()); + } else if (element.isBool()) { + str += element.asBool() ? "true" : "false"; + } else if (element.isNull()) { + str += "null"; + } else if (element.isString()) { + str += "\"" + element.asString() + "\""; + } else if (element.isObject()) { + objectToString(element.asObject(), str, currentIndentation, indentIncrement); + } else if (element.isArray()) { + arrayToString(element.asArray(), str, currentIndentation, indentIncrement); + } + --elementCount; + if (elementCount > 0) { + str += ","; + } + }); + currentIndentation -= indentIncrement; + if (array.count() > 0) { + str += "\n" + std::string(currentIndentation, ' '); + } + str += "]"; + //return str; + } + + std::string toString(std::variant& element) { + if (std::holds_alternative(element)) { + return toString(std::get(element)); + } else if (std::holds_alternative(element)) { + return toString(std::get(element)); + + } + throw std::runtime_error("Bad json format"); + } + + std::string toString(JsonObject& object) { + std::string str; + objectToString(object, str, 0, 4); + return str; + } + + std::string toString(JsonArray& array) { + std::string str; + arrayToString(array, str, 0, 4); + return str; + } +} diff --git a/tests/parsing.cpp b/tests/parsing.cpp new file mode 100644 index 0000000..4ba920a --- /dev/null +++ b/tests/parsing.cpp @@ -0,0 +1,50 @@ +#include "rei-json/Object.h" +#include "rei-json/json.h" +#include +#include + +TEST_CASE("Parsing json object") { + std::string jsonStr = R"({ + "keyArray": [ + 42, + "elemString", + "", + true, + false, + null + ], + "keyBooleanFalse": false, + "keyBooleanTrue": true, + "keyEmptyString": "", + "keyNegativeNumber": -13, + "keyNull": null, + "keyObject": { + "keyNumberOnObject": 42 + }, + "keyPositiveNumber": 12, + "keyString": "YEP" +})"; + auto json = rei::json::parse(jsonStr); + auto& objectJson = std::get(json); + + REQUIRE(objectJson.getNumber("keyPositiveNumber") == 12); + REQUIRE(objectJson.getNumber("keyNegativeNumber") == -13); + REQUIRE(objectJson.getBool("keyBooleanTrue") == true); + REQUIRE(objectJson.getBool("keyBooleanFalse") == false); + REQUIRE(objectJson.getString("keyString") == "YEP"); + REQUIRE(objectJson.getString("keyEmptyString") == ""); + REQUIRE(objectJson.getObject("keyObject").getNumber("keyNumberOnObject") == 42); + REQUIRE(objectJson.hasNull("keyNull") == true); + REQUIRE(objectJson.hasNull("keyPositiveNumber") == false); + REQUIRE_THROWS(objectJson.getNumber("nonExistantKey")); + + auto& arrayJson = objectJson.getArray("keyArray"); + REQUIRE(arrayJson.getNumber(0) == 42); + REQUIRE(arrayJson.getString(1) == "elemString"); + REQUIRE(arrayJson.getString(2) == ""); + REQUIRE(arrayJson.getBool(3) == true); + REQUIRE(arrayJson.getBool(4) == false); + REQUIRE(arrayJson.hasNull(5)); + REQUIRE_THROWS(arrayJson.getNumber(99)); +} + diff --git a/tests/stringify.cpp b/tests/stringify.cpp new file mode 100644 index 0000000..4dbcb78 --- /dev/null +++ b/tests/stringify.cpp @@ -0,0 +1,54 @@ +#include "rei-json/json.h" +#include +#include + +TEST_CASE("Stringify json") { + auto objectJson = rei::json::JsonObject{}; + objectJson + .set("keyPositiveNumber", 12) + .set("keyNegativeNumber", -13) + .set("keyBooleanTrue", true) + .set("keyBooleanFalse", false) + .set("keyString", "YEP") + .set("keyEmptyString", "") + .setNull("keyNull"); + + rei::json::JsonObject obj{}; + obj.set("keyNumberOnObject", 42); + + rei::json::JsonArray array{}; + array.push(42); + array.push("elemString"); + array.push(""); + array.push(true); + array.push(false); + array.pushNull(); + + objectJson.addObject("keyObject", std::move(obj)); + objectJson.addArray("keyArray", std::move(array)); + + std::string expectedJson = R"({ + "keyArray": [ + 42, + "elemString", + "", + true, + false, + null + ], + "keyBooleanFalse": false, + "keyBooleanTrue": true, + "keyEmptyString": "", + "keyNegativeNumber": -13, + "keyNull": null, + "keyObject": { + "keyNumberOnObject": 42 + }, + "keyPositiveNumber": 12, + "keyString": "YEP" +})"; + + REQUIRE(rei::json::toString(objectJson) == expectedJson); +} + + diff --git a/tests/usage.cpp b/tests/usage.cpp new file mode 100644 index 0000000..f600e21 --- /dev/null +++ b/tests/usage.cpp @@ -0,0 +1,51 @@ +#include "rei-json/json.h" +#include +#include +#include +#include + +TEST_CASE("Basic json") { + auto objectJson = rei::json::JsonObject{}; + objectJson + .set("keyPositiveNumber", 12) + .set("keyNegativeNumber", -13) + .set("keyBooleanTrue", true) + .set("keyBooleanFalse", false) + .set("keyString", "YEP") + .set("keyEmptyString", "") + .setNull("keyNull"); + + rei::json::JsonObject obj{}; + obj.set("keyNumberOnObject", 42); + + rei::json::JsonArray array{}; + array.push(42); + array.push("elemString"); + array.push(""); + array.push(true); + array.push(false); + array.pushNull(); + + objectJson.addObject("keyObject", std::move(obj)); + objectJson.addArray("keyArray", std::move(array)); + + REQUIRE(objectJson.getNumber("keyPositiveNumber") == 12); + REQUIRE(objectJson.getNumber("keyNegativeNumber") == -13); + REQUIRE(objectJson.getBool("keyBooleanTrue") == true); + REQUIRE(objectJson.getBool("keyBooleanFalse") == false); + REQUIRE(objectJson.getString("keyString") == "YEP"); + REQUIRE(objectJson.getString("keyEmptyString") == ""); + REQUIRE(objectJson.getObject("keyObject").getNumber("keyNumberOnObject") == 42); + REQUIRE(objectJson.hasNull("keyNull") == true); + REQUIRE(objectJson.hasNull("keyPositiveNumber") == false); + REQUIRE_THROWS(objectJson.getNumber("nonExistantKey")); + + auto& arrayJson = objectJson.getArray("keyArray"); + REQUIRE(arrayJson.getNumber(0) == 42); + REQUIRE(arrayJson.getString(1) == "elemString"); + REQUIRE(arrayJson.getString(2) == ""); + REQUIRE(arrayJson.getBool(3) == true); + REQUIRE(arrayJson.getBool(4) == false); + REQUIRE(arrayJson.hasNull(5)); + REQUIRE_THROWS(arrayJson.getNumber(99)); +}