Monado OpenXR Runtime
u_json.hpp
Go to the documentation of this file.
1// Copyright 2021, Collabora, Ltd.
2// SPDX-License-Identifier: BSL-1.0
3/*!
4 * @file
5 * @brief C++ wrapper for cJSON.
6 * @author Mateo de Mayo <mateo.demayo@collabora.com>
7 * @ingroup aux_util
8 */
9
10#pragma once
11
12#include "util/u_file.h"
13#include "util/u_debug.h"
14#include "cjson/cJSON.h"
15
16#include <cassert>
17#include <stack>
18#include <vector>
19#include <map>
20#include <memory>
21#include <variant>
22#include <fstream>
23#include <sstream>
24
25DEBUG_GET_ONCE_LOG_OPTION(json_log, "JSON_LOG", U_LOGGING_WARN)
26
27#define JSON_TRACE(...) U_LOG_IFL_T(debug_get_log_option_json_log(), __VA_ARGS__)
28#define JSON_DEBUG(...) U_LOG_IFL_D(debug_get_log_option_json_log(), __VA_ARGS__)
29#define JSON_INFO(...) U_LOG_IFL_I(debug_get_log_option_json_log(), __VA_ARGS__)
30#define JSON_WARN(...) U_LOG_IFL_W(debug_get_log_option_json_log(), __VA_ARGS__)
31#define JSON_ERROR(...) U_LOG_IFL_E(debug_get_log_option_json_log(), __VA_ARGS__)
32#define JSON_ASSERT(fatal, predicate, ...) \
33 do { \
34 bool p = predicate; \
35 if (!p) { \
36 JSON_ERROR(__VA_ARGS__); \
37 if (fatal) { \
38 assert(false && "Assertion failed: " #predicate); \
39 exit(EXIT_FAILURE); \
40 } \
41 } \
42 } while (false);
43
44// Fatal assertion
45#define JSON_ASSERTF(predicate, ...) JSON_ASSERT(true, predicate, __VA_ARGS__)
46#define JSON_ASSERTF_(predicate) JSON_ASSERT(true, predicate, "Assertion failed " #predicate)
47
48// Warn-only assertion
49#define JSON_ASSERTW(predicate, ...) JSON_ASSERT(false, predicate, __VA_ARGS__)
50
51namespace xrt::auxiliary::util::json {
52
53using std::get;
54using std::get_if;
55using std::holds_alternative;
56using std::make_shared;
57using std::map;
58using std::shared_ptr;
59using std::string;
60using std::to_string;
61using std::variant;
62using std::vector;
63
64class JSONBuilder;
65
66/*!
67 * @brief A JSONNode wraps a cJSON object and presents useful functions for
68 * accessing the different properties of the json structure like `operator[]`,
69 * `isType()` and `asType()` methods.
70 *
71 * The main ways a user can build a JSONNode is from a json string, from a
72 * json file with `loadFromFile` or with the @ref JSONBuilder.
73 */
75{
76private:
77 friend class JSONBuilder;
78 using Ptr = shared_ptr<JSONNode>;
79
80 //! Wrapped cJSON object
81 cJSON *cjson = nullptr;
82
83 //! Whether this node is responsible for deleting the cjson object
84 bool is_owner = false;
85
86 //! Parent of this node, used only by @ref JSONBuilder.
87 JSONNode::Ptr parent = nullptr;
88
89public:
90 // Class resource management
91
92 //! This is public so that make_shared works; do not use outside of this file.
93 JSONNode(cJSON *cjson, bool is_owner, const JSONNode::Ptr &parent)
94 : cjson(cjson), is_owner(is_owner), parent(parent)
95 {}
96
97 //! Wrap cJSON object for easy manipulation, does not take ownership
98 JSONNode(cJSON *cjson) : JSONNode(cjson, false, nullptr) {}
99
100 //! Makes a null object; `isInvalid()` on it returns true.
102
103 //! Receives a json string and constructs a wrapped cJSON object out of it.
104 JSONNode(const string &content)
105 {
106 cjson = cJSON_Parse(content.c_str());
107 if (cjson == nullptr) {
108 const int max_length = 64;
109 string msg = string(cJSON_GetErrorPtr()).substr(0, max_length);
110 JSON_ERROR("Invalid syntax right before: '%s'", msg.c_str());
111 return;
112 }
113 is_owner = true;
114 parent = nullptr;
115 }
116
117 JSONNode(JSONNode &&node)
118 {
119 *this = std::move(node);
120 }
121
122 JSONNode(const JSONNode &node)
123 {
124 *this = node;
125 };
126
127 JSONNode &
128 operator=(JSONNode &&rhs)
129 {
130 swap(*this, rhs);
131 return *this;
132 }
133
134 JSONNode &
135 operator=(const JSONNode &node)
136 {
137 is_owner = node.is_owner;
138 parent = node.parent;
139 if (node.is_owner) {
140 cjson = cJSON_Duplicate(node.cjson, true); // Deep copy
141 } else {
142 cjson = node.cjson; // Shallow copy
143 }
144
145 return *this;
146 };
147
148 ~JSONNode()
149 {
150 if (is_owner) {
151 cJSON_Delete(cjson);
152 }
153 }
154
155 friend void
156 swap(JSONNode &lhs, JSONNode &rhs) noexcept
157 {
158 using std::swap;
159 swap(lhs.cjson, rhs.cjson);
160 swap(lhs.is_owner, rhs.is_owner);
161 swap(lhs.parent, rhs.parent);
162 }
163
164 // Methods for explicit usage by the user of this class
165
166 static JSONNode
167 loadFromFile(const string &filepath)
168 {
169 std::ifstream file(filepath);
170 if (!file.is_open()) {
171 JSON_ERROR("Unable to open file %s", filepath.c_str());
172 return JSONNode{};
173 }
174
175 std::stringstream stream{};
176 stream << file.rdbuf();
177 string content = stream.str();
178
179 return JSONNode{content};
180 }
181
182 bool
183 saveToFile(const string &filepath) const
184 {
185 string contents = toString(false);
186 std::ofstream file(filepath);
187
188 if (!file.is_open()) {
189 JSON_ERROR("Unable to open file %s", filepath.c_str());
190 return false;
191 }
192
193 file << contents;
194 return true;
195 }
196
198 operator[](const string &key) const
199 {
200 const char *name = key.c_str();
201 JSON_ASSERTW(isObject(), "Trying to retrieve field '%s' from non-object %s", name, toString().c_str());
202
203 cJSON *value = cJSON_GetObjectItemCaseSensitive(cjson, name);
204 JSON_ASSERTW(value != nullptr, "Unable to retrieve field '%s' from %s", name, toString().c_str());
205
206 return JSONNode{value, false, nullptr};
207 }
208
210 operator[](int i) const
211 {
212 JSON_ASSERTW(isArray(), "Trying to retrieve index '%d' from non-array %s", i, toString().c_str());
213
214 cJSON *value = cJSON_GetArrayItem(cjson, i);
215 JSON_ASSERTW(value != nullptr, "Unable to retrieve index %d from %s", i, toString().c_str());
216
217 return JSONNode{value, false, nullptr};
218 }
219
220 // clang-format off
221 bool isObject() const { return cJSON_IsObject(cjson); }
222 bool isArray() const { return cJSON_IsArray(cjson); }
223 bool isString() const { return cJSON_IsString(cjson); }
224 bool isNumber() const { return cJSON_IsNumber(cjson); }
225 bool isInt() const { return isNumber() && cjson->valuedouble == cjson->valueint; }
226 bool isDouble() const { return isNumber(); }
227 bool isNull() const { return cJSON_IsNull(cjson); }
228 bool isBool() const { return cJSON_IsBool(cjson); }
229 bool isInvalid() const { return cjson == nullptr || cJSON_IsInvalid(cjson); }
230 bool isValid() const { return !isInvalid(); }
231
232 bool canBool() const { return isBool() || (isInt() && (cjson->valueint == 0 || cjson->valueint == 1)); }
233 // clang-format on
234
235 map<string, JSONNode>
236 asObject(const map<string, JSONNode> &otherwise = map<string, JSONNode>()) const
237 {
238 JSON_ASSERTW(isObject(), "Invalid object: %s, defaults", toString().c_str());
239 if (isObject()) {
240 map<string, JSONNode> object{};
241
242 cJSON *item = NULL;
243 cJSON_ArrayForEach(item, cjson)
244 {
245 const char *key = item->string;
246 JSON_ASSERTF(key, "Unexpected unnamed pair in json: %s", toString().c_str());
247 JSON_ASSERTW(object.count(key) == 0, "Duplicated key '%s'", key);
248 object.insert({key, JSONNode{item, false, nullptr}});
249 }
250
251 return object;
252 }
253 return otherwise;
254 }
255
256 vector<JSONNode>
257 asArray(const vector<JSONNode> &otherwise = vector<JSONNode>()) const
258 {
259 JSON_ASSERTW(isArray(), "Invalid array: %s, defaults", toString().c_str());
260 if (isArray()) {
261 vector<JSONNode> array{};
262
263 cJSON *item = NULL;
264 cJSON_ArrayForEach(item, cjson)
265 {
266 array.push_back(JSONNode{item, false, nullptr});
267 }
268
269 return array;
270 }
271 return otherwise;
272 }
273
274 string
275 asString(const string &otherwise = "") const
276 {
277 JSON_ASSERTW(isString(), "Invalid string: %s, defaults %s", toString().c_str(), otherwise.c_str());
278 return isString() ? cjson->valuestring : otherwise;
279 }
280
281 int
282 asInt(int otherwise = 0) const
283 {
284 JSON_ASSERTW(isInt(), "Invalid int: %s, defaults %d", toString().c_str(), otherwise);
285 return isInt() ? cjson->valueint : otherwise;
286 }
287
288 double
289 asDouble(double otherwise = 0.0) const
290 {
291 JSON_ASSERTW(isDouble(), "Invalid double: %s, defaults %lf", toString().c_str(), otherwise);
292 return isDouble() ? cjson->valuedouble : otherwise;
293 }
294
295 void *
296 asNull(void *otherwise = nullptr) const
297 {
298 JSON_ASSERTW(isNull(), "Invalid null: %s, defaults %p", toString().c_str(), otherwise);
299 return isNull() ? nullptr : otherwise;
300 }
301
302 bool
303 asBool(bool otherwise = false) const
304 {
305 JSON_ASSERTW(canBool(), "Invalid bool: %s, defaults %d", toString().c_str(), otherwise);
306 return isBool() ? cJSON_IsTrue(cjson) : (canBool() ? cjson->valueint : otherwise);
307 }
308
309 bool
310 hasKey(const string &key) const
311 {
312 return asObject().count(key) == 1;
313 }
314
315 string
316 toString(bool show_field = true) const
317 {
318 char *cstr = cJSON_Print(cjson);
319 string str{cstr};
320 free(cstr);
321
322 // Show the named field this comes from if any
323 if (show_field) {
324 str += "\nFrom field named: " + getName();
325 }
326
327 return str;
328 }
329
330 string
331 getName() const
332 {
333 return string(cjson->string ? cjson->string : "");
334 }
335
336 cJSON *
337 getCJSON()
338 {
339 return cjson;
340 }
341};
342
343
344/*!
345 * @brief Helper class for building cJSON trees through `operator<<`
346 *
347 * JSONBuild is implemented with a pushdown automata to keep track of the JSON
348 * construction state.
349 *
350 */
352{
353private:
354 enum class StackAlphabet
355 {
356 Base, // Unique occurrence as the base of the stack
357 Array,
358 Object
359 };
360
361 enum class State
362 {
363 Empty,
364 BuildArray,
365 BuildObjectKey,
366 BuildObjectValue,
367 Finish,
368 Invalid
369 };
370
371 enum class InputAlphabet
372 {
373 StartArray,
374 EndArray,
375 StartObject,
376 EndObject,
377 PushKey,
378 PushValue,
379 };
380
381 using G = StackAlphabet;
382 using S = InputAlphabet;
383 using Q = State;
384
385 std::stack<StackAlphabet> stack{{G::Base}};
386 State state{Q::Empty};
387 JSONNode::Ptr node = nullptr; //!< Current node we are pointing to in the tree.
388
389 using JSONValue = variant<string, const char *, int, double, bool>;
390
391 //! String representation of @p value.
392 static string
393 valueToString(const JSONValue &value)
394 {
395 string s = "JSONValue<invalid>()";
396 if (const string *v = get_if<string>(&value)) {
397 s = string{"JSONValue<string>("} + *v + ")";
398 } else if (const char *const *v = get_if<const char *>(&value)) {
399 s = string{"JSONValue<const char*>("} + *v + ")";
400 } else if (const int *v = get_if<int>(&value)) {
401 s = string{"JSONValue<int>("} + to_string(*v) + ")";
402 } else if (const double *v = get_if<double>(&value)) {
403 s = string{"JSONValue<double>("} + to_string(*v) + ")";
404 } else if (const bool *v = get_if<bool>(&value)) {
405 s = string{"JSONValue<bool>("} + to_string(*v) + ")";
406 } else {
407 JSON_ASSERTF(false, "Unsupported variant type");
408 s = "[Invalid JSONValue]";
409 }
410 return s;
411 }
412
413 //! Construct a cJSON object out of native types.
414 static cJSON *
415 makeCJSONValue(const JSONValue &value)
416 {
417 cJSON *ret = nullptr;
418 if (holds_alternative<string>(value)) {
419 ret = cJSON_CreateString(get<string>(value).c_str());
420 } else if (holds_alternative<const char *>(value)) {
421 ret = cJSON_CreateString(get<const char *>(value));
422 } else if (holds_alternative<int>(value)) {
423 ret = cJSON_CreateNumber(get<int>(value));
424 } else if (holds_alternative<double>(value)) {
425 ret = cJSON_CreateNumber(get<double>(value));
426 } else if (holds_alternative<bool>(value)) {
427 ret = cJSON_CreateBool(get<bool>(value));
428 } else {
429 JSON_ASSERTF(false, "Unexpected value");
430 }
431 return ret;
432 }
433
434 /*!
435 * @brief Receives inputs and transitions the automata from state to state.
436 *
437 * This is the table of transitions. Can be thought of as three regular FSM
438 * that get switched based on the stack's [top] value. The function is
439 * the implementation of the table.
440 *
441 * [top], [state], [symbol] -> [new-state], [stack-action]
442 * Base, Empty, PushValue -> Finish, -
443 * Base, Empty, StartObject -> BuildObjectKey, push(Object)
444 * Base, Empty, StartArray -> BuildArray, push(Array)
445 * Array, BuildArray, PushValue -> BuildArray, -
446 * Array, BuildArray, StartArray -> BuildArray, push(Array)
447 * Array, BuildArray, EndArray -> [1], pop
448 * Array, BuildArray, StartObject -> BuildObjectKey, push(Object)
449 * Object, BuildObjectKey, PushKey -> BuildObjectValue, -
450 * Object, BuildObjectKey, EndObject -> [1], pop
451 * Object, BuildObjectValue, PushValue -> BuildObjectKey, -
452 * Object, BuildObjectValue, StartObject -> BuildObjectKey, push(Object)
453 * Object, BuildObjectValue, StartArray -> BuildArray, push(Array)
454 * _, _, _, -> Invalid, -
455 *
456 * [1]: Empty or BuildArray or BuildObjectKey depending on new stack.top
457 */
458 void
459 transition(InputAlphabet symbol, const JSONValue &value)
460 {
461 StackAlphabet top = stack.top();
462 JSON_DEBUG("stacksz=%zu top=%d state=%d symbol=%d value=%s", stack.size(), static_cast<int>(top),
463 static_cast<int>(state), static_cast<int>(symbol), valueToString(value).c_str());
464
465 // This is basically an if-defined transition function for a pushdown automata
466 if (top == G::Base && state == Q::Empty && symbol == S::PushValue) {
467
468 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
469 cJSON *cjson_value = makeCJSONValue(value);
470 node = make_shared<JSONNode>(cjson_value, true, nullptr);
471
472 state = Q::Finish;
473
474 } else if (top == G::Base && state == Q::Empty && symbol == S::StartObject) {
475
476 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
477 cJSON *cjson_object = cJSON_CreateObject();
478 node = make_shared<JSONNode>(cjson_object, true, nullptr);
479
480 state = Q::BuildObjectKey;
481 stack.push(G::Object);
482
483 } else if (top == G::Base && state == Q::Empty && symbol == S::StartArray) {
484
485 JSON_ASSERTF(node == nullptr, "Failed with %s", valueToString(value).c_str());
486 cJSON *cjson_array = cJSON_CreateArray();
487 node = make_shared<JSONNode>(cjson_array, true, nullptr);
488
489 state = Q::BuildArray;
490 stack.push(G::Array);
491
492 } else if (top == G::Array && state == Q::BuildArray && symbol == S::PushValue) {
493
494 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
495 cJSON *cjson_value = makeCJSONValue(value);
496 cJSON_AddItemToArray(node->cjson, cjson_value);
497 // node = node; // The current node does not change, it is still the array
498
499 state = Q::BuildArray;
500
501 } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartArray) {
502
503 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
504 cJSON *cjson_array = cJSON_CreateArray();
505 cJSON_AddItemToArray(node->cjson, cjson_array);
506 node = make_shared<JSONNode>(cjson_array, false, node);
507
508 state = Q::BuildArray;
509 stack.push(G::Array);
510
511 } else if (top == G::Array && state == Q::BuildArray && symbol == S::EndArray) {
512
513 stack.pop();
514 map<G, Q> m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}};
515 state = m[stack.top()];
516
517 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
518 if (node->parent) {
519 node = node->parent;
520 } else {
521 JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without");
522 }
523
524 } else if (top == G::Array && state == Q::BuildArray && symbol == S::StartObject) {
525
526 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
527 cJSON *cjson_object = cJSON_CreateObject();
528 cJSON_AddItemToArray(node->cjson, cjson_object);
529 node = make_shared<JSONNode>(cjson_object, false, node);
530
531 state = Q::BuildObjectKey;
532 stack.push(G::Object);
533
534 } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::PushKey) {
535
536 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
537 JSON_ASSERTF(holds_alternative<string>(value), "Non-string key not allowed");
538 cJSON *cjson_null = cJSON_CreateNull();
539 cJSON_AddItemToObject(node->cjson, get<string>(value).c_str(), cjson_null);
540 node = make_shared<JSONNode>(cjson_null, false, node);
541
542 state = Q::BuildObjectValue;
543
544 } else if (top == G::Object && state == Q::BuildObjectKey && symbol == S::EndObject) {
545
546 stack.pop();
547 map<G, Q> m{{G::Object, Q::BuildObjectKey}, {G::Array, Q::BuildArray}, {G::Base, Q::Finish}};
548 state = m[stack.top()];
549
550 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
551 if (node->parent) {
552 node = node->parent;
553 } else {
554 JSON_ASSERTF(stack.top() == G::Base, "Unexpected non-root node without")
555 }
556
557 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::PushValue) {
558
559 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
560 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
561 cJSON *cjson_value = makeCJSONValue(value);
562 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_value);
563 node->cjson = cjson_value;
564 node = node->parent;
565
566 state = Q::BuildObjectKey;
567
568 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartObject) {
569
570 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
571 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
572 cJSON *cjson_object = cJSON_CreateObject();
573 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_object);
574 node->cjson = cjson_object;
575
576 state = Q::BuildObjectKey;
577 stack.push(G::Object);
578
579 } else if (top == G::Object && state == Q::BuildObjectValue && symbol == S::StartArray) {
580
581 JSON_ASSERTF(node->cjson != nullptr, "Failed with %s", valueToString(value).c_str());
582 JSON_ASSERTF(cJSON_IsNull(node->cjson), "Partial pair value is not null");
583 cJSON *cjson_array = cJSON_CreateArray();
584 cJSON_ReplaceItemInObject(node->parent->cjson, node->cjson->string, cjson_array);
585 node->cjson = cjson_array;
586
587 state = Q::BuildArray;
588 stack.push(G::Array);
589
590 } else {
591
592 JSON_ASSERTF(false, "Invalid construction transition: top=%d state=%d symbol=%d value=%s",
593 static_cast<int>(top), static_cast<int>(state), static_cast<int>(symbol),
594 valueToString(value).c_str());
595 node = make_shared<JSONNode>();
596
597 state = Q::Invalid;
598 }
599
600 JSON_DEBUG("After transition: node=%p parent=%p\n", (void *)node.get(),
601 (void *)(node ? node->parent.get() : nullptr));
602 }
603
604public:
605 JSONBuilder() {}
606
607 //! Receives "[", "]", "{", "}", or any of string, const char*, double, int,
608 //! bool as inputs. Updates the JSONBuilder state with it, after finishing the
609 //! JSON tree, obtain the result with @ref getBuiltNode.
611 operator<<(const JSONValue &value)
612 {
613 bool is_string = holds_alternative<string>(value) || holds_alternative<const char *>(value);
614 if (!is_string) {
615 transition(S::PushValue, value);
616 return *this;
617 }
618
619 string as_string = holds_alternative<string>(value) ? get<string>(value) : get<const char *>(value);
620 if (as_string == "[") {
621 transition(S::StartArray, as_string);
622 } else if (as_string == "]") {
623 transition(S::EndArray, as_string);
624 } else if (as_string == "{") {
625 transition(S::StartObject, as_string);
626 } else if (as_string == "}") {
627 transition(S::EndObject, as_string);
628 } else if (state == Q::BuildObjectKey) {
629 transition(S::PushKey, as_string);
630 } else if (state == Q::BuildObjectValue) {
631 transition(S::PushValue, as_string);
632 } else {
633 JSON_ASSERTF(false, "Invalid state=%d value=%s", static_cast<int>(state), as_string.c_str());
634 }
635 return *this;
636 }
637
638 //! Gets the built JSONNode or crash if the construction has not finished
639 JSONNode::Ptr
641 {
642 JSON_ASSERTF(state == Q::Finish, "Trying to getBuiltNode but the construction has not ended");
643 return node;
644 }
645};
646
647} // namespace xrt::auxiliary::util::json
Helper class for building cJSON trees through operator<<
Definition: u_json.hpp:352
JSONBuilder & operator<<(const JSONValue &value)
Receives "[", "]", "{", "}", or any of string, const char*, double, int, bool as inputs.
Definition: u_json.hpp:611
JSONNode::Ptr getBuiltNode()
Gets the built JSONNode or crash if the construction has not finished.
Definition: u_json.hpp:640
A JSONNode wraps a cJSON object and presents useful functions for accessing the different properties ...
Definition: u_json.hpp:75
JSONNode(cJSON *cjson)
Wrap cJSON object for easy manipulation, does not take ownership.
Definition: u_json.hpp:98
JSONNode()
Makes a null object; isInvalid() on it returns true.
Definition: u_json.hpp:101
JSONNode(cJSON *cjson, bool is_owner, const JSONNode::Ptr &parent)
This is public so that make_shared works; do not use outside of this file.
Definition: u_json.hpp:93
JSONNode(const string &content)
Receives a json string and constructs a wrapped cJSON object out of it.
Definition: u_json.hpp:104
@ U_LOGGING_WARN
Warning messages: indicating a potential problem.
Definition: u_logging.h:48
Small debug helpers.
Very simple file opening functions.
static const cJSON * get(const cJSON *json, const char *f)
Less typing.
Definition: u_json.c:36