diff --git a/README.md b/README.md index d39663ee3..2dd60bf13 100644 --- a/README.md +++ b/README.md @@ -428,7 +428,7 @@ $ make $ ./json_unit "*" =============================================================================== -All tests passed (3344299 assertions in 29 test cases) +All tests passed (3344416 assertions in 30 test cases) ``` For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). diff --git a/doc/examples/at_json_pointer.cpp b/doc/examples/at_json_pointer.cpp new file mode 100644 index 000000000..0665e608c --- /dev/null +++ b/doc/examples/at_json_pointer.cpp @@ -0,0 +1,35 @@ +#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.at("/number"_json_pointer) << '\n'; + // output element with JSON pointer "/string" + std::cout << j.at("/string"_json_pointer) << '\n'; + // output element with JSON pointer "/array" + std::cout << j.at("/array"_json_pointer) << '\n'; + // output element with JSON pointer "/array/1" + std::cout << j.at("/array/1"_json_pointer) << '\n'; + + // writing access + + // change the string + j.at("/string"_json_pointer) = "bar"; + // output the changed string + std::cout << j["string"] << '\n'; + + // change an array element + j.at("/array/1"_json_pointer) = 21; + // output the changed array + std::cout << j["array"] << '\n'; +} diff --git a/doc/examples/at_json_pointer.link b/doc/examples/at_json_pointer.link new file mode 100644 index 000000000..5356294e2 --- /dev/null +++ b/doc/examples/at_json_pointer.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/at_json_pointer.output b/doc/examples/at_json_pointer.output new file mode 100644 index 000000000..11913c723 --- /dev/null +++ b/doc/examples/at_json_pointer.output @@ -0,0 +1,6 @@ +1 +"foo" +[1,2] +2 +"bar" +[1,21] diff --git a/doc/examples/at_json_pointer_const.cpp b/doc/examples/at_json_pointer_const.cpp new file mode 100644 index 000000000..e3cfc5154 --- /dev/null +++ b/doc/examples/at_json_pointer_const.cpp @@ -0,0 +1,23 @@ +#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.at("/number"_json_pointer) << '\n'; + // output element with JSON pointer "/string" + std::cout << j.at("/string"_json_pointer) << '\n'; + // output element with JSON pointer "/array" + std::cout << j.at("/array"_json_pointer) << '\n'; + // output element with JSON pointer "/array/1" + std::cout << j.at("/array/1"_json_pointer) << '\n'; +} diff --git a/doc/examples/at_json_pointer_const.link b/doc/examples/at_json_pointer_const.link new file mode 100644 index 000000000..905e60d34 --- /dev/null +++ b/doc/examples/at_json_pointer_const.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/at_json_pointer_const.output b/doc/examples/at_json_pointer_const.output new file mode 100644 index 000000000..7b9306bbc --- /dev/null +++ b/doc/examples/at_json_pointer_const.output @@ -0,0 +1,4 @@ +1 +"foo" +[1,2] +2 diff --git a/doc/examples/flatten.cpp b/doc/examples/flatten.cpp new file mode 100644 index 000000000..0601f8a3f --- /dev/null +++ b/doc/examples/flatten.cpp @@ -0,0 +1,37 @@ +#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'; + + // flatten for a primitive value + std::cout << j["pi"].flatten() << '\n'; +} diff --git a/doc/examples/flatten.link b/doc/examples/flatten.link new file mode 100644 index 000000000..0fe78bbb1 --- /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 000000000..fedfc8ef2 --- /dev/null +++ b/doc/examples/flatten.output @@ -0,0 +1,17 @@ +{ + "/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 +} +{"":3.141} diff --git a/doc/examples/json_pointer.cpp b/doc/examples/json_pointer.cpp new file mode 100644 index 000000000..140eac3b6 --- /dev/null +++ b/doc/examples/json_pointer.cpp @@ -0,0 +1,46 @@ +#include + +using json = nlohmann::json; + +int main() +{ + // correct JSON pointers + json::json_pointer p1; + json::json_pointer p2(""); + json::json_pointer p3("/"); + json::json_pointer p4("//"); + json::json_pointer p5("/foo/bar"); + json::json_pointer p6("/foo/bar/-"); + json::json_pointer p7("/foo/~0"); + json::json_pointer p8("/foo/~1"); + + // error: JSON pointer does not begin with a slash + try + { + json::json_pointer p9("foo"); + } + catch (std::domain_error& e) + { + std::cout << "domain_error: " << e.what() << '\n'; + } + + // error: JSON pointer uses escape symbol ~ not followed by 0 or 1 + try + { + json::json_pointer p10("/foo/~"); + } + catch (std::domain_error& e) + { + std::cout << "domain_error: " << e.what() << '\n'; + } + + // error: JSON pointer uses escape symbol ~ not followed by 0 or 1 + try + { + json::json_pointer p11("/foo/~3"); + } + catch (std::domain_error& e) + { + std::cout << "domain_error: " << e.what() << '\n'; + } +} diff --git a/doc/examples/json_pointer.link b/doc/examples/json_pointer.link new file mode 100644 index 000000000..c10c5fb9e --- /dev/null +++ b/doc/examples/json_pointer.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/json_pointer.output b/doc/examples/json_pointer.output new file mode 100644 index 000000000..b81c8a201 --- /dev/null +++ b/doc/examples/json_pointer.output @@ -0,0 +1,3 @@ +domain_error: JSON pointer must be empty or begin with '/' +domain_error: escape error: '~' must be followed with '0' or '1' +domain_error: escape error: '~' must be followed with '0' or '1' diff --git a/doc/examples/operatorjson_pointer.cpp b/doc/examples/operatorjson_pointer.cpp new file mode 100644 index 000000000..18e41c1f5 --- /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 000000000..3cee69e78 --- /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 000000000..1fd1b032d --- /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 000000000..20ac36cb2 --- /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 000000000..b13a9b19c --- /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 000000000..7b9306bbc --- /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 000000000..39c674c94 --- /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 000000000..bc7594a0a --- /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 000000000..f57c9c9a1 --- /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 0fdaa281d..ffa460673 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -198,6 +198,9 @@ class basic_json AllocatorType>; public: + // forward declarations + template class json_reverse_iterator; + class json_pointer; ///////////////////// // container types // @@ -227,9 +230,6 @@ class basic_json /// the type of an element const pointer using const_pointer = typename std::allocator_traits::const_pointer; - // forward declaration - template class json_reverse_iterator; - /// an iterator for a basic_json container class iterator; /// a const iterator for a basic_json container @@ -3273,8 +3273,8 @@ class basic_json @return reference to the element at index @a idx - @throw std::domain_error if JSON is not an array or null; example: `"cannot - use operator[] with string"` + @throw std::domain_error if JSON is not an array or null; example: + `"cannot use operator[] with string"` @complexity Constant if @a idx is in the range of the array. Otherwise linear in `idx - size()`. @@ -3595,6 +3595,121 @@ class basic_json } } + /*! + @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. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. + + 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 element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get_unchecked(this); + } + + /*! + @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 JSON pointer to the desired element + + @return const reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @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_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer} + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a const reference to the element at with specified JSON pointer + @a ptr, with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer_const} + + @since version 2.0.0 + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); + } + /*! @brief access specified object element with default value @@ -8813,6 +8928,570 @@ basic_json_parser_63: /// the lexer lexer m_lexer; }; + + public: + /*! + @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: + /*! + @brief create JSON pointer + + Create a JSON pointer according to the syntax described in + [Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3). + + @param[in] s string representing the JSON pointer; if omitted, the + empty string is assumed which references the whole JSON + value + + @throw std::domain_error if reference token is nonempty and does not + begin with a slash (`/`); example: `"JSON pointer must be empty or + begin with /"` + @throw std::domain_error if a tilde (`~`) is not followed by `0` + (representing `~`) or `1` (representing `/`); example: `"escape error: + ~ must be followed with 0 or 1"` + + @liveexample{The example shows the construction several valid JSON + pointers as well as the exceptional behavior.,json_pointer} + + @since version 2.0.0 + */ + explicit json_pointer(const std::string& s = "") + : reference_tokens(split(s)) + {} + + private: + /*! + @brief create and return a reference to the pointed to value + */ + reference get_and_create(reference j) const + { + pointer result = &j; + + // in case no reference tokens exist, return a reference to the + // JSON value j which will be overwritten by a primitive value + for (const auto& reference_token : reference_tokens) + { + switch (result->m_type) + { + case value_t::null: + { + if (reference_token == "0") + { + // start a new array if reference token is 0 + result = &result->operator[](0); + } + else + { + // start a new object otherwise + result = &result->operator[](reference_token); + } + break; + } + + case value_t::object: + { + // create an entry in the object + result = &result->operator[](reference_token); + break; + } + + case value_t::array: + { + // create an entry in the array + result = &result->operator[](static_cast(std::stoi(reference_token))); + break; + } + + /* + The following code is only reached if there exists a + reference token _and_ the current value is primitive. In + this case, we have an error situation, because primitive + values may only occur as single value; that is, with an + empty list of reference tokens. + */ + default: + { + throw std::domain_error("invalid value to unflatten"); + } + } + } + + return *result; + } + + /*! + @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 + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + */ + reference get_unchecked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case value_t::array: + { + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + if (reference_token == "-") + { + // explicityly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + // convert array index to number; unchecked access + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + reference get_checked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" always fails the range check + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // note: at performs range check + 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: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" cannot be used for const access + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // use unchecked array access + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + break; + } + + 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: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" always fails the range check + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // note: at performs range check + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + /// split the string input to reference tokens + std::vector split(std::string reference_string) + { + std::vector result; + + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return result; + } + + // check if nonempty reference string begins with slash + if (reference_string[0] != '/') + { + throw std::domain_error("JSON pointer must be empty or begin with '/'"); + } + + // extract the reference tokens: + // - slash: position of the last read slash (or end of string) + // - start: position after the previous slash + for ( + // search for the first slash after the first character + size_t slash = reference_string.find_first_of("/", 1), + // set the beginning of the first reference token + start = 1; + // we can stop if start == string::npos+1 = 0 + start != 0; + // set the beginning of the next reference token + // (will eventually be 0 if slash == std::string::npos) + start = slash + 1, + // find next slash + slash = reference_string.find_first_of("/", start)) + { + // use the text between the beginning of the reference token + // (start) and the last slash (slash). + auto reference_token = reference_string.substr(start, slash - start); + + // check reference tokens are properly escaped + for (size_t pos = reference_token.find_first_of("~"); + pos != std::string::npos; + pos = reference_token.find_first_of("~", pos + 1)) + { + assert(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (pos == reference_token.size() - 1 or + (reference_token[pos + 1] != '0' and + reference_token[pos + 1] != '1')) + { + throw std::domain_error("escape error: '~' must be followed with '0' or '1'"); + } + } + + // first transform any occurrence of the sequence '~1' to '/' + replace_substring(reference_token, "~1", "/"); + // then transform any occurrence of the sequence '~0' to '~' + replace_substring(reference_token, "~0", "~"); + + // finally, store the reference token + result.push_back(reference_token); + } + + return result; + } + + private: + /*! + @brief replace all occurrences of a substring by another string + + @param[in,out] s the string to manipulate + @param[in] f the substring to replace with @a t + @param[out] t the string to replace @a f + + @return The string @a s where all occurrences of @a f are replaced + with @a t. + + @pre The search string @a f must not be empty. + + @since version 2.0.0 + */ + static void replace_substring(std::string& s, + const std::string& f, + const std::string& t) + { + assert(not f.empty()); + + for ( + size_t pos = s.find(f); // find first occurrence of f + pos != std::string::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t + pos = s.find(f, pos + t.size()) // find next occurrence of f + ); + } + + /*! + @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, + basic_json& result) + { + switch (value.m_type) + { + case value_t::array: + { + if (value.m_value.array->empty()) + { + // 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: + { + if (value.m_value.object->empty()) + { + // 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); + } + } + break; + } + + default: + { + // add primitive value with its reference string + result[reference_string] = value; + break; + } + } + } + + /*! + @param[in] value flattened JSON + + @return unflattened JSON + */ + static basic_json unflatten(const basic_json& value) + { + if (not value.is_object()) + { + throw std::domain_error("only objects can be unflattened"); + } + + basic_json result; + + // iterate the JSON object values + for (const auto& element : *value.m_value.object) + { + if (not element.second.is_primitive()) + { + throw std::domain_error("values in object must be primitive"); + } + + // assign value to reference pointed to by JSON pointer; + // Note that if the JSON pointer is "" (i.e., points to the + // whole value), function get_and_create returns a reference + // to result itself. An assignment will then create a + // primitive value. + 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` and will not be + reconstructed correctly by the @ref unflatten() function. + + @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 + { + basic_json result(value_t::object); + json_pointer::flatten("", *this, result); + return result; + } + + /*! + @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 unflatten() const + { + return json_pointer::unflatten(*this); + } + + /// @} }; @@ -8875,9 +9554,9 @@ struct hash /*! @brief user-defined string literal for JSON values -This operator implements a user-defined string literal for JSON objects. It can -be used by adding \p "_json" to a string literal and returns a JSON object if -no parse error occurred. +This operator implements a user-defined string literal for JSON objects. It +can be used by adding \p "_json" to a string literal and returns a JSON object +if no parse error occurred. @param[in] s a string representation of a JSON object @return a JSON object @@ -8889,6 +9568,16 @@ inline nlohmann::json operator "" _json(const char* s, std::size_t) return nlohmann::json::parse(reinterpret_cast(s)); } +/*! +@brief user-defined string literal for JSON pointer + +@since version 2.0.0 +*/ +inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t) +{ + return nlohmann::json::json_pointer(s); +} + // restore GCC/clang diagnostic settings #if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) #pragma GCC diagnostic pop diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 33e1a4eb8..ced7ffba2 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -198,6 +198,9 @@ class basic_json AllocatorType>; public: + // forward declarations + template class json_reverse_iterator; + class json_pointer; ///////////////////// // container types // @@ -227,9 +230,6 @@ class basic_json /// the type of an element const pointer using const_pointer = typename std::allocator_traits::const_pointer; - // forward declaration - template class json_reverse_iterator; - /// an iterator for a basic_json container class iterator; /// a const iterator for a basic_json container @@ -3273,8 +3273,8 @@ class basic_json @return reference to the element at index @a idx - @throw std::domain_error if JSON is not an array or null; example: `"cannot - use operator[] with string"` + @throw std::domain_error if JSON is not an array or null; example: + `"cannot use operator[] with string"` @complexity Constant if @a idx is in the range of the array. Otherwise linear in `idx - size()`. @@ -3595,6 +3595,121 @@ class basic_json } } + /*! + @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. Similar to + @ref operator[](const typename object_t::key_type&), `null` values + are created in arrays and objects if necessary. + + 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 element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get_unchecked(this); + } + + /*! + @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 JSON pointer to the desired element + + @return const reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @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_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer} + + @since version 2.0.0 + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a const reference to the element at with specified JSON pointer + @a ptr, with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw std::out_of_range if the JSON pointer can not be resolved + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + + @liveexample{The behavior is shown in the example.,at_json_pointer_const} + + @since version 2.0.0 + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); + } + /*! @brief access specified object element with default value @@ -8123,6 +8238,570 @@ class basic_json /// the lexer lexer m_lexer; }; + + public: + /*! + @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: + /*! + @brief create JSON pointer + + Create a JSON pointer according to the syntax described in + [Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3). + + @param[in] s string representing the JSON pointer; if omitted, the + empty string is assumed which references the whole JSON + value + + @throw std::domain_error if reference token is nonempty and does not + begin with a slash (`/`); example: `"JSON pointer must be empty or + begin with /"` + @throw std::domain_error if a tilde (`~`) is not followed by `0` + (representing `~`) or `1` (representing `/`); example: `"escape error: + ~ must be followed with 0 or 1"` + + @liveexample{The example shows the construction several valid JSON + pointers as well as the exceptional behavior.,json_pointer} + + @since version 2.0.0 + */ + explicit json_pointer(const std::string& s = "") + : reference_tokens(split(s)) + {} + + private: + /*! + @brief create and return a reference to the pointed to value + */ + reference get_and_create(reference j) const + { + pointer result = &j; + + // in case no reference tokens exist, return a reference to the + // JSON value j which will be overwritten by a primitive value + for (const auto& reference_token : reference_tokens) + { + switch (result->m_type) + { + case value_t::null: + { + if (reference_token == "0") + { + // start a new array if reference token is 0 + result = &result->operator[](0); + } + else + { + // start a new object otherwise + result = &result->operator[](reference_token); + } + break; + } + + case value_t::object: + { + // create an entry in the object + result = &result->operator[](reference_token); + break; + } + + case value_t::array: + { + // create an entry in the array + result = &result->operator[](static_cast(std::stoi(reference_token))); + break; + } + + /* + The following code is only reached if there exists a + reference token _and_ the current value is primitive. In + this case, we have an error situation, because primitive + values may only occur as single value; that is, with an + empty list of reference tokens. + */ + default: + { + throw std::domain_error("invalid value to unflatten"); + } + } + } + + return *result; + } + + /*! + @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 + @throw std::domain_error if an array index begins with '0' + @throw std::invalid_argument if an array index was not a number + */ + reference get_unchecked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case value_t::array: + { + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + if (reference_token == "-") + { + // explicityly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + // convert array index to number; unchecked access + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + reference get_checked(pointer ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->m_type) + { + case value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" always fails the range check + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // note: at performs range check + 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: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" cannot be used for const access + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // use unchecked array access + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + break; + } + + 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: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case value_t::array: + { + if (reference_token == "-") + { + // "-" always fails the range check + throw std::out_of_range("array index '-' (" + + std::to_string(ptr->m_value.array->size()) + + ") is out of range"); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') + { + throw std::domain_error("array index must not begin with '0'"); + } + + // note: at performs range check + ptr = &ptr->at(static_cast(std::stoi(reference_token))); + break; + } + + default: + { + throw std::out_of_range("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *ptr; + } + + /// split the string input to reference tokens + std::vector split(std::string reference_string) + { + std::vector result; + + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return result; + } + + // check if nonempty reference string begins with slash + if (reference_string[0] != '/') + { + throw std::domain_error("JSON pointer must be empty or begin with '/'"); + } + + // extract the reference tokens: + // - slash: position of the last read slash (or end of string) + // - start: position after the previous slash + for ( + // search for the first slash after the first character + size_t slash = reference_string.find_first_of("/", 1), + // set the beginning of the first reference token + start = 1; + // we can stop if start == string::npos+1 = 0 + start != 0; + // set the beginning of the next reference token + // (will eventually be 0 if slash == std::string::npos) + start = slash + 1, + // find next slash + slash = reference_string.find_first_of("/", start)) + { + // use the text between the beginning of the reference token + // (start) and the last slash (slash). + auto reference_token = reference_string.substr(start, slash - start); + + // check reference tokens are properly escaped + for (size_t pos = reference_token.find_first_of("~"); + pos != std::string::npos; + pos = reference_token.find_first_of("~", pos + 1)) + { + assert(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (pos == reference_token.size() - 1 or + (reference_token[pos + 1] != '0' and + reference_token[pos + 1] != '1')) + { + throw std::domain_error("escape error: '~' must be followed with '0' or '1'"); + } + } + + // first transform any occurrence of the sequence '~1' to '/' + replace_substring(reference_token, "~1", "/"); + // then transform any occurrence of the sequence '~0' to '~' + replace_substring(reference_token, "~0", "~"); + + // finally, store the reference token + result.push_back(reference_token); + } + + return result; + } + + private: + /*! + @brief replace all occurrences of a substring by another string + + @param[in,out] s the string to manipulate + @param[in] f the substring to replace with @a t + @param[out] t the string to replace @a f + + @return The string @a s where all occurrences of @a f are replaced + with @a t. + + @pre The search string @a f must not be empty. + + @since version 2.0.0 + */ + static void replace_substring(std::string& s, + const std::string& f, + const std::string& t) + { + assert(not f.empty()); + + for ( + size_t pos = s.find(f); // find first occurrence of f + pos != std::string::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t + pos = s.find(f, pos + t.size()) // find next occurrence of f + ); + } + + /*! + @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, + basic_json& result) + { + switch (value.m_type) + { + case value_t::array: + { + if (value.m_value.array->empty()) + { + // 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: + { + if (value.m_value.object->empty()) + { + // 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); + } + } + break; + } + + default: + { + // add primitive value with its reference string + result[reference_string] = value; + break; + } + } + } + + /*! + @param[in] value flattened JSON + + @return unflattened JSON + */ + static basic_json unflatten(const basic_json& value) + { + if (not value.is_object()) + { + throw std::domain_error("only objects can be unflattened"); + } + + basic_json result; + + // iterate the JSON object values + for (const auto& element : *value.m_value.object) + { + if (not element.second.is_primitive()) + { + throw std::domain_error("values in object must be primitive"); + } + + // assign value to reference pointed to by JSON pointer; + // Note that if the JSON pointer is "" (i.e., points to the + // whole value), function get_and_create returns a reference + // to result itself. An assignment will then create a + // primitive value. + 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` and will not be + reconstructed correctly by the @ref unflatten() function. + + @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 + { + basic_json result(value_t::object); + json_pointer::flatten("", *this, result); + return result; + } + + /*! + @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 unflatten() const + { + return json_pointer::unflatten(*this); + } + + /// @} }; @@ -8185,9 +8864,9 @@ struct hash /*! @brief user-defined string literal for JSON values -This operator implements a user-defined string literal for JSON objects. It can -be used by adding \p "_json" to a string literal and returns a JSON object if -no parse error occurred. +This operator implements a user-defined string literal for JSON objects. It +can be used by adding \p "_json" to a string literal and returns a JSON object +if no parse error occurred. @param[in] s a string representation of a JSON object @return a JSON object @@ -8199,6 +8878,16 @@ inline nlohmann::json operator "" _json(const char* s, std::size_t) return nlohmann::json::parse(reinterpret_cast(s)); } +/*! +@brief user-defined string literal for JSON pointer + +@since version 2.0.0 +*/ +inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t) +{ + return nlohmann::json::json_pointer(s); +} + // restore GCC/clang diagnostic settings #if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) #pragma GCC diagnostic pop diff --git a/test/unit.cpp b/test/unit.cpp index f6b670d86..2666e1111 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12052,6 +12052,345 @@ 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") + { + 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(j[json::json_pointer()] == j); + CHECK(j[json::json_pointer("")] == j); + + // array access + 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]); + + // checked array access + CHECK(j.at(json::json_pointer("/foo/0")) == j["foo"][0]); + CHECK(j.at(json::json_pointer("/foo/1")) == j["foo"][1]); + + // empty string access + CHECK(j[json::json_pointer("/")] == j[""]); + + // other cases + 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"]); + + // checked access + CHECK(j.at(json::json_pointer("/ ")) == j[" "]); + CHECK(j.at(json::json_pointer("/c%d")) == j["c%d"]); + CHECK(j.at(json::json_pointer("/e^f")) == j["e^f"]); + CHECK(j.at(json::json_pointer("/g|h")) == j["g|h"]); + CHECK(j.at(json::json_pointer("/i\\j")) == j["i\\j"]); + CHECK(j.at(json::json_pointer("/k\"l")) == j["k\"l"]); + + // escaped access + 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(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({{"a", {{"b", 42}}}})[json::json_pointer("/a/b")] == json(42)); + + // unresolved access + json j_primitive = 1; + CHECK_THROWS_AS(j_primitive["/foo"_json_pointer], std::out_of_range); + CHECK_THROWS_WITH(j_primitive["/foo"_json_pointer], "unresolved reference token 'foo'"); + CHECK_THROWS_AS(j_primitive.at("/foo"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j_primitive.at("/foo"_json_pointer), "unresolved reference token 'foo'"); + } + + 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(j[json::json_pointer()] == j); + CHECK(j[json::json_pointer("")] == j); + + // array access + 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]); + + // checked array access + CHECK(j.at(json::json_pointer("/foo/0")) == j["foo"][0]); + CHECK(j.at(json::json_pointer("/foo/1")) == j["foo"][1]); + + // empty string access + CHECK(j[json::json_pointer("/")] == j[""]); + + // other cases + 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"]); + + // checked access + CHECK(j.at(json::json_pointer("/ ")) == j[" "]); + CHECK(j.at(json::json_pointer("/c%d")) == j["c%d"]); + CHECK(j.at(json::json_pointer("/e^f")) == j["e^f"]); + CHECK(j.at(json::json_pointer("/g|h")) == j["g|h"]); + CHECK(j.at(json::json_pointer("/i\\j")) == j["i\\j"]); + CHECK(j.at(json::json_pointer("/k\"l")) == j["k\"l"]); + + // escaped access + 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(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"); + + // unresolved access + const json j_primitive = 1; + CHECK_THROWS_AS(j_primitive["/foo"_json_pointer], std::out_of_range); + CHECK_THROWS_WITH(j_primitive["/foo"_json_pointer], "unresolved reference token 'foo'"); + CHECK_THROWS_AS(j_primitive.at("/foo"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j_primitive.at("/foo"_json_pointer), "unresolved reference token 'foo'"); + } + + 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(j[""_json_pointer] == j); + + // array access + 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("array access") + { + SECTION("nonconst access") + { + json j = {1, 2, 3}; + const json j_const = j; + + // 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 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})); + + // error with leading 0 + CHECK_THROWS_AS(j["/01"_json_pointer], std::domain_error); + CHECK_THROWS_WITH(j["/01"_json_pointer], "array index must not begin with '0'"); + CHECK_THROWS_AS(j_const["/01"_json_pointer], std::domain_error); + CHECK_THROWS_WITH(j_const["/01"_json_pointer], "array index must not begin with '0'"); + CHECK_THROWS_AS(j.at("/01"_json_pointer), std::domain_error); + CHECK_THROWS_WITH(j.at("/01"_json_pointer), "array index must not begin with '0'"); + CHECK_THROWS_AS(j_const.at("/01"_json_pointer), std::domain_error); + CHECK_THROWS_WITH(j_const.at("/01"_json_pointer), "array index must not begin with '0'"); + + // error with incorrect numbers + CHECK_THROWS_AS(j["/one"_json_pointer] = 1, std::invalid_argument); + + // assign to "-" + j["/-"_json_pointer] = 99; + CHECK(j == json({1, 13, 3, 33, nullptr, 55, 99})); + + // error when using "-" in const object + CHECK_THROWS_AS(j_const["/-"_json_pointer], std::out_of_range); + CHECK_THROWS_WITH(j_const["/-"_json_pointer], "array index '-' (3) is out of range"); + + // error when using "-" with at + CHECK_THROWS_AS(j.at("/-"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j.at("/-"_json_pointer), "array index '-' (7) is out of range"); + CHECK_THROWS_AS(j_const.at("/-"_json_pointer), std::out_of_range); + CHECK_THROWS_WITH(j_const.at("/-"_json_pointer), "array index '-' (3) is out of range"); + } + + 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") + { + 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"} + } + } + }; + + json j_flatten = + { + {"/pi", 3.141}, + {"/happy", true}, + {"/name", "Niels"}, + {"/nothing", nullptr}, + {"/answer/everything", 42}, + {"/list/0", 1}, + {"/list/1", 0}, + {"/list/2", 2}, + {"/object/currency", "USD"}, + {"/object/value", 42.99}, + {"/object/", "empty string"}, + {"/object/~1", "slash"}, + {"/object/~0", "tilde"}, + {"/object/~01", "tilde1"} + }; + + // check if flattened result is as expected + CHECK(j.flatten() == j_flatten); + + // check if unflattened result is as expected + CHECK(j_flatten.unflatten() == j); + + // error for nonobjects + CHECK_THROWS_AS(json(1).unflatten(), std::domain_error); + CHECK_THROWS_WITH(json(1).unflatten(), "only objects can be unflattened"); + + // error for nonprimitve values + CHECK_THROWS_AS(json({{"/1", {1, 2, 3}}}).unflatten(), std::domain_error); + CHECK_THROWS_WITH(json({{"/1", {1, 2, 3}}}).unflatten(), "values in object must be primitive"); + + // error for conflicting values + json j_error = {{"", 42}, {"/foo", 17}}; + CHECK_THROWS_AS(j_error.unflatten(), std::domain_error); + CHECK_THROWS_WITH(j_error.unflatten(), "invalid value to unflatten"); + + // explicit roundtrip check + CHECK(j.flatten().unflatten() == j); + + // roundtrip for primitive values + json j_null; + CHECK(j_null.flatten().unflatten() == j_null); + json j_number = 42; + CHECK(j_number.flatten().unflatten() == j_number); + json j_boolean = false; + CHECK(j_boolean.flatten().unflatten() == j_boolean); + json j_string = "foo"; + 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()); + } +} + TEST_CASE("regression tests") { SECTION("issue #60 - Double quotation mark is not parsed correctly") @@ -12419,4 +12758,3 @@ TEST_CASE("regression tests") CHECK(dest == expected); } } -