From 1ad869ceeb2423927d8726d001808adc22fe887d Mon Sep 17 00:00:00 2001 From: Evgenii Zhemchugov Date: Mon, 2 Jun 2025 15:14:18 +0100 Subject: [PATCH 1/4] Json: implement json_scan, json_decode Implement full-blown JSON serialization and validation functions. Support extensions for user classes. --- src/Store/BStore.cpp | 2 +- src/Store/BStore.h | 2 +- src/Store/Json.cpp | 376 +++++++++++++++++++++++++++++++++++++++++- src/Store/Json.h | 379 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 696 insertions(+), 63 deletions(-) diff --git a/src/Store/BStore.cpp b/src/Store/BStore.cpp index 5d52515..7ce91cf 100644 --- a/src/Store/BStore.cpp +++ b/src/Store/BStore.cpp @@ -957,7 +957,7 @@ BStore::~BStore(){ } namespace ToolFramework { - bool json_encode(std::ostream& output, const BStore& store) { + bool json_encode_r(std::ostream& output, const BStore& store, adl_tag) { return store.JsonEncode(output); } } diff --git a/src/Store/BStore.h b/src/Store/BStore.h index 34b9d1d..b45e540 100644 --- a/src/Store/BStore.h +++ b/src/Store/BStore.h @@ -258,7 +258,7 @@ namespace ToolFramework{ }; - bool json_encode(std::ostream&, const BStore&); + bool json_encode_r(std::ostream&, const BStore&, adl_tag); } #endif diff --git a/src/Store/Json.cpp b/src/Store/Json.cpp index b44f19b..c1b2178 100644 --- a/src/Store/Json.cpp +++ b/src/Store/Json.cpp @@ -1,19 +1,381 @@ +#include + +#include +#include + #include namespace ToolFramework { -bool json_encode(std::ostream& output, const char* datum) { +static bool json_encode( + std::ostream& output, + const char* begin, + const char* end +) { output << '"'; - while (*datum) { - if (*datum == '"' || *datum == '\\') output << '\\'; - output << *datum; - }; + for (auto c = begin; c < end; ++c) + switch (*c) { + case '\a': + output << "\\a"; + break; + case '\b': + output << "\\b"; + break; + case '\f': + output << "\\f"; + break; + case '\n': + output << "\\n"; + break; + case '\r': + output << "\\r"; + break; + case '\t': + output << "\\t"; + break; + case '\\': + case '"': + output << '\\' << *c; + break; + default: + { + uint8_t byte = static_cast(*c); + if (byte < 0x20 || byte > 0x7F) { + uint16_t codepoint = byte; + if (++c != end) + codepoint = codepoint << 8 | static_cast(*c); + output + << "\\u" + << std::setw(4) + << std::setfill('0') + << std::hex + << codepoint + << std::dec; + } else + output << byte; + }; + }; output << '"'; return true; } -bool json_encode(std::ostream& output, const std::string& datum) { - return json_encode(output, datum.c_str()); +bool json_encode( + std::ostream& output, + std::string::const_iterator begin, + std::string::const_iterator end +) { + return json_encode(output, &*begin, &*end); +} + +bool json_encode_r(std::ostream& output, const std::string& datum, adl_tag) { + return json_encode(output, datum.begin(), datum.end()); +} + +bool json_encode_r(std::ostream& output, const char* datum, adl_tag) { + return json_encode(output, datum, datum + strlen(datum)); +} + +const char* json_scan_whitespace(const char* input) { + if (!input) return nullptr; + while (isspace(*input)) ++input; + return input; +} + +const char* json_scan_token(const char* input, char token) { + input = json_scan_whitespace(input); + return input && *input == token ? input + 1 : nullptr; +} + +const char* json_scan_token(const char* input, const char* token) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + while (*token) if (*input++ != *token++) return nullptr; + return isalnum(*input) ? nullptr : input; +} + +const char* json_scan_number(const char* input) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + + if (*input == '-' || *input == '+') ++input; + while (isdigit(*input)) ++input; + if (*input == '.') { + ++input; + while (isdigit(*input)) ++input; + }; + if (tolower(*input) == 'e') { + ++input; + if (*input == '-' || *input == '+') ++input; + while (isdigit(*input)) ++input; + }; + + return isalnum(*input) ? nullptr : input; +} + +const char* json_scan_string(const char* input) { + input = json_scan_token(input, '"'); + if (!input) return nullptr; + + bool escaped = false; + for (; *input; ++input) + if (escaped) + escaped = false; + else + switch (*input) { + case '\\': + escaped = true; + break; + case '"': + return input + 1; + break; + default: + break; + }; + + return nullptr; +} + +static const char* json_scan_list( + const char* input, + char start, + char end, + const char* (*scan_item)(const char*) +) { + bool comma = false; + input = json_scan_token(input, start); + while (input) { + input = json_scan_whitespace(input); + if (*input == end) return input + 1; + + if (comma) { + if (*input++ != ',') return nullptr; + } else + comma = true; + + input = scan_item(input); + }; + return nullptr; +} + +const char* json_scan_object(const char* input) { + return json_scan_list( + input, '{', '}', + [](const char* i) -> const char* { + i = json_scan_string(i); + i = json_scan_token(i, ':'); + return json_scan(i); + } + ); +} + +const char* json_scan_array(const char* input) { + return json_scan_list(input, '[', ']', json_scan); +} + +const char* json_scan(const char* input) { + input = json_scan_whitespace(input); + if (!input) return nullptr; + switch (*input) { + case 'f': + return json_scan_token(input, "false"); + case 't': + return json_scan_token(input, "true"); + case 'n': + return json_scan_token(input, "null"); + case '-': + case '+': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return json_scan_number(input); + case '"': + return json_scan_string(input); + case '{': + return json_scan_object(input); + case '[': + return json_scan_array(input); + default: + return nullptr; + }; +} + +bool json_decode_r(const char*& input, bool& value, adl_tag) { + const char* i = json_scan_whitespace(input); + if (!i) return false; + + switch (*i) { + case 'f': + i = json_scan_token(i, "false"); + if (!i) return false; + value = false; + break; + case 't': + i = json_scan_token(i, "true"); + if (!i) return false; + value = true; + break; + case '0': + if (isalnum(*++i)) return false; + value = false; + break; + case '1': + if (isalnum(*++i)) return false; + value = true; + break; + default: + return false; + }; + + input = i; + return true; +} + +namespace json_internal { + +bool json_decode_string( + const char*& input, + std::string& value, + bool keep_quotes +) { + const char* i = json_scan_token(input, '"'); + if (!i) return false; + + std::stringstream ss; + if (keep_quotes) ss << '"'; + + bool escaped = false; + for (; *i; ++i) { + if (escaped) { + escaped = false; + switch (*i) { + case '\\': + ss << '\\'; + break; + case 'a': + ss << '\a'; + break; + case 'b': + ss << '\b'; + break; + case 'f': + ss << '\f'; + break; + case 'n': + ss << '\n'; + break; + case 'r': + ss << '\r'; + break; + case 't': + ss << '\t'; + break; + case 'u': + { + uint16_t codepoint = 0; + for (int b = 0; b < 2; ++b) { + for (int d = 0; d < 2; ++d) { + char digit = *++i; + if (digit >= '0' && digit <= '9') + digit -= '0'; + else if (digit >= 'a' && digit <= 'f') + digit = digit - 'a' + 10; + else if (digit >= 'A' && digit <= 'F') + digit = digit - 'A' + 10; + else + return false; // TODO: invalid string + codepoint = codepoint << 4 | digit; + }; + }; + if (codepoint > 0xFF) ss << static_cast(codepoint >> 8); + ss << static_cast(codepoint & 0xFF); + }; + break; + default: + // TODO: invalid escape sequence. Store anyway? + ss << *i; + }; + } else { + switch (*i) { + case '\\': + escaped = true; + continue; + case '"': + if (keep_quotes) ss << '"'; + input = ++i; + value = ss.str(); + return true; + default: + ss << *i; + }; + }; + }; + + return false; +} + +} + +bool json_decode_r(const char*& input, std::string& value, adl_tag) { + return json_internal::json_decode_string(input, value, false); +} + +static bool json_decode_list( + const char*& input, + char start, + char end, + const std::function decode_item +) { + const char* i = json_scan_token(input, start); + if (!i) return false; + + bool comma = false; + while (true) { + i = json_scan_whitespace(i); + if (*i == end) break; + + if (comma) { + if (*i++ != ',') return false; + } else + comma = true; + + if (!decode_item(i)) return false; + }; + + input = i + 1; + return true; +} + +bool json_decode_array( + const char*& input, + const std::function& decode_item +) { + return json_decode_list(input, '[', ']', decode_item); +} + +bool json_decode_object( + const char*& input, + const std::function& decode_value +) { + return json_decode_list( + input, '{', '}', + [&](const char*& input_) -> bool { + const char* i = input_; + std::string key; + if (!json_decode_r(i, key, adl_tag {})) return false; + i = json_scan_token(i, ':'); + if (!i) return false; + if (!decode_value(i, std::move(key))) return false; + input_ = i; + return true; + } + ); } } diff --git a/src/Store/Json.h b/src/Store/Json.h index 2cb23c1..e2080a6 100644 --- a/src/Store/Json.h +++ b/src/Store/Json.h @@ -1,82 +1,173 @@ #ifndef TOOLFRAMEWORK_JSON_H #define TOOLFRAMEWORK_JSON_H +#include #include #include #include #include #include +#include #include + +/** + This file provides functions working with JSON. + + Three families of functions are provided: json_encode for JSON + serialization, json_scan for JSON validation, and json_decode for JSON + parsing. +*/ + namespace ToolFramework { +/** + JSON serialization functions + + json_encode comes in two flavours: json_encode(std::ostream&, const T&) + serializes a value into a stream, json_encode(std::string&, const T&) + serializes a value into a string. The latter calls the former, and the + former calls json_encode_r to encode specific objects. Provide overloads of + json_encode_r function or template to extend for custom classes. + + json_encode returns false if encoding has failed; for the function working + with a stream it can only happen in user extensions, functions in this file + always return true. json_encode(std::string&, const T&) checks the stream + flags. +*/ + +template bool json_encode(std::ostream& output, const T& datum); +template bool json_encode(std::string& output, const T& datum); + +// Encode a part of a string as a string +bool json_encode( + std::ostream& output, + std::string::const_iterator begin, + std::string::const_iterator end +); + +// A helper function to write fixed-size objects +// Example: call `json_encode_object(output, "x", 42, "a", false)` +// to produce `{"x":42,"a":false}` +template +bool json_encode_object(std::ostream& output, Fields... fields); + +/* + JSON decoder function + Use this if you know the structure of JSON data in advance + + Returns false if the JSON string is invalid. + If the object was decoded successfully, input is set one character past it, + otherwise it is left unchanged. +*/ +template bool json_decode(const char*& input, T& value); + +// Expects an array in input, calls decode_item(item) on each array item +bool json_decode_array( + const char*& input, + const std::function& decode_item +); + +// Expects an object in input, calls decode_value(key, value) on each key-value pair +bool json_decode_object( + const char*& input, + const std::function& + decode_value +); + +// A class to trigger argument-dependent lookup (ADL) in json_encode_r and +// json_decode_r functions. It is required for the compiler to find overloads +// declared after this file. See +// https://akrzemi1.wordpress.com/2016/01/16/a-customizable-framework/ +// for caveats. +struct adl_tag {}; + + +/** + JSON scanner functions + + Scanner functions find the end of an object without further interpretation. + + Each function returns nullptr if the JSON string is invalid. + It is okay to pass nullptr as input. +*/ +const char* json_scan_whitespace(const char* input); +const char* json_scan_token(const char* input, char token); +const char* json_scan_token(const char* input, const char* token); +const char* json_scan_number(const char* input); +const char* json_scan_string(const char* input); +const char* json_scan_object(const char* input); +const char* json_scan_array(const char* input); +const char* json_scan(const char* input); // generic + + +// json_encode_r implements encoding of specific objects. Add overloads to this +// function to support custom classes. + +template +typename std::enable_if::value, bool>::type +json_encode_r(std::ostream& output, T datum, adl_tag); + +bool json_encode_r(std::ostream& output, const std::string& datum, adl_tag); + +bool json_encode_r(std::ostream& output, const char* datum, adl_tag); + +template +bool json_encode_r(std::ostream& output, const T* data, size_t size, adl_tag); + template -bool json_encode(std::ostream&, const std::array&); +bool json_encode_r(std::ostream&, const std::array&, adl_tag); template -bool json_encode(std::ostream&, const std::vector&); +bool json_encode_r(std::ostream&, const std::vector&, adl_tag); template -bool json_encode(std::ostream&, const std::map&); +bool json_encode_r(std::ostream&, const std::map&, adl_tag); + +// json_decode_r implements decoding of specific objects. Add overloads to this +// function to support custom classes. + +bool json_decode_r(const char*& input, bool& value, adl_tag); template -typename std::enable_if::value, bool>::type -json_encode(std::ostream& output, T datum) { - output << datum; - return true; -} +typename std::enable_if< + std::is_integral::value && std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag); -bool json_encode(std::ostream& output, const char* datum); -bool json_encode(std::ostream& output, const std::string& datum); +template +typename std::enable_if< + std::is_integral::value && !std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value); template -bool json_encode(std::ostream& output, const T* data, size_t size) { - output << '['; - bool comma = false; - for (size_t i = 0; i < size; ++i) { - if (comma) - output << ','; - comma = true; - if (!json_encode(output, data[i])) return false; - }; - output << ']'; - return true; -} +typename std::enable_if::value, bool>::type +json_decode_r(const char*& input, T& value, adl_tag); -template -bool json_encode(std::ostream& output, const std::array& array) { - return json_encode(output, array.data(), array.size()); -} +bool json_decode_r(const char*& input, std::string& value, adl_tag); template -bool json_encode(std::ostream& output, const std::vector& vector) { - return json_encode(output, vector.data(), vector.size()); -} +bool json_decode_r(const char*& input, std::vector& value, adl_tag); template -bool json_encode(std::ostream& output, const std::map& data) { - output << '{'; - bool comma = false; - for (auto& datum : data) { - if (comma) - output << ','; - else - comma = true; - if (!json_encode(output, datum.first)) return false; - output << ':'; - if (!json_encode(output, datum.second)) return false; - }; - output << '}'; - return true; -} +bool json_decode_r( + const char*& input, + std::map& value, + adl_tag +); template -bool json_encode(std::string& output, T data) { - std::stringstream ss; - if (!json_encode(ss, data)) return false; - output = ss.str(); - return true; -} +bool json_decode_r( + const char*& input, + std::unordered_map& value, + adl_tag +); + + +// Implementation namespace json_internal { @@ -95,7 +186,7 @@ namespace json_internal { if (comma) output << ','; comma = true; output << '"' << name << '"' << ':'; - if (!json_encode(output, slot)) return false; + if (!json_encode_r(output, slot, adl_tag {})) return false; return json_encode_object_slots(output, comma, rest...); } @@ -110,12 +201,27 @@ namespace json_internal { return json_encode_object_slots(output, comma, name.c_str(), slot, rest...); } + bool json_decode_string( + const char*& input, + std::string& value, + bool keep_quotes + ); + } // json_internal +template +bool json_encode(std::ostream& output, const T& datum) { + return json_encode_r(output, datum, adl_tag {}); +} + +template +bool json_encode(std::string& output, const T& datum) { + std::stringstream ss; + if (!json_encode(ss, datum) || !ss) return false; + output = ss.str(); + return true; +} -// A helper function to write fixed-size objects -// Example: call `json_encode_object(output, "x", 42, "a", false)` -// to produce `{"x":42,"a":false}` template bool json_encode_object(std::ostream& output, Args... args) { output << '{'; @@ -126,6 +232,171 @@ bool json_encode_object(std::ostream& output, Args... args) { return true; } +template +bool json_decode(const char*& input, T& value) { + return json_decode_r(input, value, adl_tag {}); +} + +template +typename std::enable_if::value, bool>::type +json_encode_r(std::ostream& output, T datum, adl_tag) { + output << datum; + return true; +} + +template +bool json_encode_r(std::ostream& output, const T* data, size_t size, adl_tag tag) { + output << '['; + bool comma = false; + for (size_t i = 0; i < size; ++i) { + if (comma) + output << ','; + comma = true; + if (!json_encode_r(output, data[i], tag)) return false; + }; + output << ']'; + return true; +} + +template +bool json_encode_r( + std::ostream& output, + const std::array& array, + adl_tag tag +) { + return json_encode_r(output, array.data(), array.size(), tag); +} + +template +bool json_encode_r( + std::ostream& output, + const std::vector& vector, + adl_tag tag +) { + return json_encode_r(output, vector.data(), vector.size(), tag); +} + +template +bool json_encode_r_map(std::ostream& output, const Map& data, adl_tag tag) { + output << '{'; + bool comma = false; + for (auto& datum : data) { + if (comma) + output << ','; + else + comma = true; + if (!json_encode_r(output, datum.first, tag)) return false; + output << ':'; + if (!json_encode_r(output, datum.second, tag)) return false; + }; + output << '}'; + return true; +} + +template +bool json_encode_r( + std::ostream& output, + const std::map& data, + adl_tag tag +) { + return json_encode_r_map, T>(output, data, tag); +} + +template +bool json_encode_r( + std::ostream& output, + const std::unordered_map& data, + adl_tag tag +) { + return json_encode_r_map, T>( + output, data, tag + ); +} + +template +typename std::enable_if< + std::is_integral::value && std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtol(input, &i, 10); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +typename std::enable_if< + std::is_integral::value && !std::is_signed::value, + bool +>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtoul(input, &i, 10); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +typename std::enable_if::value, bool>::type +json_decode_r(const char*& input, T& value, adl_tag) { + char* i; + value = strtod(input, &i); + if (isalnum(*i)) return false; + input = i; + return true; +} + +template +bool json_decode_r(const char*& input, std::vector& value, adl_tag tag) { + value.clear(); + return json_decode_array( + input, + [&](const char*& i) -> bool { + T item; + if (!json_decode_r(i, item, tag)) return false; + value.push_back(std::move(item)); + return true; + } + ); +} + +template +bool json_decode_r_map(const char*& input, Map& value, adl_tag tag) { + value.clear(); + return json_decode_object( + input, + [&](const char*& i, std::string key) -> bool { + T item; + if (!json_decode_r(i, item, tag)) return false; + value[std::move(key)] = std::move(item); + return true; + } + ); +} + +template +bool json_decode_r( + const char*& input, + std::map& value, + adl_tag tag +) { + return json_decode_r_map, T>(input, value, tag); +} + +template +bool json_decode_r( + const char*& input, + std::unordered_map& value, + adl_tag tag +) { + return json_decode_r_map, T>( + input, value, tag + ); +} + } // ToolFramework #endif From e4682b9dc2783cd9116691c3fa78a6c470ba0d70 Mon Sep 17 00:00:00 2001 From: Evgenii Zhemchugov Date: Mon, 2 Jun 2025 15:18:16 +0100 Subject: [PATCH 2/4] Store: store structured data as JSON objects * Use JSON encoding for strings with special characters, vectors, and maps to ensure that deserialization returns what has been serialized. * Make JsonParser work for JSON objects containing strings with special characters. * JsonParser now returns a bool indicating whether the parsing was successful. * Replace `template void operator>>(T&)` with `void operator>>(std::string&)` since it only worked for strings anyways. * Make `operator>>` produce numbers as JSON numbers. * Rewrite UnitTests/StoreTest to thoroughly test Store implementation. The type of the stored object is encoded by the first character in the stored value: `"` for strings, `[` for arrays, and `{` for objects. A matching character is expected at the end of the value. Any other character at the beginning of the value means that the object was encoded with a custom `operator<<(std::ostream&, T)`, and it is expected that `operator>>(std::istream&, T&)` can be used to decode the object completely. If such encoding produces a string beginning with one of the characters above, it is wrapped into a pair of '"'. These conversions are handled transparently to the user. --- UnitTests/StoreTest.cpp | 628 +++++++++++++++++++++++++++++++++------- src/Store/Store.cpp | 208 ++++++------- src/Store/Store.h | 345 ++++++++++++---------- 3 files changed, 793 insertions(+), 388 deletions(-) diff --git a/UnitTests/StoreTest.cpp b/UnitTests/StoreTest.cpp index 3fca299..a1bca34 100644 --- a/UnitTests/StoreTest.cpp +++ b/UnitTests/StoreTest.cpp @@ -1,109 +1,525 @@ #include +#include + +#include + #include -using namespace ToolFramework; - -int test_counter=0; - -template int Test(T &a, T &b, std::string message=""){ -test_counter++; - -if(a!=b){ - std::cout<<"ERROR "<("j"); - pass= pass && (store.Get("k",k2)); - pass= pass && (store.Get("l",l2)); - pass= pass && (store.Get("m",m2)); - pass= pass && (store.Get("n",n2)); - - -bool tmp=true; -ret+=Test(pass,tmp, "Get Fail"); - -ret+=Test(a,a2); -ret+=Test(b,b2); -ret+=Test(c,c2); -ret+=Test(d,d2); -ret+=Test(e,e2); -ret+=Test(f,f2); -ret+=Test(g,g2); -ret+=Test(h,h2); -ret+=Test(i,i2); -ret+=Test(j,j2); -ret+=Test(h,k2); -ret+=Test(b,l2); -ret+=Test(d,m2); -ret+=Test(f,n2); - -store.Print(); - - -return ret; - -} +using ToolFramework::Store; + +class Tester { + public: + bool success = true; + + void operator()(bool result) { + success = success && result; + }; + + operator bool() { return success; }; +}; + +template <> +struct std::equal_to { + bool operator()(const Store& a, const Store& b) { + auto ia = a.begin(); + auto ib = b.begin(); + while (ia != a.end()) if (ib == b.end() || *ia++ != *ib++) return false; + return true; + }; +}; + +// Compare two JSON strings neglecting whitespace +// Skips whitespace and compares further characters. May produce false equals +// when comparing strings within strings differing in whitespace, e.g., +// "{\"a\":\"q w\"}" "{\"a\":\"q w\"}", but we don't expect those. +static bool json_eq(const char* a, const char* b) { + while (*a && *b) { + if (*a != *b) { + while (isspace(*a)) ++a; + while (isspace(*b)) ++b; + if (*a != *b) return false; + }; + ++a; + ++b; + }; + while (isspace(*a)) ++a; + while (isspace(*b)) ++b; + return *a == *b; +}; + +template +std::ostream& operator<<(std::ostream&, const std::map&); + +template +std::ostream& operator<<(std::ostream& stream, const std::vector& vector) { + stream << "[ "; + bool comma = false; + for (auto& x : vector) { + if (comma) stream << ", "; + comma = true; + stream << x; + }; + return stream << " ]"; +}; + +template +std::ostream& operator<<(std::ostream& stream, const std::map& map) { + stream << "{ "; + bool comma = false; + for (auto& kv : map) { + if (comma) stream << ", "; + comma = true; + stream << kv.first << " => " << kv.second; + }; + return stream << " }"; +}; + +// Custom user class +struct User { + std::string field; + + User() {}; + User(const char* f): field(f) {}; + virtual ~User() {}; + + bool operator==(const User& u) const { + return field == u.field; + }; +}; + +static void print(std::ostream& stream, const User& user) { + stream << "user " << user.field; +}; + +// Custom user class with implemented json encode/decode +struct User1: public User { + User1() {}; + User1(const char* f): User(f) {}; +}; + +namespace ToolFramework { + static bool json_encode_r(std::ostream& stream, const User1& user, adl_tag tag) { + return json_encode_r(stream, user.field, tag); + }; + + static bool json_decode_r(const char*& input, User1& user, adl_tag tag) { + const char* i = input; + if (!json_decode_r(i, user.field, tag)) return false; + input = i; + return true; + }; +}; + +// Custom user class with implemented input/output operators +struct User2: public User { + User2() {}; + User2(const char* f): User(f) {}; +}; + +static std::ostream& operator<<(std::ostream& output, const User2& user) { + return output << user.field; +}; + +static std::istream& operator>>(std::istream& input, User2& user) { + return input >> user.field; +}; + +// Custom user class with implemented both json encode/decode and input/output +// operators +struct User3: public User { + User3() {}; + User3(const char* f): User(f) {}; +}; + +namespace ToolFramework { + static bool json_encode_r( + std::ostream& stream, const User3& user, adl_tag tag + ) { + return json_encode_r(stream, user.field, tag); + }; + + static bool json_decode_r( + const char*& input, User3& user, adl_tag tag + ) { + const char* i = input; + if (!json_decode_r(i, user.field, tag)) return false; + input = i; + return true; + }; +}; + +static std::ostream& operator<<(std::ostream& output, const User3& user) { + return output << user.field; +}; + +static std::istream& operator>>(std::istream& input, User3& user) { + return input >> user.field; +}; + +// Custom user class with json encode/decode implemented through inheritance +struct User4: public User1 { + User4() {}; + User4(const char* f): User1(f) {}; +}; + +// Custom user class with both json encode/decode and input/output operators +// implemented through inheritance +struct User5: public User3 { + User5() {}; + User5(const char* f): User3(f) {}; +}; + +// Custom user class with json encode/decode implemented through inheritance +// and custom input/output operators +struct User6: public User1 { + User6() {}; + User6(const char* f): User1(f) {}; +}; + +static std::ostream& operator<<(std::ostream& output, const User6& user) { + return output << user.field; +}; + +static std::istream& operator>>(std::istream& input, User6& user) { + return input >> user.field; +}; + +template > +static bool test_var( + const char* key, + const T& value, + const char* json = nullptr, + Eq eq = std::equal_to(), + const std::function& printer + = [](std::ostream& stream, const T& value) { stream << value; } +) { + auto log = [&]() -> std::ostream& { + std::cout + << "test_var<" + << typeid(T).name() + << "> `" + << key + << '\''; + if (printer) { + std::cout << " `"; + printer(std::cout, value); + std::cout << '\''; + }; + return std::cout << ": "; + }; + + Store store; + store.Set(key, value); + + T value2; + store.Get(key, value2); + if (!eq(value, value2)) { + log() << "Get returned `"; + printer(std::cout, value2); + std::cout << "'. Store contents:\n"; + store.Print(); + return false; + }; + + std::string json2; + store >> json2; + if (json && !json_eq(json, json2.c_str())) { + log() + << "JSON mismatch: expected `" + << json + << "', got `" + << json2 + << "'. Store contents:\n"; + store.Print(); + return false; + }; + + Store store2; + if (!store2.JsonParser(json2)) { + log() << "JsonParser failed on `" << json2 << "'\n"; + return false; + }; + + store2.Get(key, value2); + if (!eq(value, value2)) { + log() << "Get after deserialization returned `"; + printer(std::cout, value2); + std::cout << "'\n"; + return false; + }; + + return true; +}; + +static bool test_json( + const char* json, + bool valid = true, + const char* expected = nullptr +) { + if (!expected) expected = json; + + auto log = [&]() -> std::ostream& { + return std::cout << "test_json `" << json << "': "; + }; + + Store store; + if (store.JsonParser(json) != valid) { + log() + << "JsonParser " + << (valid ? "failed" : "unexpectedly succeeded") + << '\n'; + return false; + }; + + std::string json2; + store >> json2; + + if (!json_eq(expected, json2.c_str())) { + log() << "JSON mismatch: got `" << json2 << "'\n"; + return false; + }; + + return true; +}; + +int main(int argc, char** argv) { + Tester t; + + t(test_var("a", 1, "{\"a\":1}")); + t(test_var("b", static_cast(2), "{\"b\":2}")); + t(test_var("c", 3L, "{\"c\":3}")); + t(test_var("d", 4.4f, "{\"d\":4.4}")); + t(test_var("e", 5.5, "{\"e\":5.5}")); + t(test_var("f", true, "{\"f\":1}")); + t(test_var("g", 'h', "{\"g\":\"h\"}")); + t(test_var("h", "hello world", "{\"h\":\"hello world\"}")); + + // TODO: make it work with const char* + t(test_var("i", "q \" w", "{\"i\":\"q \\\" w\"}")); + + { + Store s; + s.Set("a", "q } w"); + t(test_var("j", s, "{\"j\":{\"a\":\"q } w\"}}")); + }; + + t(test_var("k", "{\"a\":{\"q } w\"}}", "{\"k\":\"{\\\"a\\\":{\\\"q } w\\\"}}\"}")); + t(test_var("l", std::vector { 42, 11 }, "{\"l\":[42,11]}")); + + t( + test_var( + "m", + std::vector { -2.5e-10, -0.1, 0.1, 2.5e10 }, + "{\"m\":[-2.5e-10,-0.1,0.1,2.5e+10]}" + ) + ); + + { + Store s; + s.Set("x", std::vector { -5, 5 }); + s.Set("y", std::vector { 0, 1.333 }); + t(test_var("n", s, "{\"n\":{\"x\":[-5,5],\"y\":[0,1.333]}}")); + }; + + t( + test_var( + "o", + std::vector { "qwe , asd", "q \"}] w" }, + "{\"o\":[\"qwe , asd\",\"q \\\"}] w\"]}" + ) + ); + + t( + test_var( + "p", + std::vector> { { 1, 2 }, { -1, -2 } }, + "{\"p\":[[1,2],[-1,-2]]}" + ) + ); + + t( + test_var( + "q", + std::map { + { "a", "qwe" }, + { "q \" w", "x } z" } + }, + "{\"q\":{\"a\":\"qwe\",\"q \\\" w\":\"x } z\"}}" + ) + ); + + t( + test_var( + "r", + std::vector>> { + { + { "a", { 1, 2 } }, + { "b", { -1, -2 } } + }, + { + { "q \" w", { 0, 42 } }, + { "x ] z", { 15, 11 } } + } + }, + "{\"r\":[{\"a\":[1,2],\"b\":[-1,-2]},{\"q \\\" w\":[0,42],\"x ] z\":[15,11]}]}" + ) + ); + + t( + test_var( + "user1", + User1("user1"), + "{\"user1\":\"user1\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_var( + "user2", + User2("user2"), + "{\"user2\":\"user2\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_var( + "user3", + User3("user3"), + "{\"user3\":\"user3\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_var( + "user4", + User4("user4"), + "{\"user4\":\"user4\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_var( + "user5", + User5("user5"), + "{\"user5\":\"user5\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_var( + "user6", + User6("user6"), + "{\"user6\":\"user6\"}", + std::equal_to(), + std::function(print) + ) + ); + + t( + test_json( + "{\"key\":[{\"a\":{\"b\":[1,false]}},{\"a\":{\"b\":[null,-2.5e-2]}}]}" + ) + ); + + // adapted from https://github.com/briandfoy/json-acceptance-tests, pass1 + { + const char* json = + "{\n" +" \"integer\": 1234567890,\n" +" \"real\": -9876.543210,\n" +" \"e\": 0.123456789e-12,\n" +" \"E\": 1.234567890E+34,\n" +" \"\": 23456789012E66,\n" +" \"zero\": 0,\n" +" \"one\": 1,\n" +" \"space\": \" \",\n" +" \"quote\": \"\\\"\",\n" +" \"backslash\": \"\\\\\",\n" +" \"controls\": \"\\b\\f\\n\\r\\t\",\n" +" \"slash\": \"/ & \\/\",\n" +" \"alpha\": \"abcdefghijklmnopqrstuvwyz\",\n" +" \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",\n" +" \"digit\": \"0123456789\",\n" +" \"0123456789\": \"digit\",\n" +" \"special\": \"`1~!@#$%^&*()_+-={':[,]}|;.?\",\n" +" \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",\n" +" \"true\": true,\n" +" \"false\": false,\n" +" \"null\": null,\n" +" \"array\":[ ],\n" +" \"object\":{ },\n" +" \"address\": \"50 St. James Street\",\n" +" \"url\": \"http://www.JSON.org/\",\n" +" \"comment\": \"// /* */\": \" \",\n" +" \" s p a c e d \" :[1,2 , 3\n" +"\n" +",\n" +"\n" +"4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],\n" +" \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",\n" +" \"quotes\": \"" \\u0022 %22 0x22 034 "\",\n" +" \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"\n" +": \"A key can be any string\"\n" +" }" + ; + + const char* expected = +" {\n" +" \"\": 23456789012E66,\n" +" \" s p a c e d \" :[1,2 , 3\n" +"\n" +" ,\n" +"\n" +" 4 , 5 , 6 ,7 ],\n" +" \"# -- --> */\": \" \",\n" +" \"/\\\\\\\"\\ucafe\\ubabe\\uab98\\ufcde\\ubcda\\uef4a\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"\n" +" : \"A key can be any string\",\n" +" \"0123456789\": \"digit\",\n" +" \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",\n" +" \"E\": 1.234567890E+34,\n" +" \"address\": \"50 St. James Street\",\n" +" \"alpha\": \"abcdefghijklmnopqrstuvwyz\",\n" +" \"array\":[ ],\n" +" \"backslash\": \"\\\\\",\n" +" \"comment\": \"// /*