From 726051e9b75009e68bb216171ac8e1ad56fdb99a Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 11 Apr 2016 23:17:03 +0200 Subject: [PATCH 01/13] very first draft of a JSON pointer API --- src/json.hpp | 73 +++++++++++++++++++++++++++++++++++++++++++++++ src/json.hpp.re2c | 73 +++++++++++++++++++++++++++++++++++++++++++++++ test/unit.cpp | 29 +++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/src/json.hpp b/src/json.hpp index 8f671fb0..c8b81504 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8844,6 +8844,79 @@ basic_json_parser_64: /// the lexer lexer m_lexer; }; + + public: + class json_pointer + { + public: + /// empty reference token + json_pointer() = default; + + /// nonempty reference token + json_pointer(const std::string& s) + { + split(s); + } + + /// return referenced value + reference get(reference j) + { + reference result = j; + + for (const auto& reference_token : reference_tokens) + { + switch (result.m_type) + { + case value_t::object: + result = result[reference_token]; + continue; + + case value_t::array: + result = result[std::stoi(reference_token)]; + continue; + + default: + throw std::domain_error("unresolved reference token '" + reference_token + "'"); + } + } + + return result; + } + + private: + /// the reference tokens + std::vector reference_tokens {}; + + /// split the string input to reference tokens + void split(std::string reference_string) + { + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return; + } + + // check if nonempty reference string begins with slash + if (reference_string[0] != '/') + { + throw std::domain_error("JSON pointer must be empty or begin with '/'"); + } + + // tokenize reference string + auto ptr = std::strtok(&reference_string[0], "/"); + while (ptr != nullptr) + { + reference_tokens.push_back(ptr); + ptr = std::strtok(NULL, "/"); + } + + // special case: reference string was just "/" + if (reference_tokens.empty()) + { + reference_tokens.push_back(""); + } + } + }; }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index ebf83d83..164f4962 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8123,6 +8123,79 @@ class basic_json /// the lexer lexer m_lexer; }; + + public: + class json_pointer + { + public: + /// empty reference token + json_pointer() = default; + + /// nonempty reference token + json_pointer(const std::string& s) + { + split(s); + } + + /// return referenced value + reference get(reference j) + { + reference result = j; + + for (const auto& reference_token : reference_tokens) + { + switch (result.m_type) + { + case value_t::object: + result = result[reference_token]; + continue; + + case value_t::array: + result = result[std::stoi(reference_token)]; + continue; + + default: + throw std::domain_error("unresolved reference token '" + reference_token + "'"); + } + } + + return result; + } + + private: + /// the reference tokens + std::vector reference_tokens {}; + + /// split the string input to reference tokens + void split(std::string reference_string) + { + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return; + } + + // check if nonempty reference string begins with slash + if (reference_string[0] != '/') + { + throw std::domain_error("JSON pointer must be empty or begin with '/'"); + } + + // tokenize reference string + auto ptr = std::strtok(&reference_string[0], "/"); + while (ptr != nullptr) + { + reference_tokens.push_back(ptr); + ptr = std::strtok(NULL, "/"); + } + + // special case: reference string was just "/" + if (reference_tokens.empty()) + { + reference_tokens.push_back(""); + } + } + }; }; diff --git a/test/unit.cpp b/test/unit.cpp index ab96364c..6051ee3e 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12052,6 +12052,35 @@ TEST_CASE("Unicode", "[hide]") } } +TEST_CASE("JSON pointers") +{ + 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; + + json::json_pointer jp0(""); + json::json_pointer jp1("/foo"); + //json::json_pointer jp2("/foo/0"); + + auto jp0_ = jp0.get(j); + auto jp1_ = jp1.get(j); + //auto jp2_ = jp2.get(j); + } +} + TEST_CASE("regression tests") { SECTION("issue #60 - Double quotation mark is not parsed correctly") From 2cb925c186e623f19b61fe0ce41bb772406df462 Mon Sep 17 00:00:00 2001 From: Niels Date: Wed, 13 Apr 2016 17:41:19 +0200 Subject: [PATCH 02/13] adding support for escaped reference tokens --- src/json.hpp | 57 ++++++++++++++++++++++++++++++++++++++++++----- src/json.hpp.re2c | 57 ++++++++++++++++++++++++++++++++++++++++++----- test/unit.cpp | 53 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 150 insertions(+), 17 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index c8b81504..f2c3813a 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8861,18 +8861,18 @@ basic_json_parser_64: /// return referenced value reference get(reference j) { - reference result = j; + pointer result = &j; for (const auto& reference_token : reference_tokens) { - switch (result.m_type) + switch (result->m_type) { case value_t::object: - result = result[reference_token]; + result = &result->at(reference_token); continue; case value_t::array: - result = result[std::stoi(reference_token)]; + result = &result->at(static_cast(std::stoi(reference_token))); continue; default: @@ -8880,13 +8880,52 @@ basic_json_parser_64: } } - return result; + return *result; + } + + const_reference get(const_reference j) const + { + 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; } private: /// the reference tokens std::vector reference_tokens {}; + /// replace all occurrences of a substring by another string + 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 + ); + } + /// split the string input to reference tokens void split(std::string reference_string) { @@ -8915,6 +8954,14 @@ basic_json_parser_64: { reference_tokens.push_back(""); } + + for (auto& reference_token : reference_tokens) + { + // 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", "~"); + } } }; }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 164f4962..ea62dca5 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8140,18 +8140,18 @@ class basic_json /// return referenced value reference get(reference j) { - reference result = j; + pointer result = &j; for (const auto& reference_token : reference_tokens) { - switch (result.m_type) + switch (result->m_type) { case value_t::object: - result = result[reference_token]; + result = &result->at(reference_token); continue; case value_t::array: - result = result[std::stoi(reference_token)]; + result = &result->at(static_cast(std::stoi(reference_token))); continue; default: @@ -8159,13 +8159,52 @@ class basic_json } } - return result; + return *result; + } + + const_reference get(const_reference j) const + { + 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; } private: /// the reference tokens std::vector reference_tokens {}; + /// replace all occurrences of a substring by another string + 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 + ); + } + /// split the string input to reference tokens void split(std::string reference_string) { @@ -8194,6 +8233,14 @@ class basic_json { reference_tokens.push_back(""); } + + for (auto& reference_token : reference_tokens) + { + // 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", "~"); + } } }; }; diff --git a/test/unit.cpp b/test/unit.cpp index 6051ee3e..d6ec00a7 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12071,13 +12071,53 @@ TEST_CASE("JSON pointers") } )"_json; - json::json_pointer jp0(""); - json::json_pointer jp1("/foo"); - //json::json_pointer jp2("/foo/0"); + const json j_const = j; - auto jp0_ = jp0.get(j); - auto jp1_ = jp1.get(j); - //auto jp2_ = jp2.get(j); + SECTION("nonconst access") + { + // the whole document + CHECK(json::json_pointer().get(j) == j); + CHECK(json::json_pointer("").get(j) == 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]); + + // empty string access + CHECK(json::json_pointer("/").get(j) == 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"]); + + // escaped access + CHECK(json::json_pointer("/a~1b").get(j) == j["a/b"]); + CHECK(json::json_pointer("/m~0n").get(j) == 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)); + } + + SECTION("const access") + { + CHECK(j_const == json::json_pointer().get(j_const)); + CHECK(j_const == json::json_pointer("").get(j_const)); + + CHECK(j_const["foo"] == json::json_pointer("/foo").get(j_const)); + CHECK(j_const["foo"][0] == json::json_pointer("/foo/0").get(j_const)); + CHECK(j_const["foo"][1] == json::json_pointer("/foo/1").get(j_const)); + + CHECK(j_const[""] == json::json_pointer("/").get(j_const)); + CHECK(j_const[" "] == json::json_pointer("/ ").get(j_const)); + } } } @@ -12437,4 +12477,3 @@ TEST_CASE("regression tests") CHECK(j3c.dump() == "1e04"); } } - From 94af8abdff77de3526aca2c2ccfd03517ee678e5 Mon Sep 17 00:00:00 2001 From: Niels Date: Wed, 13 Apr 2016 23:23:54 +0200 Subject: [PATCH 03/13] overworked reference token parsing --- src/json.hpp | 84 ++++++++++++++++++++++++++++++++++++----------- src/json.hpp.re2c | 84 ++++++++++++++++++++++++++++++++++++----------- test/unit.cpp | 23 +++++++++++++ 3 files changed, 151 insertions(+), 40 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index f2c3813a..84e10064 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8911,10 +8911,21 @@ basic_json_parser_64: /// the reference tokens std::vector reference_tokens {}; - /// replace all occurrences of a substring by another string - void replace_substring(std::string& s, - const std::string& f, - const std::string& t) + /*! + @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 @f must not be empty. + */ + static void replace_substring(std::string& s, + const std::string& f, + const std::string& t) { assert(not f.empty()); @@ -8941,26 +8952,49 @@ basic_json_parser_64: throw std::domain_error("JSON pointer must be empty or begin with '/'"); } - // tokenize reference string - auto ptr = std::strtok(&reference_string[0], "/"); - while (ptr != nullptr) + // 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 + // (could be 0 if slash == std::string::npos) + start = slash + 1, + // find next slash + slash = reference_string.find_first_of("/", start)) { - reference_tokens.push_back(ptr); - ptr = std::strtok(NULL, "/"); - } + // 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); - // special case: reference string was just "/" - if (reference_tokens.empty()) - { - reference_tokens.push_back(""); - } + // 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'"); + } + } - for (auto& reference_token : reference_tokens) - { // 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", "~"); + + // store the reference token + reference_tokens.push_back(reference_token); } } }; @@ -9026,9 +9060,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 @@ -9040,6 +9074,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 ea62dca5..5501b35c 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8190,10 +8190,21 @@ class basic_json /// the reference tokens std::vector reference_tokens {}; - /// replace all occurrences of a substring by another string - void replace_substring(std::string& s, - const std::string& f, - const std::string& t) + /*! + @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 @f must not be empty. + */ + static void replace_substring(std::string& s, + const std::string& f, + const std::string& t) { assert(not f.empty()); @@ -8220,26 +8231,49 @@ class basic_json throw std::domain_error("JSON pointer must be empty or begin with '/'"); } - // tokenize reference string - auto ptr = std::strtok(&reference_string[0], "/"); - while (ptr != nullptr) + // 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 + // (could be 0 if slash == std::string::npos) + start = slash + 1, + // find next slash + slash = reference_string.find_first_of("/", start)) { - reference_tokens.push_back(ptr); - ptr = std::strtok(NULL, "/"); - } + // 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); - // special case: reference string was just "/" - if (reference_tokens.empty()) - { - reference_tokens.push_back(""); - } + // 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'"); + } + } - for (auto& reference_token : reference_tokens) - { // 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", "~"); + + // store the reference token + reference_tokens.push_back(reference_token); } } }; @@ -8305,9 +8339,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 @@ -8319,6 +8353,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 d6ec00a7..7c8fd8b7 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12118,6 +12118,29 @@ TEST_CASE("JSON pointers") CHECK(j_const[""] == json::json_pointer("/").get(j_const)); CHECK(j_const[" "] == json::json_pointer("/ ").get(j_const)); } + + SECTION("user-defined string literal") + { + // the whole document + CHECK(""_json_pointer.get(j) == 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]); + } + + 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'"); + } } } From 3401954f5b3b163c7b152a314f8e78b534eb54e1 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 16 Apr 2016 14:02:14 +0200 Subject: [PATCH 04/13] cleaned up API --- src/json.hpp | 45 ++++++++++++++++++++++++++++++++++++--------- src/json.hpp.re2c | 45 ++++++++++++++++++++++++++++++++++++--------- test/unit.cpp | 40 +++++++++++++++++++++++++++++++++------- 3 files changed, 105 insertions(+), 25 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 34750884..2d383136 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 @@ -3595,6 +3595,28 @@ class basic_json } } + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr. + + @param p JSON pointer to the desired element + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get(*this); + } + + /*! + @copydoc basic_json::operator[](const json_pointer&) + */ + const_reference operator[](const json_pointer& ptr) const + { + return ptr.get(*this); + } + /*! @brief access specified object element with default value @@ -8815,6 +8837,11 @@ basic_json_parser_63: }; public: + /*! + @brief JSON Pointer + + @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + */ class json_pointer { public: @@ -8822,13 +8849,14 @@ basic_json_parser_63: json_pointer() = default; /// nonempty reference token - json_pointer(const std::string& s) + explicit json_pointer(const std::string& s) { split(s); } + private: /// return referenced value - reference get(reference j) + reference get(reference j) const { pointer result = &j; @@ -8876,7 +8904,6 @@ basic_json_parser_63: return *result; } - private: /// the reference tokens std::vector reference_tokens {}; @@ -8890,7 +8917,7 @@ basic_json_parser_63: @return The string @a s where all occurrences of @a f are replaced with @a t. - @pre The search string @f must not be empty. + @pre The search string @a f must not be empty. */ static void replace_substring(std::string& s, const std::string& f, @@ -8932,7 +8959,7 @@ basic_json_parser_63: // we can stop if start == string::npos+1 = 0 start != 0; // set the beginning of the next reference token - // (could be 0 if slash == std::string::npos) + // (will eventually be 0 if slash == std::string::npos) start = slash + 1, // find next slash slash = reference_string.find_first_of("/", start)) @@ -8962,7 +8989,7 @@ basic_json_parser_63: // then transform any occurrence of the sequence '~0' to '~' replace_substring(reference_token, "~0", "~"); - // store the reference token + // finally, store the reference token reference_tokens.push_back(reference_token); } } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 8364d03b..dd4eeb83 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 @@ -3595,6 +3595,28 @@ class basic_json } } + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr. + + @param p JSON pointer to the desired element + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get(*this); + } + + /*! + @copydoc basic_json::operator[](const json_pointer&) + */ + const_reference operator[](const json_pointer& ptr) const + { + return ptr.get(*this); + } + /*! @brief access specified object element with default value @@ -8125,6 +8147,11 @@ class basic_json }; public: + /*! + @brief JSON Pointer + + @sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + */ class json_pointer { public: @@ -8132,13 +8159,14 @@ class basic_json json_pointer() = default; /// nonempty reference token - json_pointer(const std::string& s) + explicit json_pointer(const std::string& s) { split(s); } + private: /// return referenced value - reference get(reference j) + reference get(reference j) const { pointer result = &j; @@ -8186,7 +8214,6 @@ class basic_json return *result; } - private: /// the reference tokens std::vector reference_tokens {}; @@ -8200,7 +8227,7 @@ class basic_json @return The string @a s where all occurrences of @a f are replaced with @a t. - @pre The search string @f must not be empty. + @pre The search string @a f must not be empty. */ static void replace_substring(std::string& s, const std::string& f, @@ -8242,7 +8269,7 @@ class basic_json // we can stop if start == string::npos+1 = 0 start != 0; // set the beginning of the next reference token - // (could be 0 if slash == std::string::npos) + // (will eventually be 0 if slash == std::string::npos) start = slash + 1, // find next slash slash = reference_string.find_first_of("/", start)) @@ -8272,7 +8299,7 @@ class basic_json // then transform any occurrence of the sequence '~0' to '~' replace_substring(reference_token, "~0", "~"); - // store the reference token + // finally, store the reference token reference_tokens.push_back(reference_token); } } diff --git a/test/unit.cpp b/test/unit.cpp index 628c2755..223de2c2 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12078,11 +12078,17 @@ TEST_CASE("JSON pointers") // 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[""]); @@ -12108,15 +12114,35 @@ TEST_CASE("JSON pointers") SECTION("const access") { - CHECK(j_const == json::json_pointer().get(j_const)); - CHECK(j_const == json::json_pointer("").get(j_const)); + // the whole document + CHECK(json::json_pointer().get(j_const) == j_const); + CHECK(json::json_pointer("").get(j_const) == j_const); - CHECK(j_const["foo"] == json::json_pointer("/foo").get(j_const)); - CHECK(j_const["foo"][0] == json::json_pointer("/foo/0").get(j_const)); - CHECK(j_const["foo"][1] == json::json_pointer("/foo/1").get(j_const)); + // 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_const[""] == json::json_pointer("/").get(j_const)); - CHECK(j_const[" "] == json::json_pointer("/ ").get(j_const)); + // empty string access + CHECK(json::json_pointer("/").get(j_const) == j_const[""]); + + // 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"]); + + // 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"]); + + // 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)); } SECTION("user-defined string literal") From 007359675b0ee39f5a70ce1a7adfc9d268388050 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 16 Apr 2016 16:39:20 +0200 Subject: [PATCH 05/13] added a flatten function --- src/json.hpp | 111 +++++++++++++++++++++++++++++++++++----------- src/json.hpp.re2c | 111 +++++++++++++++++++++++++++++++++++----------- test/unit.cpp | 47 ++++++++++++++++++++ 3 files changed, 217 insertions(+), 52 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 2d383136..6aca24ad 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8907,32 +8907,6 @@ basic_json_parser_63: /// the reference tokens std::vector reference_tokens {}; - /*! - @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. - */ - 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 - ); - } - /// split the string input to reference tokens void split(std::string reference_string) { @@ -8993,7 +8967,92 @@ basic_json_parser_63: reference_tokens.push_back(reference_token); } } + + /*! + @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 + */ + static void flatten(const std::string reference_string, + const basic_json& value, + basic_json& result) + { + switch (value.m_type) + { + case value_t::array: + { + // 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) + { + // 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; + } + } + } }; + + /*! + @return an object that maps JSON pointers to primitve values + */ + basic_json flatten() const + { + basic_json result(value_t::object); + json_pointer::flatten("", *this, result); + return result; + } }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index dd4eeb83..74827e2c 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8217,32 +8217,6 @@ class basic_json /// the reference tokens std::vector reference_tokens {}; - /*! - @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. - */ - 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 - ); - } - /// split the string input to reference tokens void split(std::string reference_string) { @@ -8303,7 +8277,92 @@ class basic_json reference_tokens.push_back(reference_token); } } + + /*! + @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 + */ + static void flatten(const std::string reference_string, + const basic_json& value, + basic_json& result) + { + switch (value.m_type) + { + case value_t::array: + { + // 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) + { + // 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; + } + } + } }; + + /*! + @return an object that maps JSON pointers to primitve values + */ + basic_json flatten() const + { + basic_json result(value_t::object); + json_pointer::flatten("", *this, result); + return result; + } }; diff --git a/test/unit.cpp b/test/unit.cpp index 223de2c2..0d9aa0a7 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12168,6 +12168,53 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH(json::json_pointer("/~"), "escape error: '~' must be followed with '0' or '1'"); } } + + 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(j.flatten() == j_flatten); + } } TEST_CASE("regression tests") From f834965b44572b14a0865763aed3fde02facf0e4 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 16 Apr 2016 20:45:40 +0200 Subject: [PATCH 06/13] implemented deflatten function --- src/json.hpp | 81 ++++++++++++++++++++++++++++++++++++++++++++++- src/json.hpp.re2c | 81 ++++++++++++++++++++++++++++++++++++++++++++++- test/unit.cpp | 17 ++++++++++ 3 files changed, 177 insertions(+), 2 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 6aca24ad..96bc1f32 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8855,7 +8855,6 @@ basic_json_parser_63: } private: - /// return referenced value reference get(reference j) const { pointer result = &j; @@ -8880,6 +8879,49 @@ basic_json_parser_63: return *result; } + reference get2(reference j) const + { + pointer result = &j; + + for (const auto& reference_token : reference_tokens) + { + switch (result->m_type) + { + case value_t::null: + { + if (reference_token == "0") + { + result = &result->operator[](0); + } + else + { + result = &result->operator[](reference_token); + } + continue; + } + + case value_t::object: + { + result = &result->operator[](reference_token); + continue; + } + + case value_t::array: + { + result = &result->operator[](static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::domain_error("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *result; + } + const_reference get(const_reference j) const { const_pointer result = &j; @@ -9042,6 +9084,35 @@ basic_json_parser_63: } } } + + /*! + @param[in] value flattened JSON + + @return deflattened JSON + */ + static basic_json deflatten(const basic_json& value) + { + if (not value.is_object()) + { + throw std::domain_error("only objects can be deflattened"); + } + + 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 + json_pointer(element.first).get2(result) = element.second; + } + + return result; + } }; /*! @@ -9053,6 +9124,14 @@ basic_json_parser_63: json_pointer::flatten("", *this, result); return result; } + + /*! + @return the original JSON from a flattened version + */ + basic_json deflatten() const + { + return json_pointer::deflatten(*this); + } }; diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 74827e2c..1a049cd5 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8165,7 +8165,6 @@ class basic_json } private: - /// return referenced value reference get(reference j) const { pointer result = &j; @@ -8190,6 +8189,49 @@ class basic_json return *result; } + reference get2(reference j) const + { + pointer result = &j; + + for (const auto& reference_token : reference_tokens) + { + switch (result->m_type) + { + case value_t::null: + { + if (reference_token == "0") + { + result = &result->operator[](0); + } + else + { + result = &result->operator[](reference_token); + } + continue; + } + + case value_t::object: + { + result = &result->operator[](reference_token); + continue; + } + + case value_t::array: + { + result = &result->operator[](static_cast(std::stoi(reference_token))); + continue; + } + + default: + { + throw std::domain_error("unresolved reference token '" + reference_token + "'"); + } + } + } + + return *result; + } + const_reference get(const_reference j) const { const_pointer result = &j; @@ -8352,6 +8394,35 @@ class basic_json } } } + + /*! + @param[in] value flattened JSON + + @return deflattened JSON + */ + static basic_json deflatten(const basic_json& value) + { + if (not value.is_object()) + { + throw std::domain_error("only objects can be deflattened"); + } + + 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 + json_pointer(element.first).get2(result) = element.second; + } + + return result; + } }; /*! @@ -8363,6 +8434,14 @@ class basic_json json_pointer::flatten("", *this, result); return result; } + + /*! + @return the original JSON from a flattened version + */ + basic_json deflatten() const + { + return json_pointer::deflatten(*this); + } }; diff --git a/test/unit.cpp b/test/unit.cpp index 0d9aa0a7..1ace40d0 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12213,7 +12213,24 @@ TEST_CASE("JSON pointers") {"/object/~01", "tilde1"} }; + // check if flattened result is as expected CHECK(j.flatten() == j_flatten); + + // check if deflattened result is as expected + CHECK(j_flatten.deflatten() == j); + + // explicit roundtrip check + CHECK(j.flatten().deflatten() == j); + + // roundtrip for primitive values + json j_null; + CHECK(j_null.flatten().deflatten() == j_null); + json j_number = 42; + CHECK(j_number.flatten().deflatten() == j_number); + json j_boolean = false; + CHECK(j_boolean.flatten().deflatten() == j_boolean); + json j_string = "foo"; + CHECK(j_string.flatten().deflatten() == j_string); } } From 40e899a819ac105a591ab1e8405b938728c94e2e Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 17:39:35 +0200 Subject: [PATCH 07/13] cleanup and documentation --- doc/examples/flatten.cpp | 34 ++ doc/examples/flatten.link | 1 + doc/examples/flatten.output | 16 + doc/examples/operatorjson_pointer.cpp | 47 +++ doc/examples/operatorjson_pointer.link | 1 + doc/examples/operatorjson_pointer.output | 8 + doc/examples/operatorjson_pointer_const.cpp | 23 ++ doc/examples/operatorjson_pointer_const.link | 1 + .../operatorjson_pointer_const.output | 4 + doc/examples/unflatten.cpp | 28 ++ doc/examples/unflatten.link | 1 + doc/examples/unflatten.output | 22 + src/json.hpp | 386 +++++++++++++++--- src/json.hpp.re2c | 386 +++++++++++++++--- test/unit.cpp | 222 ++++++---- 15 files changed, 978 insertions(+), 202 deletions(-) create mode 100644 doc/examples/flatten.cpp create mode 100644 doc/examples/flatten.link create mode 100644 doc/examples/flatten.output create mode 100644 doc/examples/operatorjson_pointer.cpp create mode 100644 doc/examples/operatorjson_pointer.link create mode 100644 doc/examples/operatorjson_pointer.output create mode 100644 doc/examples/operatorjson_pointer_const.cpp create mode 100644 doc/examples/operatorjson_pointer_const.link create mode 100644 doc/examples/operatorjson_pointer_const.output create mode 100644 doc/examples/unflatten.cpp create mode 100644 doc/examples/unflatten.link create mode 100644 doc/examples/unflatten.output 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()); } } From f883a04c87dd3c3ccd3828172dfe9160376cda67 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 18:18:49 +0200 Subject: [PATCH 08/13] more documentation --- doc/examples/json_pointer.cpp | 46 ++++++++++++++++++++++++++++++++ doc/examples/json_pointer.link | 1 + doc/examples/json_pointer.output | 3 +++ src/json.hpp | 37 +++++++++++++++++-------- src/json.hpp.re2c | 37 +++++++++++++++++-------- 5 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 doc/examples/json_pointer.cpp create mode 100644 doc/examples/json_pointer.link create mode 100644 doc/examples/json_pointer.output diff --git a/doc/examples/json_pointer.cpp b/doc/examples/json_pointer.cpp new file mode 100644 index 00000000..140eac3b --- /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 00000000..c10c5fb9 --- /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 00000000..b81c8a20 --- /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/src/json.hpp b/src/json.hpp index 6cf369bd..c40e004a 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8913,11 +8913,26 @@ basic_json_parser_63: friend class basic_json; public: - /// empty reference token - json_pointer() = default; + /*! + @brief create JSON pointer - /// nonempty reference token - explicit json_pointer(const std::string& s) + 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 (`/`), or if a tilde (`~`) is not followed + by `0` (representing `~`) or `1` (representing `/`). + + @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)) {} @@ -8943,19 +8958,19 @@ basic_json_parser_63: { result = &result->operator[](reference_token); } - continue; + break; } case value_t::object: { result = &result->operator[](reference_token); - continue; + break; } case value_t::array: { result = &result->operator[](static_cast(std::stoi(reference_token))); - continue; + break; } default: @@ -9066,7 +9081,7 @@ basic_json_parser_63: case value_t::object: { ptr = &ptr->operator[](reference_token); - continue; + break; } case value_t::array: @@ -9078,7 +9093,7 @@ basic_json_parser_63: ") is out of range"); } ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); - continue; + break; } default: @@ -9100,7 +9115,7 @@ basic_json_parser_63: case value_t::object: { ptr = &ptr->at(reference_token); - continue; + break; } case value_t::array: @@ -9112,7 +9127,7 @@ basic_json_parser_63: ") is out of range"); } ptr = &ptr->at(static_cast(std::stoi(reference_token))); - continue; + break; } default: diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 7fe9673f..e2ea78b6 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8223,11 +8223,26 @@ class basic_json friend class basic_json; public: - /// empty reference token - json_pointer() = default; + /*! + @brief create JSON pointer - /// nonempty reference token - explicit json_pointer(const std::string& s) + 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 (`/`), or if a tilde (`~`) is not followed + by `0` (representing `~`) or `1` (representing `/`). + + @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)) {} @@ -8253,19 +8268,19 @@ class basic_json { result = &result->operator[](reference_token); } - continue; + break; } case value_t::object: { result = &result->operator[](reference_token); - continue; + break; } case value_t::array: { result = &result->operator[](static_cast(std::stoi(reference_token))); - continue; + break; } default: @@ -8376,7 +8391,7 @@ class basic_json case value_t::object: { ptr = &ptr->operator[](reference_token); - continue; + break; } case value_t::array: @@ -8388,7 +8403,7 @@ class basic_json ") is out of range"); } ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); - continue; + break; } default: @@ -8410,7 +8425,7 @@ class basic_json case value_t::object: { ptr = &ptr->at(reference_token); - continue; + break; } case value_t::array: @@ -8422,7 +8437,7 @@ class basic_json ") is out of range"); } ptr = &ptr->at(static_cast(std::stoi(reference_token))); - continue; + break; } default: From 0835eb293ffa80bab9591fe55656d57d76805f40 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 18:54:54 +0200 Subject: [PATCH 09/13] improved RFC compliance and code coverage --- src/json.hpp | 48 +++++++++++++++++++++++++++++++++++++++++++---- src/json.hpp.re2c | 48 +++++++++++++++++++++++++++++++++++++++++++---- test/unit.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index c40e004a..47046c03 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8992,7 +8992,9 @@ basic_json_parser_63: @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 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 { @@ -9002,18 +9004,27 @@ basic_json_parser_63: { 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; @@ -9037,6 +9048,7 @@ basic_json_parser_63: { case value_t::object: { + // note: at performs range check ptr = &ptr->at(reference_token); break; } @@ -9045,12 +9057,20 @@ basic_json_parser_63: { if (reference_token == "-") { - throw std::out_of_range("cannot resolve 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"); } - else + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') { - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + 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; } @@ -9080,6 +9100,7 @@ basic_json_parser_63: { case value_t::object: { + // use unchecked object access ptr = &ptr->operator[](reference_token); break; } @@ -9088,10 +9109,19 @@ basic_json_parser_63: { 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; } @@ -9114,6 +9144,7 @@ basic_json_parser_63: { case value_t::object: { + // note: at performs range check ptr = &ptr->at(reference_token); break; } @@ -9122,10 +9153,19 @@ basic_json_parser_63: { 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; } diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index e2ea78b6..ac11f08a 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8302,7 +8302,9 @@ class basic_json @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 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 { @@ -8312,18 +8314,27 @@ class basic_json { 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; @@ -8347,6 +8358,7 @@ class basic_json { case value_t::object: { + // note: at performs range check ptr = &ptr->at(reference_token); break; } @@ -8355,12 +8367,20 @@ class basic_json { if (reference_token == "-") { - throw std::out_of_range("cannot resolve 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"); } - else + + // error condition (cf. RFC 6901, Sect. 4) + if (reference_token.size() > 1 and reference_token[0] == '0') { - ptr = &ptr->at(static_cast(std::stoi(reference_token))); + 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; } @@ -8390,6 +8410,7 @@ class basic_json { case value_t::object: { + // use unchecked object access ptr = &ptr->operator[](reference_token); break; } @@ -8398,10 +8419,19 @@ class basic_json { 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; } @@ -8424,6 +8454,7 @@ class basic_json { case value_t::object: { + // note: at performs range check ptr = &ptr->at(reference_token); break; } @@ -8432,10 +8463,19 @@ class basic_json { 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; } diff --git a/test/unit.cpp b/test/unit.cpp index a3b9035d..f4d162e1 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12196,6 +12196,7 @@ TEST_CASE("JSON pointers") SECTION("nonconst access") { json j = {1, 2, 3}; + const json j_const = j; // check reading access CHECK(j["/0"_json_pointer] == j[0]); @@ -12214,9 +12215,32 @@ TEST_CASE("JSON pointers") 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") From 7034ae2486fa58a2cf31bfc86c42f4c07a6e7b23 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 19:12:12 +0200 Subject: [PATCH 10/13] improved test coverage --- test/unit.cpp | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test/unit.cpp b/test/unit.cpp index f4d162e1..f79a29fd 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12095,6 +12095,10 @@ TEST_CASE("JSON pointers") 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[""]); @@ -12106,6 +12110,14 @@ TEST_CASE("JSON pointers") 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"]); @@ -12115,6 +12127,13 @@ TEST_CASE("JSON pointers") 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") @@ -12144,6 +12163,10 @@ TEST_CASE("JSON pointers") 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[""]); @@ -12155,6 +12178,14 @@ TEST_CASE("JSON pointers") 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"]); @@ -12162,6 +12193,13 @@ TEST_CASE("JSON pointers") // 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") @@ -12319,6 +12357,14 @@ TEST_CASE("JSON pointers") // 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"); + // explicit roundtrip check CHECK(j.flatten().unflatten() == j); From 6268287940ce3a2523fda959ed1efe7d5f0051c4 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 22:08:21 +0200 Subject: [PATCH 11/13] improved documentation and test coverage --- README.md | 2 +- doc/examples/flatten.cpp | 3 ++ doc/examples/flatten.link | 2 +- doc/examples/flatten.output | 1 + src/json.hpp | 58 ++++++++++++++++++++++++++++++------- src/json.hpp.re2c | 58 ++++++++++++++++++++++++++++++------- test/unit.cpp | 5 ++++ 7 files changed, 105 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index d39663ee..2dd60bf1 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/flatten.cpp b/doc/examples/flatten.cpp index 5d769202..0601f8a3 100644 --- a/doc/examples/flatten.cpp +++ b/doc/examples/flatten.cpp @@ -31,4 +31,7 @@ int main() // 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 index 70ba78ba..0fe78bbb 100644 --- a/doc/examples/flatten.link +++ b/doc/examples/flatten.link @@ -1 +1 @@ -online \ No newline at end of file +online \ No newline at end of file diff --git a/doc/examples/flatten.output b/doc/examples/flatten.output index beb368fa..fedfc8ef 100644 --- a/doc/examples/flatten.output +++ b/doc/examples/flatten.output @@ -14,3 +14,4 @@ "/object/~1": "slash", "/pi": 3.141 } +{"":3.141} diff --git a/src/json.hpp b/src/json.hpp index 47046c03..030c8f2c 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -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()`. @@ -3620,7 +3620,9 @@ class basic_json @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 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} @@ -3645,8 +3647,9 @@ class basic_json @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 + @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} @@ -8923,9 +8926,12 @@ basic_json_parser_63: 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 (`/`), or if a tilde (`~`) is not followed - by `0` (representing `~`) or `1` (representing `/`). + @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} @@ -8944,6 +8950,8 @@ basic_json_parser_63: { 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) @@ -8952,10 +8960,12 @@ basic_json_parser_63: { 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; @@ -8963,19 +8973,38 @@ basic_json_parser_63: 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; } + /* + This function is only to be called from the unflatten() + function. There, j is initially of type null. + + - In case the reference tokens are empty, a reference to + j is returned and overwritten by the desired value by + the unflatten() function. + - If there are reference tokens, the null value of j will + be changed to an object or array after reading the first + reference token. + - All subsequent tokens work on arrays or objects and will + not change the type of j. + + Consequently, the type of @a j will always be null, + object, or array. Hence, the following line is + unreachable. + */ default: { - throw std::domain_error("unresolved reference token '" + reference_token + "'"); + break; // LCOV_EXCL_LINE } } } @@ -9361,7 +9390,11 @@ basic_json_parser_63: throw std::domain_error("values in object must be primitive"); } - // assign value to reference pointed to by JSON pointer + // 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; } @@ -9390,7 +9423,8 @@ basic_json_parser_63: @return an object that maps JSON pointers to primitve values - @note Empty objects and arrays are flattened to `null`. + @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. @@ -9428,6 +9462,8 @@ basic_json_parser_63: @complexity Linear in the size the JSON value. + @throws std::domain_error + @liveexample{The following code shows how a flattened JSON object is unflattened into the original nested JSON object.,unflatten} diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index ac11f08a..f5fbe65b 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -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()`. @@ -3620,7 +3620,9 @@ class basic_json @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 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} @@ -3645,8 +3647,9 @@ class basic_json @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 + @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} @@ -8233,9 +8236,12 @@ class basic_json 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 (`/`), or if a tilde (`~`) is not followed - by `0` (representing `~`) or `1` (representing `/`). + @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} @@ -8254,6 +8260,8 @@ class basic_json { 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) @@ -8262,10 +8270,12 @@ class basic_json { 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; @@ -8273,19 +8283,38 @@ class basic_json 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; } + /* + This function is only to be called from the unflatten() + function. There, j is initially of type null. + + - In case the reference tokens are empty, a reference to + j is returned and overwritten by the desired value by + the unflatten() function. + - If there are reference tokens, the null value of j will + be changed to an object or array after reading the first + reference token. + - All subsequent tokens work on arrays or objects and will + not change the type of j. + + Consequently, the type of @a j will always be null, + object, or array. Hence, the following line is + unreachable. + */ default: { - throw std::domain_error("unresolved reference token '" + reference_token + "'"); + break; // LCOV_EXCL_LINE } } } @@ -8671,7 +8700,11 @@ class basic_json throw std::domain_error("values in object must be primitive"); } - // assign value to reference pointed to by JSON pointer + // 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; } @@ -8700,7 +8733,8 @@ class basic_json @return an object that maps JSON pointers to primitve values - @note Empty objects and arrays are flattened to `null`. + @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. @@ -8738,6 +8772,8 @@ class basic_json @complexity Linear in the size the JSON value. + @throws std::domain_error + @liveexample{The following code shows how a flattened JSON object is unflattened into the original nested JSON object.,unflatten} diff --git a/test/unit.cpp b/test/unit.cpp index f79a29fd..f5a6fc09 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12365,6 +12365,11 @@ TEST_CASE("JSON pointers") 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(), "unresolved reference token 'foo'"); + // explicit roundtrip check CHECK(j.flatten().unflatten() == j); From 1dee40a9697dd4bc22ed6dd13ad5c928d44ff0f6 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 22:34:39 +0200 Subject: [PATCH 12/13] fixed test case --- src/json.hpp | 24 ++++++------------------ src/json.hpp.re2c | 24 ++++++------------------ test/unit.cpp | 2 +- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 030c8f2c..5fdba140 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -8986,25 +8986,15 @@ basic_json_parser_63: } /* - This function is only to be called from the unflatten() - function. There, j is initially of type null. - - - In case the reference tokens are empty, a reference to - j is returned and overwritten by the desired value by - the unflatten() function. - - If there are reference tokens, the null value of j will - be changed to an object or array after reading the first - reference token. - - All subsequent tokens work on arrays or objects and will - not change the type of j. - - Consequently, the type of @a j will always be null, - object, or array. Hence, the following line is - unreachable. + 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: { - break; // LCOV_EXCL_LINE + throw std::domain_error("invalid value to unflatten"); } } } @@ -9462,8 +9452,6 @@ basic_json_parser_63: @complexity Linear in the size the JSON value. - @throws std::domain_error - @liveexample{The following code shows how a flattened JSON object is unflattened into the original nested JSON object.,unflatten} diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index f5fbe65b..95a484f8 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8296,25 +8296,15 @@ class basic_json } /* - This function is only to be called from the unflatten() - function. There, j is initially of type null. - - - In case the reference tokens are empty, a reference to - j is returned and overwritten by the desired value by - the unflatten() function. - - If there are reference tokens, the null value of j will - be changed to an object or array after reading the first - reference token. - - All subsequent tokens work on arrays or objects and will - not change the type of j. - - Consequently, the type of @a j will always be null, - object, or array. Hence, the following line is - unreachable. + 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: { - break; // LCOV_EXCL_LINE + throw std::domain_error("invalid value to unflatten"); } } } @@ -8772,8 +8762,6 @@ class basic_json @complexity Linear in the size the JSON value. - @throws std::domain_error - @liveexample{The following code shows how a flattened JSON object is unflattened into the original nested JSON object.,unflatten} diff --git a/test/unit.cpp b/test/unit.cpp index f5a6fc09..2666e111 100644 --- a/test/unit.cpp +++ b/test/unit.cpp @@ -12368,7 +12368,7 @@ TEST_CASE("JSON pointers") // 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(), "unresolved reference token 'foo'"); + CHECK_THROWS_WITH(j_error.unflatten(), "invalid value to unflatten"); // explicit roundtrip check CHECK(j.flatten().unflatten() == j); From 08c97df4207282c280b252609d8d64f0dda80ed9 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 17 Apr 2016 23:18:07 +0200 Subject: [PATCH 13/13] added examples --- doc/examples/at_json_pointer.cpp | 35 +++++++++++++++++ doc/examples/at_json_pointer.link | 1 + doc/examples/at_json_pointer.output | 6 +++ doc/examples/at_json_pointer_const.cpp | 23 +++++++++++ doc/examples/at_json_pointer_const.link | 1 + doc/examples/at_json_pointer_const.output | 4 ++ src/json.hpp | 47 ++++++++++++++++++----- src/json.hpp.re2c | 47 ++++++++++++++++++----- 8 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 doc/examples/at_json_pointer.cpp create mode 100644 doc/examples/at_json_pointer.link create mode 100644 doc/examples/at_json_pointer.output create mode 100644 doc/examples/at_json_pointer_const.cpp create mode 100644 doc/examples/at_json_pointer_const.link create mode 100644 doc/examples/at_json_pointer_const.output diff --git a/doc/examples/at_json_pointer.cpp b/doc/examples/at_json_pointer.cpp new file mode 100644 index 00000000..0665e608 --- /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 00000000..5356294e --- /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 00000000..11913c72 --- /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 00000000..e3cfc515 --- /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 00000000..905e60d3 --- /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 00000000..7b9306bb --- /dev/null +++ b/doc/examples/at_json_pointer_const.output @@ -0,0 +1,4 @@ +1 +"foo" +[1,2] +2 diff --git a/src/json.hpp b/src/json.hpp index 5fdba140..ffa46067 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -3616,9 +3616,9 @@ class basic_json @param[in] ptr a JSON pointer - @return reference to the JSON value pointed to by @a ptr + @return reference to the element pointed to by @a ptr - @complexity Linear in the length of the JSON pointer. + @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' @@ -3641,18 +3641,17 @@ class basic_json value; no `null` values are created. In particular, the the special value `-` yields an exception. - @param[in] ptr a JSON pointer + @param[in] ptr JSON pointer to the desired element - @return reference to the JSON value pointed to by @a ptr + @return const reference to the element pointed to by @a ptr - @complexity Linear in the length of the JSON pointer. + @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} + @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} @since version 2.0.0 */ @@ -3664,9 +3663,20 @@ class basic_json /*! @brief access specified element via JSON Pointer - Returns a reference to the element at with specified JSON pointer @a ptr. + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. - @param ptr JSON pointer to the desired element + @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 */ @@ -3676,7 +3686,24 @@ class basic_json } /*! - @copydoc basic_json::at(const json_pointer&) + @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 { diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 95a484f8..ced7ffba 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -3616,9 +3616,9 @@ class basic_json @param[in] ptr a JSON pointer - @return reference to the JSON value pointed to by @a ptr + @return reference to the element pointed to by @a ptr - @complexity Linear in the length of the JSON pointer. + @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' @@ -3641,18 +3641,17 @@ class basic_json value; no `null` values are created. In particular, the the special value `-` yields an exception. - @param[in] ptr a JSON pointer + @param[in] ptr JSON pointer to the desired element - @return reference to the JSON value pointed to by @a ptr + @return const reference to the element pointed to by @a ptr - @complexity Linear in the length of the JSON pointer. + @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} + @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} @since version 2.0.0 */ @@ -3664,9 +3663,20 @@ class basic_json /*! @brief access specified element via JSON Pointer - Returns a reference to the element at with specified JSON pointer @a ptr. + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. - @param ptr JSON pointer to the desired element + @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 */ @@ -3676,7 +3686,24 @@ class basic_json } /*! - @copydoc basic_json::at(const json_pointer&) + @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 {