diff --git a/doc/examples/flatten.cpp b/doc/examples/flatten.cpp new file mode 100644 index 00000000..5d769202 --- /dev/null +++ b/doc/examples/flatten.cpp @@ -0,0 +1,34 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // create JSON value + json j = + { + {"pi", 3.141}, + {"happy", true}, + {"name", "Niels"}, + {"nothing", nullptr}, + { + "answer", { + {"everything", 42} + } + }, + {"list", {1, 0, 2}}, + { + "object", { + {"currency", "USD"}, + {"value", 42.99}, + {"", "empty string"}, + {"/", "slash"}, + {"~", "tilde"}, + {"~1", "tilde1"} + } + } + }; + + // call flatten() + std::cout << std::setw(4) << j.flatten() << '\n'; +} diff --git a/doc/examples/flatten.link b/doc/examples/flatten.link new file mode 100644 index 00000000..70ba78ba --- /dev/null +++ b/doc/examples/flatten.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/flatten.output b/doc/examples/flatten.output new file mode 100644 index 00000000..beb368fa --- /dev/null +++ b/doc/examples/flatten.output @@ -0,0 +1,16 @@ +{ + "/answer/everything": 42, + "/happy": true, + "/list/0": 1, + "/list/1": 0, + "/list/2": 2, + "/name": "Niels", + "/nothing": null, + "/object/": "empty string", + "/object/currency": "USD", + "/object/value": 42.99, + "/object/~0": "tilde", + "/object/~01": "tilde1", + "/object/~1": "slash", + "/pi": 3.141 +} diff --git a/doc/examples/operatorjson_pointer.cpp b/doc/examples/operatorjson_pointer.cpp new file mode 100644 index 00000000..18e41c1f --- /dev/null +++ b/doc/examples/operatorjson_pointer.cpp @@ -0,0 +1,47 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // create a JSON value + json j = + { + {"number", 1}, {"string", "foo"}, {"array", {1, 2}} + }; + + // read-only access + + // output element with JSON pointer "/number" + std::cout << j["/number"_json_pointer] << '\n'; + // output element with JSON pointer "/string" + std::cout << j["/string"_json_pointer] << '\n'; + // output element with JSON pointer "/array" + std::cout << j["/array"_json_pointer] << '\n'; + // output element with JSON pointer "/array/1" + std::cout << j["/array/1"_json_pointer] << '\n'; + + // writing access + + // change the string + j["/string"_json_pointer] = "bar"; + // output the changed string + std::cout << j["string"] << '\n'; + + // "change" a nonexisting object entry + j["/boolean"_json_pointer] = true; + // output the changed object + std::cout << j << '\n'; + + // change an array element + j["/array/1"_json_pointer] = 21; + // "change" an array element with nonexisting index + j["/array/4"_json_pointer] = 44; + // output the changed array + std::cout << j["array"] << '\n'; + + // "change" the arry element past the end + j["/array/-"_json_pointer] = 55; + // output the changed array + std::cout << j["array"] << '\n'; +} diff --git a/doc/examples/operatorjson_pointer.link b/doc/examples/operatorjson_pointer.link new file mode 100644 index 00000000..3cee69e7 --- /dev/null +++ b/doc/examples/operatorjson_pointer.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/operatorjson_pointer.output b/doc/examples/operatorjson_pointer.output new file mode 100644 index 00000000..1fd1b032 --- /dev/null +++ b/doc/examples/operatorjson_pointer.output @@ -0,0 +1,8 @@ +1 +"foo" +[1,2] +2 +"bar" +{"array":[1,2],"boolean":true,"number":1,"string":"bar"} +[1,21,null,null,44] +[1,21,null,null,44,55] diff --git a/doc/examples/operatorjson_pointer_const.cpp b/doc/examples/operatorjson_pointer_const.cpp new file mode 100644 index 00000000..20ac36cb --- /dev/null +++ b/doc/examples/operatorjson_pointer_const.cpp @@ -0,0 +1,23 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // create a JSON value + const json j = + { + {"number", 1}, {"string", "foo"}, {"array", {1, 2}} + }; + + // read-only access + + // output element with JSON pointer "/number" + std::cout << j["/number"_json_pointer] << '\n'; + // output element with JSON pointer "/string" + std::cout << j["/string"_json_pointer] << '\n'; + // output element with JSON pointer "/array" + std::cout << j["/array"_json_pointer] << '\n'; + // output element with JSON pointer "/array/1" + std::cout << j["/array/1"_json_pointer] << '\n'; +} diff --git a/doc/examples/operatorjson_pointer_const.link b/doc/examples/operatorjson_pointer_const.link new file mode 100644 index 00000000..b13a9b19 --- /dev/null +++ b/doc/examples/operatorjson_pointer_const.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/operatorjson_pointer_const.output b/doc/examples/operatorjson_pointer_const.output new file mode 100644 index 00000000..7b9306bb --- /dev/null +++ b/doc/examples/operatorjson_pointer_const.output @@ -0,0 +1,4 @@ +1 +"foo" +[1,2] +2 diff --git a/doc/examples/unflatten.cpp b/doc/examples/unflatten.cpp new file mode 100644 index 00000000..39c674c9 --- /dev/null +++ b/doc/examples/unflatten.cpp @@ -0,0 +1,28 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // create JSON value + json j_flattened = + { + {"/answer/everything", 42}, + {"/happy", true}, + {"/list/0", 1}, + {"/list/1", 0}, + {"/list/2", 2}, + {"/name", "Niels"}, + {"/nothing", nullptr}, + {"/object/", "empty string"}, + {"/object/currency", "USD"}, + {"/object/value", 42.99}, + {"/object/~0", "tilde"}, + {"/object/~01", "tilde1"}, + {"/object/~1", "slash"}, + {"/pi", 3.141} + }; + + // call unflatten() + std::cout << std::setw(4) << j_flattened.unflatten() << '\n'; +} diff --git a/doc/examples/unflatten.link b/doc/examples/unflatten.link new file mode 100644 index 00000000..bc7594a0 --- /dev/null +++ b/doc/examples/unflatten.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/unflatten.output b/doc/examples/unflatten.output new file mode 100644 index 00000000..f57c9c9a --- /dev/null +++ b/doc/examples/unflatten.output @@ -0,0 +1,22 @@ +{ + "answer": { + "everything": 42 + }, + "happy": true, + "list": [ + 1, + 0, + 2 + ], + "name": "Niels", + "nothing": null, + "object": { + "": "empty string", + "/": "slash", + "currency": "USD", + "value": 42.99, + "~": "tilde", + "~1": "tilde1" + }, + "pi": 3.141 +} diff --git a/src/json.hpp b/src/json.hpp index 96bc1f32..6cf369bd 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -3598,23 +3598,86 @@ class basic_json /*! @brief access specified element via JSON Pointer - Returns a reference to the element at with specified JSON pointer @a ptr. + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. - @param p JSON pointer to the desired element + In particular: + - If the JSON pointer points to an object key that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. + - If the JSON pointer points to an array index that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. All indices between the current maximum and the given + index are also filled with `null`. + - The special value `-` is treated as a synonym for the index past the + end. + + @param[in] ptr a JSON pointer + + @return reference to the JSON value pointed to by @a ptr + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} @since version 2.0.0 */ reference operator[](const json_pointer& ptr) { - return ptr.get(*this); + return ptr.get_unchecked(this); } /*! - @copydoc basic_json::operator[](const json_pointer&) + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. The function does not change the JSON + value; no `null` values are created. In particular, the the special value + `-` yields an exception. + + @param[in] ptr a JSON pointer + + @return reference to the JSON value pointed to by @a ptr + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::out_of_range if the special value `-` is used for an array + + @liveexample{The behavior is shown in the example., + operatorjson_pointer_const} + + @since version 2.0.0 */ const_reference operator[](const json_pointer& ptr) const { - return ptr.get(*this); + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr. + + @param ptr JSON pointer to the desired element + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @copydoc basic_json::at(const json_pointer&) + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); } /*! @@ -8841,45 +8904,28 @@ basic_json_parser_63: @brief JSON Pointer @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + + @since version 2.0.0 */ class json_pointer { + /// allow basic_json to access private members + friend class basic_json; + public: /// empty reference token json_pointer() = default; /// nonempty reference token explicit json_pointer(const std::string& s) - { - split(s); - } + : reference_tokens(split(s)) + {} private: - reference get(reference j) const - { - pointer result = &j; - - for (const auto& reference_token : reference_tokens) - { - switch (result->m_type) - { - case value_t::object: - result = &result->at(reference_token); - continue; - - case value_t::array: - result = &result->at(static_cast(std::stoi(reference_token))); - continue; - - default: - throw std::domain_error("unresolved reference token '" + reference_token + "'"); - } - } - - return *result; - } - - reference get2(reference j) const + /*! + @brief create and return a reference to the pointed to value + */ + reference get_and_create(reference j) const { pointer result = &j; @@ -8922,40 +8968,172 @@ basic_json_parser_63: return *result; } - const_reference get(const_reference j) const - { - const_pointer result = &j; + /*! + @brief return a reference to the pointed to value + @param[in] ptr a JSON value + + @return reference to the JSON value pointed to by the JSON pointer + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + */ + reference get_unchecked(pointer ptr) const + { for (const auto& reference_token : reference_tokens) { - switch (result->m_type) + switch (ptr->m_type) { case value_t::object: - result = &result->at(reference_token); - continue; + { + ptr = &ptr->operator[](reference_token); + break; + } case value_t::array: - result = &result->at(static_cast(std::stoi(reference_token))); - continue; + { + if (reference_token == "-") + { + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + break; + } default: - throw std::domain_error("unresolved reference token '" + reference_token + "'"); + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } } } - return *result; + return *ptr; } - /// the reference tokens - std::vector reference_tokens {}; + reference get_checked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("cannot resolve reference token '-'"); + } + else + { + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + } + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + /*! + @brief return a const reference to the pointed to value + + @param[in] ptr a JSON value + + @return const reference to the JSON value pointed to by the JSON + pointer + */ + const_reference get_unchecked(const_pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->operator[](reference_token); + continue; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + const_reference get_checked(const_pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->at(reference_token); + continue; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } /// split the string input to reference tokens - void split(std::string reference_string) + std::vector split(std::string reference_string) { + std::vector result; + // special case: empty reference string -> no reference tokens if (reference_string.empty()) { - return; + return result; } // check if nonempty reference string begins with slash @@ -9006,10 +9184,13 @@ basic_json_parser_63: replace_substring(reference_token, "~0", "~"); // finally, store the reference token - reference_tokens.push_back(reference_token); + result.push_back(reference_token); } + + return result; } + private: /*! @brief replace all occurrences of a substring by another string @@ -9042,6 +9223,8 @@ basic_json_parser_63: @param[in] reference_string the reference string to the current value @param[in] value the value to consider @param[in,out] result the result object to insert values to + + @note Empty objects or arrays are flattened to `null`. */ static void flatten(const std::string reference_string, const basic_json& value, @@ -9051,27 +9234,43 @@ basic_json_parser_63: { case value_t::array: { - // iterate array and use index as reference string - for (size_t i = 0; i < value.m_value.array->size(); ++i) + if (value.m_value.array->empty()) { - flatten(reference_string + "/" + std::to_string(i), - value.m_value.array->operator[](i), result); + // flatten empty array as null + result[reference_string] = nullptr; + } + else + { + // iterate array and use index as reference string + for (size_t i = 0; i < value.m_value.array->size(); ++i) + { + flatten(reference_string + "/" + std::to_string(i), + value.m_value.array->operator[](i), result); + } } break; } case value_t::object: { - // iterate object and use keys as reference string - for (const auto& element : *value.m_value.object) + if (value.m_value.object->empty()) { - // escape "~"" to "~0" and "/" to "~1" - std::string key(element.first); - replace_substring(key, "~", "~0"); - replace_substring(key, "/", "~1"); + // flatten empty object as null + result[reference_string] = nullptr; + } + else + { + // iterate object and use keys as reference string + for (const auto& element : *value.m_value.object) + { + // escape "~"" to "~0" and "/" to "~1" + std::string key(element.first); + replace_substring(key, "~", "~0"); + replace_substring(key, "/", "~1"); - flatten(reference_string + "/" + key, - element.second, result); + flatten(reference_string + "/" + key, + element.second, result); + } } break; } @@ -9088,13 +9287,13 @@ basic_json_parser_63: /*! @param[in] value flattened JSON - @return deflattened JSON + @return unflattened JSON */ - static basic_json deflatten(const basic_json& value) + static basic_json unflatten(const basic_json& value) { if (not value.is_object()) { - throw std::domain_error("only objects can be deflattened"); + throw std::domain_error("only objects can be unflattened"); } basic_json result; @@ -9108,15 +9307,44 @@ basic_json_parser_63: } // assign value to reference pointed to by JSON pointer - json_pointer(element.first).get2(result) = element.second; + json_pointer(element.first).get_and_create(result) = element.second; } return result; } + + private: + /// the reference tokens + const std::vector reference_tokens {}; }; + //////////////////////////// + // JSON Pointer functions // + //////////////////////////// + + /// @name JSON Pointer functions + /// @{ + /*! + @brief return flattened JSON value + + The function creates a JSON object whose keys are JSON pointers (see + [RFC 6901](https://tools.ietf.org/html/rfc6901)) and whose values are all + primitive. The original JSON value can be restored using the + @ref unflatten() function. + @return an object that maps JSON pointers to primitve values + + @note Empty objects and arrays are flattened to `null`. + + @complexity Linear in the size the JSON value. + + @liveexample{The following code shows how a JSON object is flattened to an + object whose keys consist of JSON pointers.,flatten} + + @sa @ref unflatten() for the reverse function + + @since version 2.0.0 */ basic_json flatten() const { @@ -9126,12 +9354,38 @@ basic_json_parser_63: } /*! + @brief unflatten a previously flattened JSON value + + The function restores the arbitrary nesting of a JSON value that has been + flattened before using the @ref flatten() function. The JSON value must + meet certain constraints: + 1. The value must be an object. + 2. The keys must be JSON pointers (see + [RFC 6901](https://tools.ietf.org/html/rfc6901)) + 3. The mapped values must be primitive JSON types. + @return the original JSON from a flattened version + + @note Empty objects and arrays are flattened by @ref flatten() to `null` + values and can not unflattened to their original type. Apart from + this example, for a JSON value `j`, the following is always true: + `j == j.flatten().unflatten()`. + + @complexity Linear in the size the JSON value. + + @liveexample{The following code shows how a flattened JSON object is + unflattened into the original nested JSON object.,unflatten} + + @sa @ref flatten() for the reverse function + + @since version 2.0.0 */ - basic_json deflatten() const + basic_json unflatten() const { - return json_pointer::deflatten(*this); + return json_pointer::unflatten(*this); } + + /// @} }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 1a049cd5..7fe9673f 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -3598,23 +3598,86 @@ class basic_json /*! @brief access specified element via JSON Pointer - Returns a reference to the element at with specified JSON pointer @a ptr. + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. - @param p JSON pointer to the desired element + In particular: + - If the JSON pointer points to an object key that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. + - If the JSON pointer points to an array index that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. All indices between the current maximum and the given + index are also filled with `null`. + - The special value `-` is treated as a synonym for the index past the + end. + + @param[in] ptr a JSON pointer + + @return reference to the JSON value pointed to by @a ptr + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} @since version 2.0.0 */ reference operator[](const json_pointer& ptr) { - return ptr.get(*this); + return ptr.get_unchecked(this); } /*! - @copydoc basic_json::operator[](const json_pointer&) + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. The function does not change the JSON + value; no `null` values are created. In particular, the the special value + `-` yields an exception. + + @param[in] ptr a JSON pointer + + @return reference to the JSON value pointed to by @a ptr + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::out_of_range if the special value `-` is used for an array + + @liveexample{The behavior is shown in the example., + operatorjson_pointer_const} + + @since version 2.0.0 */ const_reference operator[](const json_pointer& ptr) const { - return ptr.get(*this); + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr. + + @param ptr JSON pointer to the desired element + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @copydoc basic_json::at(const json_pointer&) + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); } /*! @@ -8151,45 +8214,28 @@ class basic_json @brief JSON Pointer @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + + @since version 2.0.0 */ class json_pointer { + /// allow basic_json to access private members + friend class basic_json; + public: /// empty reference token json_pointer() = default; /// nonempty reference token explicit json_pointer(const std::string& s) - { - split(s); - } + : reference_tokens(split(s)) + {} private: - reference get(reference j) const - { - pointer result = &j; - - for (const auto& reference_token : reference_tokens) - { - switch (result->m_type) - { - case value_t::object: - result = &result->at(reference_token); - continue; - - case value_t::array: - result = &result->at(static_cast(std::stoi(reference_token))); - continue; - - default: - throw std::domain_error("unresolved reference token '" + reference_token + "'"); - } - } - - return *result; - } - - reference get2(reference j) const + /*! + @brief create and return a reference to the pointed to value + */ + reference get_and_create(reference j) const { pointer result = &j; @@ -8232,40 +8278,172 @@ class basic_json return *result; } - const_reference get(const_reference j) const - { - const_pointer result = &j; + /*! + @brief return a reference to the pointed to value + @param[in] ptr a JSON value + + @return reference to the JSON value pointed to by the JSON pointer + + @complexity Linear in the length of the JSON pointer. + + @throw std::out_of_range if the JSON pointer can not be resolved + */ + reference get_unchecked(pointer ptr) const + { for (const auto& reference_token : reference_tokens) { - switch (result->m_type) + switch (ptr->m_type) { case value_t::object: - result = &result->at(reference_token); - continue; + { + ptr = &ptr->operator[](reference_token); + break; + } case value_t::array: - result = &result->at(static_cast(std::stoi(reference_token))); - continue; + { + if (reference_token == "-") + { + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + break; + } default: - throw std::domain_error("unresolved reference token '" + reference_token + "'"); + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } } } - return *result; + return *ptr; } - /// the reference tokens - std::vector reference_tokens {}; + reference get_checked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("cannot resolve reference token '-'"); + } + else + { + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + } + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + /*! + @brief return a const reference to the pointed to value + + @param[in] ptr a JSON value + + @return const reference to the JSON value pointed to by the JSON + pointer + */ + const_reference get_unchecked(const_pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->operator[](reference_token); + continue; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + const_reference get_checked(const_pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + ptr = &ptr->at(reference_token); + continue; + } + + case value_t::array: + { + if (reference_token == "-") + { + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } /// split the string input to reference tokens - void split(std::string reference_string) + std::vector split(std::string reference_string) { + std::vector result; + // special case: empty reference string -> no reference tokens if (reference_string.empty()) { - return; + return result; } // check if nonempty reference string begins with slash @@ -8316,10 +8494,13 @@ class basic_json replace_substring(reference_token, "~0", "~"); // finally, store the reference token - reference_tokens.push_back(reference_token); + result.push_back(reference_token); } + + return result; } + private: /*! @brief replace all occurrences of a substring by another string @@ -8352,6 +8533,8 @@ class basic_json @param[in] reference_string the reference string to the current value @param[in] value the value to consider @param[in,out] result the result object to insert values to + + @note Empty objects or arrays are flattened to `null`. */ static void flatten(const std::string reference_string, const basic_json& value, @@ -8361,27 +8544,43 @@ class basic_json { case value_t::array: { - // iterate array and use index as reference string - for (size_t i = 0; i < value.m_value.array->size(); ++i) + if (value.m_value.array->empty()) { - flatten(reference_string + "/" + std::to_string(i), - value.m_value.array->operator[](i), result); + // flatten empty array as null + result[reference_string] = nullptr; + } + else + { + // iterate array and use index as reference string + for (size_t i = 0; i < value.m_value.array->size(); ++i) + { + flatten(reference_string + "/" + std::to_string(i), + value.m_value.array->operator[](i), result); + } } break; } case value_t::object: { - // iterate object and use keys as reference string - for (const auto& element : *value.m_value.object) + if (value.m_value.object->empty()) { - // escape "~"" to "~0" and "/" to "~1" - std::string key(element.first); - replace_substring(key, "~", "~0"); - replace_substring(key, "/", "~1"); + // flatten empty object as null + result[reference_string] = nullptr; + } + else + { + // iterate object and use keys as reference string + for (const auto& element : *value.m_value.object) + { + // escape "~"" to "~0" and "/" to "~1" + std::string key(element.first); + replace_substring(key, "~", "~0"); + replace_substring(key, "/", "~1"); - flatten(reference_string + "/" + key, - element.second, result); + flatten(reference_string + "/" + key, + element.second, result); + } } break; } @@ -8398,13 +8597,13 @@ class basic_json /*! @param[in] value flattened JSON - @return deflattened JSON + @return unflattened JSON */ - static basic_json deflatten(const basic_json& value) + static basic_json unflatten(const basic_json& value) { if (not value.is_object()) { - throw std::domain_error("only objects can be deflattened"); + throw std::domain_error("only objects can be unflattened"); } basic_json result; @@ -8418,15 +8617,44 @@ class basic_json } // assign value to reference pointed to by JSON pointer - json_pointer(element.first).get2(result) = element.second; + json_pointer(element.first).get_and_create(result) = element.second; } return result; } + + private: + /// the reference tokens + const std::vector reference_tokens {}; }; + //////////////////////////// + // JSON Pointer functions // + //////////////////////////// + + /// @name JSON Pointer functions + /// @{ + /*! + @brief return flattened JSON value + + The function creates a JSON object whose keys are JSON pointers (see + [RFC 6901](https://tools.ietf.org/html/rfc6901)) and whose values are all + primitive. The original JSON value can be restored using the + @ref unflatten() function. + @return an object that maps JSON pointers to primitve values + + @note Empty objects and arrays are flattened to `null`. + + @complexity Linear in the size the JSON value. + + @liveexample{The following code shows how a JSON object is flattened to an + object whose keys consist of JSON pointers.,flatten} + + @sa @ref unflatten() for the reverse function + + @since version 2.0.0 */ basic_json flatten() const { @@ -8436,12 +8664,38 @@ class basic_json } /*! + @brief unflatten a previously flattened JSON value + + The function restores the arbitrary nesting of a JSON value that has been + flattened before using the @ref flatten() function. The JSON value must + meet certain constraints: + 1. The value must be an object. + 2. The keys must be JSON pointers (see + [RFC 6901](https://tools.ietf.org/html/rfc6901)) + 3. The mapped values must be primitive JSON types. + @return the original JSON from a flattened version + + @note Empty objects and arrays are flattened by @ref flatten() to `null` + values and can not unflattened to their original type. Apart from + this example, for a JSON value `j`, the following is always true: + `j == j.flatten().unflatten()`. + + @complexity Linear in the size the JSON value. + + @liveexample{The following code shows how a flattened JSON object is + unflattened into the original nested JSON object.,unflatten} + + @sa @ref flatten() for the reverse function + + @since version 2.0.0 */ - basic_json deflatten() const + basic_json unflatten() const { - return json_pointer::deflatten(*this); + return json_pointer::unflatten(*this); } + + /// @} }; diff --git a/test/unit.cpp b/test/unit.cpp index 1ace40d0..a3b9035d 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12054,119 +12054,195 @@ TEST_CASE("Unicode", "[hide]") TEST_CASE("JSON pointers") { + SECTION("errors") + { + CHECK_THROWS_AS(json::json_pointer("foo"), std::domain_error); + CHECK_THROWS_WITH(json::json_pointer("foo"), "JSON pointer must be empty or begin with '/'"); + + CHECK_THROWS_AS(json::json_pointer("/~~"), std::domain_error); + CHECK_THROWS_WITH(json::json_pointer("/~~"), "escape error: '~' must be followed with '0' or '1'"); + + CHECK_THROWS_AS(json::json_pointer("/~"), std::domain_error); + CHECK_THROWS_WITH(json::json_pointer("/~"), "escape error: '~' must be followed with '0' or '1'"); + } + SECTION("examples from RFC 6901") { - json j = R"( - { - "foo": ["bar", "baz"], - "": 0, - "a/b": 1, - "c%d": 2, - "e^f": 3, - "g|h": 4, - "i\\j": 5, - "k\"l": 6, - " ": 7, - "m~n": 8 - } - )"_json; - - const json j_const = j; - SECTION("nonconst access") { + json j = R"( + { + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + } + )"_json; + // the whole document - CHECK(json::json_pointer().get(j) == j); - CHECK(json::json_pointer("").get(j) == j); CHECK(j[json::json_pointer()] == j); CHECK(j[json::json_pointer("")] == j); // array access - CHECK(json::json_pointer("/foo").get(j) == j["foo"]); - CHECK(json::json_pointer("/foo/0").get(j) == j["foo"][0]); - CHECK(json::json_pointer("/foo/1").get(j) == j["foo"][1]); CHECK(j[json::json_pointer("/foo")] == j["foo"]); CHECK(j[json::json_pointer("/foo/0")] == j["foo"][0]); CHECK(j[json::json_pointer("/foo/1")] == j["foo"][1]); CHECK(j["/foo/1"_json_pointer] == j["foo"][1]); // empty string access - CHECK(json::json_pointer("/").get(j) == j[""]); + CHECK(j[json::json_pointer("/")] == j[""]); // other cases - CHECK(json::json_pointer("/ ").get(j) == j[" "]); - CHECK(json::json_pointer("/c%d").get(j) == j["c%d"]); - CHECK(json::json_pointer("/e^f").get(j) == j["e^f"]); - CHECK(json::json_pointer("/g|h").get(j) == j["g|h"]); - CHECK(json::json_pointer("/i\\j").get(j) == j["i\\j"]); - CHECK(json::json_pointer("/k\"l").get(j) == j["k\"l"]); + CHECK(j[json::json_pointer("/ ")] == j[" "]); + CHECK(j[json::json_pointer("/c%d")] == j["c%d"]); + CHECK(j[json::json_pointer("/e^f")] == j["e^f"]); + CHECK(j[json::json_pointer("/g|h")] == j["g|h"]); + CHECK(j[json::json_pointer("/i\\j")] == j["i\\j"]); + CHECK(j[json::json_pointer("/k\"l")] == j["k\"l"]); // escaped access - CHECK(json::json_pointer("/a~1b").get(j) == j["a/b"]); - CHECK(json::json_pointer("/m~0n").get(j) == j["m~n"]); + CHECK(j[json::json_pointer("/a~1b")] == j["a/b"]); + CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]); // unescaped access - CHECK_THROWS_AS(json::json_pointer("/a/b").get(j), std::out_of_range); - CHECK_THROWS_WITH(json::json_pointer("/a/b").get(j), "key 'a' not found"); + CHECK_THROWS_AS(j[json::json_pointer("/a/b")], std::out_of_range); + CHECK_THROWS_WITH(j[json::json_pointer("/a/b")], "unresolved reference token 'b'"); // "/a/b" works for JSON {"a": {"b": 42}} - CHECK(json::json_pointer("/a/b").get({{"a", {{"b", 42}}}}) == json(42)); + CHECK(json({{"a", {{"b", 42}}}})[json::json_pointer("/a/b")] == json(42)); } SECTION("const access") { + const json j = R"( + { + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + } + )"_json; + // the whole document - CHECK(json::json_pointer().get(j_const) == j_const); - CHECK(json::json_pointer("").get(j_const) == j_const); + CHECK(j[json::json_pointer()] == j); + CHECK(j[json::json_pointer("")] == j); // array access - CHECK(json::json_pointer("/foo").get(j_const) == j_const["foo"]); - CHECK(json::json_pointer("/foo/0").get(j_const) == j_const["foo"][0]); - CHECK(json::json_pointer("/foo/1").get(j_const) == j_const["foo"][1]); + CHECK(j[json::json_pointer("/foo")] == j["foo"]); + CHECK(j[json::json_pointer("/foo/0")] == j["foo"][0]); + CHECK(j[json::json_pointer("/foo/1")] == j["foo"][1]); + CHECK(j["/foo/1"_json_pointer] == j["foo"][1]); // empty string access - CHECK(json::json_pointer("/").get(j_const) == j_const[""]); + CHECK(j[json::json_pointer("/")] == j[""]); // other cases - CHECK(json::json_pointer("/ ").get(j_const) == j_const[" "]); - CHECK(json::json_pointer("/c%d").get(j_const) == j_const["c%d"]); - CHECK(json::json_pointer("/e^f").get(j_const) == j_const["e^f"]); - CHECK(json::json_pointer("/g|h").get(j_const) == j_const["g|h"]); - CHECK(json::json_pointer("/i\\j").get(j_const) == j_const["i\\j"]); - CHECK(json::json_pointer("/k\"l").get(j_const) == j_const["k\"l"]); + CHECK(j[json::json_pointer("/ ")] == j[" "]); + CHECK(j[json::json_pointer("/c%d")] == j["c%d"]); + CHECK(j[json::json_pointer("/e^f")] == j["e^f"]); + CHECK(j[json::json_pointer("/g|h")] == j["g|h"]); + CHECK(j[json::json_pointer("/i\\j")] == j["i\\j"]); + CHECK(j[json::json_pointer("/k\"l")] == j["k\"l"]); // escaped access - CHECK(json::json_pointer("/a~1b").get(j_const) == j_const["a/b"]); - CHECK(json::json_pointer("/m~0n").get(j_const) == j_const["m~n"]); + CHECK(j[json::json_pointer("/a~1b")] == j["a/b"]); + CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]); // unescaped access - CHECK_THROWS_AS(json::json_pointer("/a/b").get(j), std::out_of_range); - CHECK_THROWS_WITH(json::json_pointer("/a/b").get(j), "key 'a' not found"); - // "/a/b" works for JSON {"a": {"b": 42}} - CHECK(json::json_pointer("/a/b").get({{"a", {{"b", 42}}}}) == json(42)); + CHECK_THROWS_AS(j.at(json::json_pointer("/a/b")), std::out_of_range); + CHECK_THROWS_WITH(j.at(json::json_pointer("/a/b")), "key 'a' not found"); } SECTION("user-defined string literal") { + json j = R"( + { + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + } + )"_json; + // the whole document - CHECK(""_json_pointer.get(j) == j); + CHECK(j[""_json_pointer] == j); // array access - CHECK("/foo"_json_pointer.get(j) == j["foo"]); - CHECK("/foo/0"_json_pointer.get(j) == j["foo"][0]); - CHECK("/foo/1"_json_pointer.get(j) == j["foo"][1]); + CHECK(j["/foo"_json_pointer] == j["foo"]); + CHECK(j["/foo/0"_json_pointer] == j["foo"][0]); + CHECK(j["/foo/1"_json_pointer] == j["foo"][1]); } + } - SECTION("errors") + SECTION("array access") + { + SECTION("nonconst access") { - CHECK_THROWS_AS(json::json_pointer("foo"), std::domain_error); - CHECK_THROWS_WITH(json::json_pointer("foo"), "JSON pointer must be empty or begin with '/'"); + json j = {1, 2, 3}; - CHECK_THROWS_AS(json::json_pointer("/~~"), std::domain_error); - CHECK_THROWS_WITH(json::json_pointer("/~~"), "escape error: '~' must be followed with '0' or '1'"); + // check reading access + CHECK(j["/0"_json_pointer] == j[0]); + CHECK(j["/1"_json_pointer] == j[1]); + CHECK(j["/2"_json_pointer] == j[2]); - CHECK_THROWS_AS(json::json_pointer("/~"), std::domain_error); - CHECK_THROWS_WITH(json::json_pointer("/~"), "escape error: '~' must be followed with '0' or '1'"); + // assign to existing index + j["/1"_json_pointer] = 13; + CHECK(j[1] == json(13)); + + // assign to nonexisting index + j["/3"_json_pointer] = 33; + CHECK(j[3] == json(33)); + + // assign to nonexisting index (with gap) + j["/5"_json_pointer] = 55; + CHECK(j == json({1, 13, 3, 33, nullptr, 55})); + + // assign to "-" + j["/-"_json_pointer] = 99; + CHECK(j == json({1, 13, 3, 33, nullptr, 55, 99})); } + + SECTION("const access") + { + const json j = {1, 2, 3}; + + // check reading access + CHECK(j["/0"_json_pointer] == j[0]); + CHECK(j["/1"_json_pointer] == j[1]); + CHECK(j["/2"_json_pointer] == j[2]); + + // assign to nonexisting index + CHECK_THROWS_AS(j.at("/3"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j.at("/3"_json_pointer), "array index 3 is out of range"); + + // assign to nonexisting index (with gap) + CHECK_THROWS_AS(j.at("/5"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j.at("/5"_json_pointer), "array index 5 is out of range"); + + // assign to "-" + CHECK_THROWS_AS(j["/-"_json_pointer], std::out_of_range); + CHECK_THROWS_WITH(j["/-"_json_pointer], "array index '-' (3) is out of range"); + CHECK_THROWS_AS(j.at("/-"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j.at("/-"_json_pointer), "array index '-' (3) is out of range"); + } + } SECTION("flatten") @@ -12216,21 +12292,27 @@ TEST_CASE("JSON pointers") // check if flattened result is as expected CHECK(j.flatten() == j_flatten); - // check if deflattened result is as expected - CHECK(j_flatten.deflatten() == j); + // check if unflattened result is as expected + CHECK(j_flatten.unflatten() == j); // explicit roundtrip check - CHECK(j.flatten().deflatten() == j); + CHECK(j.flatten().unflatten() == j); // roundtrip for primitive values json j_null; - CHECK(j_null.flatten().deflatten() == j_null); + CHECK(j_null.flatten().unflatten() == j_null); json j_number = 42; - CHECK(j_number.flatten().deflatten() == j_number); + CHECK(j_number.flatten().unflatten() == j_number); json j_boolean = false; - CHECK(j_boolean.flatten().deflatten() == j_boolean); + CHECK(j_boolean.flatten().unflatten() == j_boolean); json j_string = "foo"; - CHECK(j_string.flatten().deflatten() == j_string); + CHECK(j_string.flatten().unflatten() == j_string); + + // roundtrip for empty structured values (will be unflattened to null) + json j_array(json::value_t::array); + CHECK(j_array.flatten().unflatten() == json()); + json j_object(json::value_t::object); + CHECK(j_object.flatten().unflatten() == json()); } }