1
0
mirror of https://github.com/nlohmann/json.git synced 2025-08-07 18:02:57 +03:00

Re-template json_pointer on string type (#3415)

* Make exception context optional

Change exception context parameter to pointer and replace context with
nullptr where appropriate.

* Support escaping other string types

* Add string concatenation function

Add variadic concat() function for concatenating char *, char, and
string types.

* Replace string concatenations using + with concat()

* Template json_pointer on string type

Change json_pointer from being templated on basic_json to being
templated on string type.

* Add unit test for #3388

Closes #3388.

* Fix regression test for #2958

* Add backwards compatibility with json_pointer<basic_json>

* Update json_pointer docs

* Allow comparing different json_pointers

* Update version numbers
This commit is contained in:
Florian Albrechtskirchinger
2022-04-12 14:18:16 +02:00
committed by GitHub
parent 1deeb434c6
commit 616caea27a
28 changed files with 1638 additions and 784 deletions

View File

@@ -2,6 +2,8 @@
#include <algorithm> // all_of
#include <cctype> // isdigit
#include <cerrno> // errno, ERANGE
#include <cstdlib> // strtoull
#include <limits> // max
#include <numeric> // accumulate
#include <string> // string
@@ -10,6 +12,7 @@
#include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/string_concat.hpp>
#include <nlohmann/detail/string_escape.hpp>
#include <nlohmann/detail/value_t.hpp>
@@ -18,35 +21,53 @@ namespace nlohmann
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename BasicJsonType>
template<typename RefStringType>
class json_pointer
{
// allow basic_json to access private members
NLOHMANN_BASIC_JSON_TPL_DECLARATION
friend class basic_json;
template<typename>
friend class json_pointer;
template<typename T>
struct string_t_helper
{
using type = T;
};
NLOHMANN_BASIC_JSON_TPL_DECLARATION
struct string_t_helper<NLOHMANN_BASIC_JSON_TPL>
{
using type = StringType;
};
public:
// for backwards compatibility accept BasicJsonType
using string_t = typename string_t_helper<RefStringType>::type;
/// @brief create JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/json_pointer/
explicit json_pointer(const std::string& s = "")
explicit json_pointer(const string_t& s = "")
: reference_tokens(split(s))
{}
/// @brief return a string representation of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/to_string/
std::string to_string() const
string_t to_string() const
{
return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
std::string{},
[](const std::string & a, const std::string & b)
string_t{},
[](const string_t& a, const string_t& b)
{
return a + "/" + detail::escape(b);
return detail::concat(a, '/', detail::escape(b));
});
}
/// @brief return a string representation of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_string/
operator std::string() const
operator string_t() const
{
return to_string();
}
@@ -63,7 +84,7 @@ class json_pointer
/// @brief append an unescaped reference token at the end of this JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slasheq/
json_pointer& operator/=(std::string token)
json_pointer& operator/=(string_t token)
{
push_back(std::move(token));
return *this;
@@ -86,7 +107,7 @@ class json_pointer
/// @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer
/// @sa https://json.nlohmann.me/api/json_pointer/operator_slash/
friend json_pointer operator/(const json_pointer& lhs, std::string token) // NOLINT(performance-unnecessary-value-param)
friend json_pointer operator/(const json_pointer& lhs, string_t token) // NOLINT(performance-unnecessary-value-param)
{
return json_pointer(lhs) /= std::move(token);
}
@@ -118,7 +139,7 @@ class json_pointer
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType()));
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr));
}
reference_tokens.pop_back();
@@ -126,11 +147,11 @@ class json_pointer
/// @brief return last reference token
/// @sa https://json.nlohmann.me/api/json_pointer/back/
const std::string& back() const
const string_t& back() const
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType()));
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr));
}
return reference_tokens.back();
@@ -138,14 +159,14 @@ class json_pointer
/// @brief append an unescaped token at the end of the reference pointer
/// @sa https://json.nlohmann.me/api/json_pointer/push_back/
void push_back(const std::string& token)
void push_back(const string_t& token)
{
reference_tokens.push_back(token);
}
/// @brief append an unescaped token at the end of the reference pointer
/// @sa https://json.nlohmann.me/api/json_pointer/push_back/
void push_back(std::string&& token)
void push_back(string_t&& token)
{
reference_tokens.push_back(std::move(token));
}
@@ -168,44 +189,39 @@ class json_pointer
@throw out_of_range.404 if string @a s could not be converted to an integer
@throw out_of_range.410 if an array index exceeds size_type
*/
static typename BasicJsonType::size_type array_index(const std::string& s)
template<typename BasicJsonType>
static typename BasicJsonType::size_type array_index(const string_t& s)
{
using size_type = typename BasicJsonType::size_type;
// error condition (cf. RFC 6901, Sect. 4)
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0'))
{
JSON_THROW(detail::parse_error::create(106, 0, "array index '" + s + "' must not begin with '0'", BasicJsonType()));
JSON_THROW(detail::parse_error::create(106, 0, detail::concat("array index '", s, "' must not begin with '0'"), nullptr));
}
// error condition (cf. RFC 6901, Sect. 4)
if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9')))
{
JSON_THROW(detail::parse_error::create(109, 0, "array index '" + s + "' is not a number", BasicJsonType()));
JSON_THROW(detail::parse_error::create(109, 0, detail::concat("array index '", s, "' is not a number"), nullptr));
}
std::size_t processed_chars = 0;
unsigned long long res = 0; // NOLINT(runtime/int)
JSON_TRY
const char* p = s.c_str();
char* p_end = nullptr;
errno = 0; // strtoull doesn't reset errno
unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int)
if (p == p_end // invalid input or empty string
|| errno == ERANGE // out of range
|| JSON_HEDLEY_UNLIKELY(static_cast<std::size_t>(p_end - p) != s.size())) // incomplete read
{
res = std::stoull(s, &processed_chars);
}
JSON_CATCH(std::out_of_range&)
{
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'", BasicJsonType()));
}
// check if the string was completely read
if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size()))
{
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'", BasicJsonType()));
JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", s, "'"), nullptr));
}
// only triggered on special platforms (like 32bit), see also
// https://github.com/nlohmann/json/pull/2203
if (res >= static_cast<unsigned long long>((std::numeric_limits<size_type>::max)())) // NOLINT(runtime/int)
{
JSON_THROW(detail::out_of_range::create(410, "array index " + s + " exceeds size_type", BasicJsonType())); // LCOV_EXCL_LINE
JSON_THROW(detail::out_of_range::create(410, detail::concat("array index ", s, " exceeds size_type"), nullptr)); // LCOV_EXCL_LINE
}
return static_cast<size_type>(res);
@@ -216,7 +232,7 @@ class json_pointer
{
if (JSON_HEDLEY_UNLIKELY(empty()))
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType()));
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", nullptr));
}
json_pointer result = *this;
@@ -233,6 +249,7 @@ class json_pointer
@throw parse_error.109 if array index is not a number
@throw type_error.313 if value cannot be unflattened
*/
template<typename BasicJsonType>
BasicJsonType& get_and_create(BasicJsonType& j) const
{
auto* result = &j;
@@ -268,7 +285,7 @@ class json_pointer
case detail::value_t::array:
{
// create an entry in the array
result = &result->operator[](array_index(reference_token));
result = &result->operator[](array_index<BasicJsonType>(reference_token));
break;
}
@@ -286,7 +303,7 @@ class json_pointer
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", j));
JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j));
}
}
@@ -312,6 +329,7 @@ class json_pointer
@throw parse_error.109 if an array index was not a number
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
BasicJsonType& get_unchecked(BasicJsonType* ptr) const
{
for (const auto& reference_token : reference_tokens)
@@ -352,7 +370,7 @@ class json_pointer
else
{
// convert array index to number; unchecked access
ptr = &ptr->operator[](array_index(reference_token));
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
}
break;
}
@@ -366,7 +384,7 @@ class json_pointer
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr));
JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr));
}
}
@@ -379,6 +397,7 @@ class json_pointer
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
BasicJsonType& get_checked(BasicJsonType* ptr) const
{
for (const auto& reference_token : reference_tokens)
@@ -397,13 +416,13 @@ class json_pointer
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
JSON_THROW(detail::out_of_range::create(402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range", *ptr));
JSON_THROW(detail::out_of_range::create(402, detail::concat(
"array index '-' (", std::to_string(ptr->m_value.array->size()),
") is out of range"), ptr));
}
// note: at performs range check
ptr = &ptr->at(array_index(reference_token));
ptr = &ptr->at(array_index<BasicJsonType>(reference_token));
break;
}
@@ -416,7 +435,7 @@ class json_pointer
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr));
JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr));
}
}
@@ -436,6 +455,7 @@ class json_pointer
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const
{
for (const auto& reference_token : reference_tokens)
@@ -454,11 +474,11 @@ class json_pointer
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" cannot be used for const access
JSON_THROW(detail::out_of_range::create(402, "array index '-' (" + std::to_string(ptr->m_value.array->size()) + ") is out of range", *ptr));
JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_value.array->size()), ") is out of range"), ptr));
}
// use unchecked array access
ptr = &ptr->operator[](array_index(reference_token));
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
break;
}
@@ -471,7 +491,7 @@ class json_pointer
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr));
JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr));
}
}
@@ -484,6 +504,7 @@ class json_pointer
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
template<typename BasicJsonType>
const BasicJsonType& get_checked(const BasicJsonType* ptr) const
{
for (const auto& reference_token : reference_tokens)
@@ -502,13 +523,13 @@ class json_pointer
if (JSON_HEDLEY_UNLIKELY(reference_token == "-"))
{
// "-" always fails the range check
JSON_THROW(detail::out_of_range::create(402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range", *ptr));
JSON_THROW(detail::out_of_range::create(402, detail::concat(
"array index '-' (", std::to_string(ptr->m_value.array->size()),
") is out of range"), ptr));
}
// note: at performs range check
ptr = &ptr->at(array_index(reference_token));
ptr = &ptr->at(array_index<BasicJsonType>(reference_token));
break;
}
@@ -521,7 +542,7 @@ class json_pointer
case detail::value_t::binary:
case detail::value_t::discarded:
default:
JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr));
JSON_THROW(detail::out_of_range::create(404, detail::concat("unresolved reference token '", reference_token, "'"), ptr));
}
}
@@ -532,6 +553,7 @@ class json_pointer
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
*/
template<typename BasicJsonType>
bool contains(const BasicJsonType* ptr) const
{
for (const auto& reference_token : reference_tokens)
@@ -579,7 +601,7 @@ class json_pointer
}
}
const auto idx = array_index(reference_token);
const auto idx = array_index<BasicJsonType>(reference_token);
if (idx >= ptr->size())
{
// index out of range
@@ -620,9 +642,9 @@ class json_pointer
@throw parse_error.107 if the pointer is not empty or begins with '/'
@throw parse_error.108 if character '~' is not followed by '0' or '1'
*/
static std::vector<std::string> split(const std::string& reference_string)
static std::vector<string_t> split(const string_t& reference_string)
{
std::vector<std::string> result;
std::vector<string_t> result;
// special case: empty reference string -> no reference tokens
if (reference_string.empty())
@@ -633,7 +655,7 @@ class json_pointer
// check if nonempty reference string begins with slash
if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/'))
{
JSON_THROW(detail::parse_error::create(107, 1, "JSON pointer must be empty or begin with '/' - was: '" + reference_string + "'", BasicJsonType()));
JSON_THROW(detail::parse_error::create(107, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", reference_string, "'"), nullptr));
}
// extract the reference tokens:
@@ -644,11 +666,11 @@ class json_pointer
std::size_t slash = reference_string.find_first_of('/', 1),
// set the beginning of the first reference token
start = 1;
// we can stop if start == 0 (if slash == std::string::npos)
// we can stop if start == 0 (if slash == string_t::npos)
start != 0;
// set the beginning of the next reference token
// (will eventually be 0 if slash == std::string::npos)
start = (slash == std::string::npos) ? 0 : slash + 1,
// (will eventually be 0 if slash == string_t::npos)
start = (slash == string_t::npos) ? 0 : slash + 1,
// find next slash
slash = reference_string.find_first_of('/', start))
{
@@ -658,7 +680,7 @@ class json_pointer
// check reference tokens are properly escaped
for (std::size_t pos = reference_token.find_first_of('~');
pos != std::string::npos;
pos != string_t::npos;
pos = reference_token.find_first_of('~', pos + 1))
{
JSON_ASSERT(reference_token[pos] == '~');
@@ -668,7 +690,7 @@ class json_pointer
(reference_token[pos + 1] != '0' &&
reference_token[pos + 1] != '1')))
{
JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'", BasicJsonType()));
JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
}
}
@@ -688,7 +710,8 @@ class json_pointer
@note Empty objects or arrays are flattened to `null`.
*/
static void flatten(const std::string& reference_string,
template<typename BasicJsonType>
static void flatten(const string_t& reference_string,
const BasicJsonType& value,
BasicJsonType& result)
{
@@ -706,7 +729,7 @@ class json_pointer
// iterate array and use index as reference string
for (std::size_t i = 0; i < value.m_value.array->size(); ++i)
{
flatten(reference_string + "/" + std::to_string(i),
flatten(detail::concat(reference_string, '/', std::to_string(i)),
value.m_value.array->operator[](i), result);
}
}
@@ -725,7 +748,7 @@ class json_pointer
// iterate object and use keys as reference string
for (const auto& element : *value.m_value.object)
{
flatten(reference_string + "/" + detail::escape(element.first), element.second, result);
flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result);
}
}
break;
@@ -758,12 +781,13 @@ class json_pointer
@throw type_error.315 if object values are not primitive
@throw type_error.313 if value cannot be unflattened
*/
template<typename BasicJsonType>
static BasicJsonType
unflatten(const BasicJsonType& value)
{
if (JSON_HEDLEY_UNLIKELY(!value.is_object()))
{
JSON_THROW(detail::type_error::create(314, "only objects can be unflattened", value));
JSON_THROW(detail::type_error::create(314, "only objects can be unflattened", &value));
}
BasicJsonType result;
@@ -773,7 +797,7 @@ class json_pointer
{
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
{
JSON_THROW(detail::type_error::create(315, "values in object must be primitive", element.second));
JSON_THROW(detail::type_error::create(315, "values in object must be primitive", &element.second));
}
// assign value to reference pointed to by JSON pointer; Note that if
@@ -786,6 +810,21 @@ class json_pointer
return result;
}
// can't use conversion operator because of ambiguity
json_pointer<string_t> convert() const&
{
json_pointer<string_t> result;
result.reference_tokens = reference_tokens;
return result;
}
json_pointer<string_t> convert()&&
{
json_pointer<string_t> result;
result.reference_tokens = std::move(reference_tokens);
return result;
}
/*!
@brief compares two JSON pointers for equality
@@ -797,11 +836,10 @@ class json_pointer
@exceptionsafety No-throw guarantee: this function never throws exceptions.
*/
friend bool operator==(json_pointer const& lhs,
json_pointer const& rhs) noexcept
{
return lhs.reference_tokens == rhs.reference_tokens;
}
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
// NOLINTNEXTLINE(readability-redundant-declaration)
friend bool operator==(json_pointer<RefStringTypeLhs> const& lhs,
json_pointer<RefStringTypeRhs> const& rhs) noexcept;
/*!
@brief compares two JSON pointers for inequality
@@ -814,13 +852,27 @@ class json_pointer
@exceptionsafety No-throw guarantee: this function never throws exceptions.
*/
friend bool operator!=(json_pointer const& lhs,
json_pointer const& rhs) noexcept
{
return !(lhs == rhs);
}
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
// NOLINTNEXTLINE(readability-redundant-declaration)
friend bool operator!=(json_pointer<RefStringTypeLhs> const& lhs,
json_pointer<RefStringTypeRhs> const& rhs) noexcept;
/// the reference tokens
std::vector<std::string> reference_tokens;
std::vector<string_t> reference_tokens;
};
// functions cannot be defined inside class due to ODR violations
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
inline bool operator==(json_pointer<RefStringTypeLhs> const& lhs,
json_pointer<RefStringTypeRhs> const& rhs) noexcept
{
return lhs.reference_tokens == rhs.reference_tokens;
}
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
inline bool operator!=(json_pointer<RefStringTypeLhs> const& lhs,
json_pointer<RefStringTypeRhs> const& rhs) noexcept
{
return !(lhs == rhs);
}
} // namespace nlohmann