diff --git a/src/xrt/auxiliary/CMakeLists.txt b/src/xrt/auxiliary/CMakeLists.txt index c4c5c9d42..906525f1d 100644 --- a/src/xrt/auxiliary/CMakeLists.txt +++ b/src/xrt/auxiliary/CMakeLists.txt @@ -171,6 +171,7 @@ add_library( util/u_hashset.h util/u_json.c util/u_json.h + util/u_json.hpp util/u_logging.c util/u_logging.h util/u_misc.c diff --git a/src/xrt/auxiliary/meson.build b/src/xrt/auxiliary/meson.build index 610b1586e..8d2c2c15f 100644 --- a/src/xrt/auxiliary/meson.build +++ b/src/xrt/auxiliary/meson.build @@ -52,6 +52,7 @@ lib_aux_util = static_library( 'util/u_hashset.h', 'util/u_json.c', 'util/u_json.h', + 'util/u_json.hpp', 'util/u_logging.c', 'util/u_logging.h', 'util/u_misc.c', diff --git a/src/xrt/auxiliary/util/u_json.hpp b/src/xrt/auxiliary/util/u_json.hpp new file mode 100644 index 000000000..32bb11fe5 --- /dev/null +++ b/src/xrt/auxiliary/util/u_json.hpp @@ -0,0 +1,630 @@ +// Copyright 2021, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief C++ wrapper for cJSON. + * @author Mateo de Mayo + * @ingroup aux_util + */ + +#pragma once + +#include "util/u_file.h" +#include "util/u_debug.h" +#include "cjson/cJSON.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +DEBUG_GET_ONCE_LOG_OPTION(json_log, "JSON_LOG", U_LOGGING_WARN) + +#define JSON_TRACE(...) U_LOG_IFL_T(debug_get_log_option_json_log(), __VA_ARGS__) +#define JSON_DEBUG(...) U_LOG_IFL_D(debug_get_log_option_json_log(), __VA_ARGS__) +#define JSON_INFO(...) U_LOG_IFL_I(debug_get_log_option_json_log(), __VA_ARGS__) +#define JSON_WARN(...) U_LOG_IFL_W(debug_get_log_option_json_log(), __VA_ARGS__) +#define JSON_ERROR(...) U_LOG_IFL_E(debug_get_log_option_json_log(), __VA_ARGS__) +#define JSON_ASSERT(fatal, predicate, ...) \ + do { \ + bool p = predicate; \ + if (!p) { \ + JSON_ERROR(__VA_ARGS__); \ + if (fatal) { \ + assert(false && "Assertion failed: " #predicate); \ + exit(EXIT_FAILURE); \ + } \ + } \ + } while (false); + +// Fatal assertion +#define JSON_ASSERTF(...) JSON_ASSERT(true, __VA_ARGS__) +#define JSON_ASSERTF_(predicate) JSON_ASSERT(true, predicate, "Assertion failed " #predicate) + +// Warn-only assertion +#define JSON_ASSERTW(...) JSON_ASSERT(false, __VA_ARGS__) + +namespace xrt::auxiliary::util::json { + +using std::get; +using std::get_if; +using std::holds_alternative; +using std::make_shared; +using std::map; +using std::shared_ptr; +using std::string; +using std::to_string; +using std::variant; +using std::vector; + +class JSONBuilder; + +/*! + * @brief A JSONNode wraps a cJSON object and presents useful functions for + * accessing the different properties of the json structure like `operator[]`, + * `isType()` and `asType()` methods. + * + * The main ways a user can build a JSONNode is from a json string, from a + * json file with the @ref loadFromFile or with the @ref JSONBuilder. + */ +class JSONNode +{ +private: + friend class JSONBuilder; + using Ptr = shared_ptr; + + //! Wrapped cJSON object + cJSON *cjson = nullptr; + + //! Whether this node is responsible for deleting the cjson object + bool is_owner = false; + + //! Parent of this node, used only by @ref JSONBuilder. + JSONNode::Ptr parent = nullptr; + +public: + // Class resource management + + //! This is public just so that make_shared works; do not use outside of this file. + JSONNode(cJSON *cjson, bool is_owner, const JSONNode::Ptr &parent) + : cjson(cjson), is_owner(is_owner), parent(parent) + {} + + //! Makes a null object; `isInvalid()` on it returns true. + JSONNode() {} + + //! Receives a json string and constructs a wrapped cJSON object out of it. + JSONNode(const string &content) + { + cjson = cJSON_Parse(content.c_str()); + if (cjson == nullptr) { + const int max_length = 64; + string msg = string(cJSON_GetErrorPtr()).substr(0, max_length); + JSON_ERROR("Invalid syntax right before: '%s'", msg.c_str()); + return; + } + is_owner = true; + parent = nullptr; + } + + JSONNode(JSONNode &&) = default; + + JSONNode(const JSONNode &node) + { + is_owner = node.is_owner; + parent = node.parent; + if (node.is_owner) { + cjson = cJSON_Duplicate(node.cjson, true); // Deep copy + } else { + cjson = node.cjson; // Shallow copy + } + }; + + JSONNode & + operator=(JSONNode &&) = default; + + JSONNode & + operator=(JSONNode rhs) + { + swap(*this, rhs); + return *this; + }; + + ~JSONNode() + { + if (is_owner) { + cJSON_Delete(cjson); + } + } + + friend void + swap(JSONNode &lhs, JSONNode &rhs) noexcept + { + using std::swap; + swap(lhs.cjson, rhs.cjson); + swap(lhs.is_owner, rhs.is_owner); + swap(lhs.parent, rhs.parent); + } + + // Methods for explicit usage by the user of this class + + static JSONNode + loadFromFile(const string &filepath) + { + std::ifstream file(filepath); + if (!file.is_open()) { + JSON_ERROR("Unable to open file %s", filepath.c_str()); + return JSONNode{}; + } + + std::stringstream stream{}; + stream << file.rdbuf(); + string content = stream.str(); + + return JSONNode{content}; + } + + bool + saveToFile(const string &filepath) const + { + string contents = toString(false); + std::ofstream file(filepath); + + if (!file.is_open()) { + JSON_ERROR("Unable to open file %s", filepath.c_str()); + return false; + } + + file << contents; + return true; + } + + JSONNode + operator[](const string &key) const + { + const char *name = key.c_str(); + JSON_ASSERTW(isObject(), "Trying to access field '%s' from non-object %s", name, toString().c_str()); + + cJSON *value = cJSON_GetObjectItemCaseSensitive(cjson, name); + JSON_ASSERTW(value != nullptr, "Unable to retrieve field '%s' from %s", name, toString().c_str()); + + return JSONNode{value, false, nullptr}; + } + + JSONNode + operator[](int i) const + { + JSON_ASSERTW(isArray(), "Trying to access index '%d' from non-array %s", i, toString().c_str()); + + cJSON *value = cJSON_GetArrayItem(cjson, i); + JSON_ASSERTW(value != nullptr, "Unable to retrieve index %d from %s", i, toString().c_str()); + + return JSONNode{value, false, nullptr}; + } + + // clang-format off + bool isObject() const { return cJSON_IsObject(cjson); } + bool isArray() const { return cJSON_IsArray(cjson); } + bool isString() const { return cJSON_IsString(cjson); } + bool isNumber() const { return cJSON_IsNumber(cjson); } + bool isInt() const { return isNumber() && cjson->valuedouble == cjson->valueint; } + bool isDouble() const { return isNumber(); } + bool isNull() const { return cJSON_IsNull(cjson); } + bool isBool() const { return cJSON_IsBool(cjson); } + bool isInvalid() const { return cjson == nullptr || cJSON_IsInvalid(cjson); } + bool isValid() const { return !isInvalid(); } + + bool canBool() const { return isBool() || (isInt() && (cjson->valueint == 0 || cjson->valueint == 1)); } + // clang-format on + + map + asObject(const map &otherwise = map()) const + { + JSON_ASSERTW(isObject(), "Invalid object: %s, defaults", toString().c_str()); + if (isObject()) { + map object{}; + + cJSON *item = NULL; + cJSON_ArrayForEach(item, cjson) + { + const char *key = item->string; + JSON_ASSERTF(key, "Unexpected unnamed pair in json: %s", toString().c_str()); + JSON_ASSERTW(object.count(key) == 0, "Duplicated key '%s'", key); + object.insert({key, JSONNode{item, false, nullptr}}); + } + + return object; + } + return otherwise; + } + + vector + asArray(const vector &otherwise = vector()) const + { + JSON_ASSERTW(isArray(), "Invalid array: %s, defaults", toString().c_str()); + if (isArray()) { + vector array{}; + + cJSON *item = NULL; + cJSON_ArrayForEach(item, cjson) + { + array.push_back(JSONNode{item, false, nullptr}); + } + + return array; + } + return otherwise; + } + + string + asString(const string &otherwise = "") const + { + JSON_ASSERTW(isString(), "Invalid string: %s, defaults %s", toString().c_str(), otherwise.c_str()); + return isString() ? cjson->valuestring : otherwise; + } + + int + asInt(int otherwise = 0) const + { + JSON_ASSERTW(isInt(), "Invalid int: %s, defaults %d", toString().c_str(), otherwise); + return isInt() ? cjson->valueint : otherwise; + } + + double + asDouble(double otherwise = 0.0) const + { + JSON_ASSERTW(isDouble(), "Invalid double: %s, defaults %lf", toString().c_str(), otherwise); + return isDouble() ? cjson->valuedouble : otherwise; + } + + void * + asNull(void *otherwise = nullptr) const + { + JSON_ASSERTW(isNull(), "Invalid null: %s, defaults %p", toString().c_str(), otherwise); + return isNull() ? nullptr : otherwise; + } + + bool + asBool(bool otherwise = false) const + { + JSON_ASSERTW(canBool(), "Invalid bool: %s, defaults %d", toString().c_str(), otherwise); + return isBool() ? cJSON_IsTrue(cjson) : (canBool() ? cjson->valueint : otherwise); + } + + bool + hasKey(const string &key) + { + return asObject().count(key) == 1; + } + + string + toString(bool show_field = true) const + { + char *cstr = cJSON_Print(cjson); + string str{cstr}; + free(cstr); + + // Show the named field this comes from if any + if (show_field) { + str += "\nFrom field named: " + getName(); + } + + return str; + } + + string + getName() const + { + return string(cjson->string ? cjson->string : ""); + } +}; + + +/*! + * @brief Helper class for building cJSON trees through `operator<<` + * + * JSONBuild is implemented with a pushdown automata to keep track of the JSON + * construction state. + * + */ +class JSONBuilder +{ +private: + enum class StackAlphabet + { + Base, // Unique occurrence as the base of the stack + Array, + Object + }; + + enum class State + { + Empty, + BuildArray, + BuildObjectKey, + BuildObjectValue, + Finish, + Invalid + }; + + enum class InputAlphabet + { + StartArray, + EndArray, + StartObject, + EndObject, + PushKey, + PushValue, + }; + + using G = StackAlphabet; + using S = InputAlphabet; + using Q = State; + + std::stack stack{{G::Base}}; + State state{Q::Empty}; + JSONNode::Ptr node = nullptr; //!< Current node we are pointing to in the tree. + + using JSONValue = variant; + + //! String representation of @p value. + static string + valueToString(const JSONValue &value) + { + string s = "JSONValue()"; + if (const string *v = get_if(&value)) { + s = string{"JSONValue("} + *v + ")"; + } else if (const char *const *v = get_if(&value)) { + s = string{"JSONValue("} + *v + ")"; + } else if (const int *v = get_if(&value)) { + s = string{"JSONValue("} + to_string(*v) + ")"; + } else if (const double *v = get_if(&value)) { + s = string{"JSONValue("} + to_string(*v) + ")"; + } else if (const bool *v = get_if(&value)) { + s = string{"JSONValue("} + to_string(*v) + ")"; + } else { + JSON_ASSERTF(false, "Unsupported variant type"); + s = "[Invalid JSONValue]"; + } + return s; + } + + //! Construct a cJSON object out of native types. + static cJSON * + makeCJSONValue(const JSONValue &value) + { + cJSON *ret = nullptr; + if (holds_alternative(value)) { + ret = cJSON_CreateString(get(value).c_str()); + } else if (holds_alternative(value)) { + ret = cJSON_CreateString(get(value)); + } else if (holds_alternative(value)) { + ret = cJSON_CreateNumber(get(value)); + } else if (holds_alternative(value)) { + ret = cJSON_CreateNumber(get(value)); + } else if (holds_alternative(value)) { + ret = cJSON_CreateBool(get(value)); + } else { + JSON_ASSERTF(false, "Unexpected value"); + } + return ret; + } + + /*! + * @brief Receives inputs and transitions the automata from state to state. + * + * This is the table of transitions. Can be thought of as three regular FSM + * that get switched based on the stack's [top] value. The function is just + * the implementation of the table. + * + * [top], [state], [symbol] -> [new-state], [stack-action] + * Base, Empty, PushValue -> Finish, - + * Base, Empty, StartObject -> BuildObjectKey, push(Object) + * Base, Empty, StartArray -> BuildArray, push(Array) + * Array, BuildArray, PushValue -> BuildArray, - + * Array, BuildArray, StartArray -> BuildArray, push(Array) + * Array, BuildArray, EndArray -> [1], pop + * Array, BuildArray, StartObject -> BuildObjectKey, push(Object) + * Object, BuildObjectKey, PushKey -> BuildObjectValue, - + * Object, BuildObjectKey, EndObject -> [1], pop + * Object, BuildObjectValue, PushValue -> BuildObjectKey, - + * Object, BuildObjectValue, StartObject -> BuildObjectKey, push(Object) + * Object, BuildObjectValue, StartArray -> BuildArray, push(Array) + * _, _, _, -> Invalid, - + * + * [1]: Empty or BuildArray or BuildObjectKey depending on new stack.top + */ + void + transition(InputAlphabet symbol, const JSONValue &value) + { + StackAlphabet top = stack.top(); + JSON_DEBUG("stacksz=%zu top=%d state=%d symbol=%d value=%s", stack.size(), static_cast(top), + static_cast(state), static_cast(symbol), valueToString(value).c_str()); + + // This is basically an if-defined transition function for a pushdown automata + if (top == G::Base && state == Q::Empty && symbol == S::PushValue) { + + JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_value = makeCJSONValue(value); + node = make_shared(cjson_value, true, nullptr); + + state = Q::Finish; + + } else if (top == G::Base && state == Q::Empty && symbol == S::StartObject) { + + JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_object = cJSON_CreateObject(); + node = make_shared(cjson_object, true, nullptr); + + state = Q::BuildObjectKey; + stack.push(G::Object); + + } else if (top == G::Base && state == Q::Empty && symbol == S::StartArray) { + + JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_array = cJSON_CreateArray(); + node = make_shared(cjson_array, true, nullptr); + + state = Q::BuildArray; + stack.push(G::Array); + + } else if (top == G::Array && state == Q::BuildArray && symbol == S::PushValue) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_value = makeCJSONValue(value); + cJSON_AddItemToArray(node->cjson, cjson_value); + // node = node; // The current node does not change, it is still the array + + state = Q::BuildArray; + + } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartArray) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_array = cJSON_CreateArray(); + cJSON_AddItemToArray(node->cjson, cjson_array); + node = make_shared(cjson_array, false, node); + + state = Q::BuildArray; + stack.push(G::Array); + + } else if (top == G::Array && state == Q::BuildArray && symbol == S::EndArray) { + + stack.pop(); + map m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}}; + state = m[stack.top()]; + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + if (node->parent) { + node = node->parent; + } else { + JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without"); + } + + } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartObject) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + cJSON *cjson_object = cJSON_CreateObject(); + cJSON_AddItemToArray(node->cjson, cjson_object); + node = make_shared(cjson_object, false, node); + + state = Q::BuildObjectKey; + stack.push(G::Object); + + } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::PushKey) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + JSON_ASSERTF(holds_alternative(value), "Non-string key not allowed"); + cJSON *cjson_null = cJSON_CreateNull(); + cJSON_AddItemToObject(node->cjson, get(value).c_str(), cjson_null); + node = make_shared(cjson_null, false, node); + + state = Q::BuildObjectValue; + + } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::EndObject) { + + stack.pop(); + map m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}}; + state = m[stack.top()]; + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + if (node->parent) { + node = node->parent; + } else { + JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without") + } + + } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::PushValue) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null"); + cJSON *cjson_value = makeCJSONValue(value); + cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_value); + node->cjson = cjson_value; + node = node->parent; + + state = Q::BuildObjectKey; + + } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartObject) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null"); + cJSON *cjson_object = cJSON_CreateObject(); + cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_object); + node->cjson = cjson_object; + + state = Q::BuildObjectKey; + stack.push(G::Object); + + } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartArray) { + + JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str()); + JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null"); + cJSON *cjson_array = cJSON_CreateArray(); + cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_array); + node->cjson = cjson_array; + + state = Q::BuildArray; + stack.push(G::Array); + + } else { + + JSON_ASSERTF(false, "Invalid construction transition: top=%d state=%d symbol=%d value=%s", + static_cast(top), static_cast(state), static_cast(symbol), + valueToString(value).c_str()); + node = make_shared(); + + state = Q::Invalid; + } + + JSON_DEBUG("After transition: node=%p parent=%p\n", (void *)node.get(), + (void *)(node ? node->parent.get() : nullptr)); + } + +public: + JSONBuilder() {} + + //! Receives "[", "]", "{", "}", or any of string, const char*, double, int, + //! bool as inputs. Updates the JSONBuilder state with it, after finishing the + //! JSON tree, obtain the result with @ref getBuiltNode. + JSONBuilder & + operator<<(const JSONValue &value) + { + bool is_string = holds_alternative(value) || holds_alternative(value); + if (!is_string) { + transition(S::PushValue, value); + return *this; + } + + string as_string = holds_alternative(value) ? get(value) : get(value); + if (as_string == "[") { + transition(S::StartArray, as_string); + } else if (as_string == "]") { + transition(S::EndArray, as_string); + } else if (as_string == "{") { + transition(S::StartObject, as_string); + } else if (as_string == "}") { + transition(S::EndObject, as_string); + } else if (state == Q::BuildObjectKey) { + transition(S::PushKey, as_string); + } else if (state == Q::BuildObjectValue) { + transition(S::PushValue, as_string); + } else { + JSON_ASSERTF(false, "Invalid state=%d value=%s", static_cast(state), as_string.c_str()); + } + return *this; + } + + //! Gets the built JSONNode or crash if the construction has not finished + JSONNode::Ptr + getBuiltNode() + { + JSON_ASSERTF(state == Q::Finish, "Trying to getBuiltNode but the construction has not ended"); + return node; + } +}; + +} // namespace xrt::auxiliary::util::json diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fdc52a40d..bb1a0b227 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,3 +22,9 @@ add_executable(tests_generic_callbacks tests_generic_callbacks.cpp) target_link_libraries(tests_generic_callbacks PRIVATE tests_main) target_link_libraries(tests_generic_callbacks PRIVATE aux_util) add_test(NAME tests_generic_callbacks COMMAND tests_generic_callbacks --success) + +# cJSON Wrapper +add_executable(tests_json tests_json.cpp) +target_link_libraries(tests_json PRIVATE tests_main) +target_link_libraries(tests_json PRIVATE aux_util) +add_test(NAME tests_json COMMAND tests_json --success) diff --git a/tests/tests_json.cpp b/tests/tests_json.cpp new file mode 100644 index 000000000..2e3d4c143 --- /dev/null +++ b/tests/tests_json.cpp @@ -0,0 +1,140 @@ +// Copyright 2021, Collabora, Ltd. +// SPDX-License-Identifier: BSL-1.0 +/*! + * @file + * @brief JSON C++ wrapper tests. + * @author Mateo de Mayo + */ + + +#include "catch/catch.hpp" +#include "util/u_json.hpp" +#include + +using std::string; +using xrt::auxiliary::util::json::JSONBuilder; +using xrt::auxiliary::util::json::JSONNode; + +TEST_CASE("u_json") +{ + // This is the json data we will be dealing with + // { + // "alpha": [1, true, 3.14, {"beta" : 4, "gamma" : 5}, {"delta" : 6}, [{"epsilon": [7], "zeta": false}]], + // "eta": "theta", + // "iota": {"kappa": [{"lambda": [5.5, [4.4, 3.3], {}, 2.2, 1, 0, {}, [-1], -2.2, -3.3, -4.4, -5.5]}]}, + // "mu" : true, + // "nu" : false, + // "xi": 42, + // "omicron": [], + // "pi": 3.141592, + // "rho": [{"sigma": [{ "tau": [{"upsilon": [[[]]]}]}]}] + // } + + JSONBuilder jb{}; + jb << "{"; + jb << "alpha" + << "[" << 1 << true << 3.14 << "{" + << "beta" << 4 << "gamma" << 5 << "}" + << "{" + << "delta" << 6 << "}" + << "[" + << "{" + << "epsilon" + << "[" << 7 << "]" + << "zeta" << false << "}" + << "]" + << "]"; + jb << "eta" + << "theta"; + jb << "iota" + << "{" + << "kappa" + << "[" + << "{" + << "lambda" + << "[" << 5.5 << "[" << 4.4 << 3.3 << "]" + << "{" + << "}" << 2.2 << 1 << 0 << "{" + << "}" + << "[" << -1 << "]" << -2.2 << -3.3 << -4.4 << -5.5 << "]" + << "}" + << "]" + << "}"; + jb << "mu" << true; + jb << "nu" << false; + jb << "xi" << 42; + jb << "omicron" + << "[" + << "]"; + jb << "pi" << 3.141592; + jb << "rho" + << "[" + << "{" + << "sigma" + << "[" + << "{" + << "tau" + << "[" + << "{" + << "upsilon" + << "[" + << "[" + << "[" + << "]" + << "]" + << "]" + << "}" + << "]" + << "}" + << "]" + << "}" + << "]"; + jb << "}"; + + JSONNode json_node = *jb.getBuiltNode(); + + SECTION("JSONBuilder builds as expected") + { + string raw_json = + "{" + "\"alpha\": [1, true, 3.14, {\"beta\" : 4, \"gamma\" : 5}, {\"delta\" : 6}, [{\"epsilon\": [7], " + "\"zeta\": " + "false}]]," + "\"eta\": \"theta\"," + "\"iota\": {\"kappa\": [{\"lambda\": [5.5, [4.4, 3.3], {}, 2.2, 1, 0, {}, [-1], -2.2, -3.3, -4.4, " + "-5.5]}]}," + "\"mu\" : true," + "\"nu\" : false," + "\"xi\": 42," + "\"omicron\": []," + "\"pi\": 3.141592," + "\"rho\": [{\"sigma\": [{ \"tau\": [{\"upsilon\": [[[]]]}]}]}]" + "}"; + JSONNode node_from_string{raw_json}; + INFO("json_node=" << json_node.toString(false)); + INFO("node_from_string=" << node_from_string.toString(false)); + CHECK(json_node.toString(false) == node_from_string.toString(false)); + } + + SECTION("Complex JSON is preserved through save and load") + { + JSONNode loaded{jb.getBuiltNode()->toString(false)}; + INFO("json_node=" << json_node.toString(false)); + INFO("loaded=" << loaded.toString(false)); + CHECK(json_node.toString(false) == loaded.toString(false)); + } + + SECTION("Access methods work") + { + CHECK(json_node["eta"].asString() == "theta"); + CHECK(json_node["eta"].asDouble(3.14) == 3.14); + CHECK(json_node["mu"].asBool(false) == true); + CHECK(json_node["alpha"][0].canBool()); + CHECK(json_node["alpha"][0].asBool(false) == true); + CHECK(json_node["alpha"][4]["delta"].asInt(-1) == 6); + CHECK(json_node["alpha"][4]["delta"].asDouble(-1) == 6.0); + CHECK(json_node["rho"][0]["sigma"][0]["tau"][0]["upsilon"].asArray().size() == 1); + CHECK(json_node["alpha"][3].asObject()["gamma"].asInt() == 5); + CHECK(json_node["iota"].hasKey("kappa")); + } +}