From 2fa8ea0f747538091bafb311cd3c843778b15817 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 8 Oct 2016 14:27:28 +0200 Subject: [PATCH 1/5] started fixing #323 --- doc/examples/operatorjson_pointer.cpp | 2 +- src/json.hpp | 49 +++++++++++++++++++++++---- src/json.hpp.re2c | 49 +++++++++++++++++++++++---- test/src/unit-json_pointer.cpp | 9 +++-- test/src/unit-regression.cpp | 7 ++++ 5 files changed, 101 insertions(+), 15 deletions(-) diff --git a/doc/examples/operatorjson_pointer.cpp b/doc/examples/operatorjson_pointer.cpp index 18e41c1f..d7e8faff 100644 --- a/doc/examples/operatorjson_pointer.cpp +++ b/doc/examples/operatorjson_pointer.cpp @@ -40,7 +40,7 @@ int main() // output the changed array std::cout << j["array"] << '\n'; - // "change" the arry element past the end + // "change" the array element past the end j["/array/-"_json_pointer] = 55; // output the changed array std::cout << j["array"] << '\n'; diff --git a/src/json.hpp b/src/json.hpp index 04c8573f..1cbcedf6 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9436,6 +9436,12 @@ basic_json_parser_63: /*! @brief return a reference to the pointed to value + @note This version does not throw if a value is not present, but tries + to create nested values instead. For instance, calling this function + with pointer `"/this/that"` on a null value is equivalent to calling + `operator[]("this").operator[]("that")` on that value, effectively + changing the null value to an object. + @param[in] ptr a JSON value @return reference to the JSON value pointed to by the JSON pointer @@ -9450,6 +9456,12 @@ basic_json_parser_63: { for (const auto& reference_token : reference_tokens) { + // 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'"); + } + switch (ptr->m_type) { case value_t::object: @@ -9461,12 +9473,6 @@ basic_json_parser_63: 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 @@ -9480,6 +9486,37 @@ basic_json_parser_63: break; } + // null values are converted to arrays or objects + case value_t::null: + { + // check if reference token is a number + const bool nums = std::all_of(reference_token.begin(), + reference_token.end(), + [](const char x) + { + return std::isdigit(x); + }); + + if (nums) + { + // if reference token consists solely of numbers + // use it as array index -> create array + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + else if (reference_token == "-") + { + // explicityly treat "-" as index beyond the end + // which is 0 for an empty array -> create array + ptr = &ptr->operator[](0); + } + else + { + // treat reference token as key -> create object + ptr = &ptr->operator[](reference_token); + } + break; + } + default: { throw std::out_of_range("unresolved reference token '" + reference_token + "'"); diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 48224835..2dabc031 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8733,6 +8733,12 @@ class basic_json /*! @brief return a reference to the pointed to value + @note This version does not throw if a value is not present, but tries + to create nested values instead. For instance, calling this function + with pointer `"/this/that"` on a null value is equivalent to calling + `operator[]("this").operator[]("that")` on that value, effectively + changing the null value to an object. + @param[in] ptr a JSON value @return reference to the JSON value pointed to by the JSON pointer @@ -8747,6 +8753,12 @@ class basic_json { for (const auto& reference_token : reference_tokens) { + // 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'"); + } + switch (ptr->m_type) { case value_t::object: @@ -8758,12 +8770,6 @@ class basic_json 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 @@ -8777,6 +8783,37 @@ class basic_json break; } + // null values are converted to arrays or objects + case value_t::null: + { + // check if reference token is a number + const bool nums = std::all_of(reference_token.begin(), + reference_token.end(), + [](const char x) + { + return std::isdigit(x); + }); + + if (nums) + { + // if reference token consists solely of numbers + // use it as array index -> create array + ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); + } + else if (reference_token == "-") + { + // explicityly treat "-" as index beyond the end + // which is 0 for an empty array -> create array + ptr = &ptr->operator[](0); + } + else + { + // treat reference token as key -> create object + ptr = &ptr->operator[](reference_token); + } + break; + } + default: { throw std::out_of_range("unresolved reference token '" + reference_token + "'"); diff --git a/test/src/unit-json_pointer.cpp b/test/src/unit-json_pointer.cpp index c7f1708c..892a958f 100644 --- a/test/src/unit-json_pointer.cpp +++ b/test/src/unit-json_pointer.cpp @@ -109,8 +109,13 @@ TEST_CASE("JSON pointers") CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]); // unescaped access - CHECK_THROWS_AS(j[json::json_pointer("/a/b")], std::out_of_range); - CHECK_THROWS_WITH(j[json::json_pointer("/a/b")], "unresolved reference token 'b'"); + // access to nonexisting values yield object creation + CHECK_NOTHROW(j[json::json_pointer("/a/b")] = 42); + CHECK(j["a"]["b"] == json(42)); + CHECK_NOTHROW(j[json::json_pointer("/a/c/1")] = 42); + CHECK(j["a"]["c"] == json({nullptr, 42})); + CHECK_NOTHROW(j[json::json_pointer("/a/d/-")] = 42); + CHECK(j["a"]["d"] == json::array({42})); // "/a/b" works for JSON {"a": {"b": 42}} CHECK(json({{"a", {{"b", 42}}}})[json::json_pointer("/a/b")] == json(42)); diff --git a/test/src/unit-regression.cpp b/test/src/unit-regression.cpp index 34eb197f..6d93e744 100644 --- a/test/src/unit-regression.cpp +++ b/test/src/unit-regression.cpp @@ -482,4 +482,11 @@ TEST_CASE("regression tests") CHECK_NOTHROW(j << f); } } + + SECTION("issue #323 - add nested object capabilities to pointers") + { + json j; + j["/this/that"_json_pointer] = 27; + CHECK(j == json({{"this", {{"that", 27}}}})); + } } From 54bf5f20e99c2912e7102e6e9955cdccf00d2c57 Mon Sep 17 00:00:00 2001 From: Niels Date: Sat, 8 Oct 2016 14:33:10 +0200 Subject: [PATCH 2/5] adjusted README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 962e9dda..bc156057 100644 --- a/README.md +++ b/README.md @@ -515,17 +515,17 @@ To compile and run the tests, you need to execute $ make check =============================================================================== -All tests passed (8905161 assertions in 35 test cases) +All tests passed (8905166 assertions in 35 test cases) ``` Alternatively, you can use [https://cmake.org](CMake) and run ```sh -mkdir build -cd build -cmake .. -make -ctest +$ mkdir build +$ cd build +$ cmake .. +$ make +$ ctest ``` For more information, have a look at the file [.travis.yml](https://github.com/nlohmann/json/blob/master/.travis.yml). From 97280bbcfc6038c5e135d6822d92f71ce0e9bde3 Mon Sep 17 00:00:00 2001 From: Niels Date: Sun, 9 Oct 2016 16:32:01 +0200 Subject: [PATCH 3/5] added missing header --- src/json.hpp | 1 + src/json.hpp.re2c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/json.hpp b/src/json.hpp index 1cbcedf6..0e8eece3 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -32,6 +32,7 @@ SOFTWARE. #include #include #include +#include #include #include #include diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 2dabc031..757f8859 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -32,6 +32,7 @@ SOFTWARE. #include #include #include +#include #include #include #include From d3e7f9da6797596bae944c8c07ef29398cba3668 Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 10 Oct 2016 20:38:50 +0200 Subject: [PATCH 4/5] code cleanup for #323 --- src/json.hpp | 61 +++++++++++++++++++++-------------------------- src/json.hpp.re2c | 61 +++++++++++++++++++++-------------------------- 2 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/json.hpp b/src/json.hpp index 0e8eece3..78cc7093 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -9457,10 +9457,27 @@ basic_json_parser_63: { for (const auto& reference_token : reference_tokens) { - // error condition (cf. RFC 6901, Sect. 4) - if (reference_token.size() > 1 and reference_token[0] == '0') + // convert null values to arrays or objects before continuing + if (ptr->m_type == value_t::null) { - throw std::domain_error("array index must not begin with '0'"); + // check if reference token is a number + const bool nums = std::all_of(reference_token.begin(), + reference_token.end(), + [](const char x) + { + return std::isdigit(x); + }); + + // change value to array for numbers or "-" or to object + // otherwise + if (nums or reference_token == "-") + { + *ptr = value_t::array; + } + else + { + *ptr = value_t::object; + } } switch (ptr->m_type) @@ -9474,6 +9491,13 @@ basic_json_parser_63: 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 @@ -9487,37 +9511,6 @@ basic_json_parser_63: break; } - // null values are converted to arrays or objects - case value_t::null: - { - // check if reference token is a number - const bool nums = std::all_of(reference_token.begin(), - reference_token.end(), - [](const char x) - { - return std::isdigit(x); - }); - - if (nums) - { - // if reference token consists solely of numbers - // use it as array index -> create array - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); - } - else if (reference_token == "-") - { - // explicityly treat "-" as index beyond the end - // which is 0 for an empty array -> create array - ptr = &ptr->operator[](0); - } - else - { - // treat reference token as key -> create object - ptr = &ptr->operator[](reference_token); - } - break; - } - default: { throw std::out_of_range("unresolved reference token '" + reference_token + "'"); diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c index 757f8859..61196844 100644 --- a/src/json.hpp.re2c +++ b/src/json.hpp.re2c @@ -8754,10 +8754,27 @@ class basic_json { for (const auto& reference_token : reference_tokens) { - // error condition (cf. RFC 6901, Sect. 4) - if (reference_token.size() > 1 and reference_token[0] == '0') + // convert null values to arrays or objects before continuing + if (ptr->m_type == value_t::null) { - throw std::domain_error("array index must not begin with '0'"); + // check if reference token is a number + const bool nums = std::all_of(reference_token.begin(), + reference_token.end(), + [](const char x) + { + return std::isdigit(x); + }); + + // change value to array for numbers or "-" or to object + // otherwise + if (nums or reference_token == "-") + { + *ptr = value_t::array; + } + else + { + *ptr = value_t::object; + } } switch (ptr->m_type) @@ -8771,6 +8788,13 @@ class basic_json 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 @@ -8784,37 +8808,6 @@ class basic_json break; } - // null values are converted to arrays or objects - case value_t::null: - { - // check if reference token is a number - const bool nums = std::all_of(reference_token.begin(), - reference_token.end(), - [](const char x) - { - return std::isdigit(x); - }); - - if (nums) - { - // if reference token consists solely of numbers - // use it as array index -> create array - ptr = &ptr->operator[](static_cast(std::stoi(reference_token))); - } - else if (reference_token == "-") - { - // explicityly treat "-" as index beyond the end - // which is 0 for an empty array -> create array - ptr = &ptr->operator[](0); - } - else - { - // treat reference token as key -> create object - ptr = &ptr->operator[](reference_token); - } - break; - } - default: { throw std::out_of_range("unresolved reference token '" + reference_token + "'"); From 470197bd0bfda213ad772b64d1af1b3e70c99380 Mon Sep 17 00:00:00 2001 From: Niels Date: Mon, 10 Oct 2016 21:58:31 +0200 Subject: [PATCH 5/5] improve test coverage --- test/src/unit-deserialization.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/src/unit-deserialization.cpp b/test/src/unit-deserialization.cpp index 672f15df..11a2c7a2 100644 --- a/test/src/unit-deserialization.cpp +++ b/test/src/unit-deserialization.cpp @@ -238,6 +238,18 @@ TEST_CASE("deserialization") uint8_t v[] = {'\"', 'a', 'a', 'a', 'a', 'a', 'a', '\\', 'u', '1'}; CHECK_THROWS_AS(json::parse(std::begin(v), std::end(v)), std::invalid_argument); } + + SECTION("case 3") + { + uint8_t v[] = {'\"', 'a', 'a', 'a', 'a', 'a', 'a', '\\', 'u', '1', '1', '1', '1', '1', '1', '1', '1'}; + CHECK_THROWS_AS(json::parse(std::begin(v), std::end(v)), std::invalid_argument); + } + + SECTION("case 4") + { + uint8_t v[] = {'\"', 'a', 'a', 'a', 'a', 'a', 'a', 'u', '1', '1', '1', '1', '1', '1', '1', '1', '\\'}; + CHECK_THROWS_AS(json::parse(std::begin(v), std::end(v)), std::invalid_argument); + } } } }