u/json: Implement C++ wrapper for cJSON

This commit is contained in:
Mateo de Mayo 2021-10-31 00:18:12 -03:00
parent 87a1198b0a
commit b94b7d1f52
5 changed files with 778 additions and 0 deletions

View file

@ -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

View file

@ -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',

View file

@ -0,0 +1,630 @@
// Copyright 2021, Collabora, Ltd.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief C++ wrapper for cJSON.
* @author Mateo de Mayo <mateo.demayo@collabora.com>
* @ingroup aux_util
*/
#pragma once
#include "util/u_file.h"
#include "util/u_debug.h"
#include "cjson/cJSON.h"
#include <cassert>
#include <stack>
#include <vector>
#include <map>
#include <memory>
#include <variant>
#include <fstream>
#include <sstream>
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<JSONNode>;
//! 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<string, JSONNode>
asObject(const map<string, JSONNode> &otherwise = map<string, JSONNode>()) const
{
JSON_ASSERTW(isObject(), "Invalid object: %s, defaults", toString().c_str());
if (isObject()) {
map<string, JSONNode> 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<JSONNode>
asArray(const vector<JSONNode> &otherwise = vector<JSONNode>()) const
{
JSON_ASSERTW(isArray(), "Invalid array: %s, defaults", toString().c_str());
if (isArray()) {
vector<JSONNode> 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<StackAlphabet> stack{{G::Base}};
State state{Q::Empty};
JSONNode::Ptr node = nullptr; //!< Current node we are pointing to in the tree.
using JSONValue = variant<string, const char *, int, double, bool>;
//! String representation of @p value.
static string
valueToString(const JSONValue &value)
{
string s = "JSONValue<invalid>()";
if (const string *v = get_if<string>(&value)) {
s = string{"JSONValue<string>("} + *v + ")";
} else if (const char *const *v = get_if<const char *>(&value)) {
s = string{"JSONValue<const char*>("} + *v + ")";
} else if (const int *v = get_if<int>(&value)) {
s = string{"JSONValue<int>("} + to_string(*v) + ")";
} else if (const double *v = get_if<double>(&value)) {
s = string{"JSONValue<double>("} + to_string(*v) + ")";
} else if (const bool *v = get_if<bool>(&value)) {
s = string{"JSONValue<bool>("} + 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<string>(value)) {
ret = cJSON_CreateString(get<string>(value).c_str());
} else if (holds_alternative<const char *>(value)) {
ret = cJSON_CreateString(get<const char *>(value));
} else if (holds_alternative<int>(value)) {
ret = cJSON_CreateNumber(get<int>(value));
} else if (holds_alternative<double>(value)) {
ret = cJSON_CreateNumber(get<double>(value));
} else if (holds_alternative<bool>(value)) {
ret = cJSON_CreateBool(get<bool>(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<int>(top),
static_cast<int>(state), static_cast<int>(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<JSONNode>(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<JSONNode>(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<JSONNode>(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<JSONNode>(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<G, Q> 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<JSONNode>(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<string>(value), "Non-string key not allowed");
cJSON *cjson_null = cJSON_CreateNull();
cJSON_AddItemToObject(node->cjson, get<string>(value).c_str(), cjson_null);
node = make_shared<JSONNode>(cjson_null, false, node);
state = Q::BuildObjectValue;
} else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::EndObject) {
stack.pop();
map<G, Q> 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<int>(top), static_cast<int>(state), static_cast<int>(symbol),
valueToString(value).c_str());
node = make_shared<JSONNode>();
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<string>(value) || holds_alternative<const char *>(value);
if (!is_string) {
transition(S::PushValue, value);
return *this;
}
string as_string = holds_alternative<string>(value) ? get<string>(value) : get<const char *>(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<int>(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

View file

@ -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)

140
tests/tests_json.cpp Normal file
View file

@ -0,0 +1,140 @@
// Copyright 2021, Collabora, Ltd.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief JSON C++ wrapper tests.
* @author Mateo de Mayo <mateo.demayo@collabora.com>
*/
#include "catch/catch.hpp"
#include "util/u_json.hpp"
#include <string>
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"));
}
}