move json_pointer outside of basic_json

parent fe086d74
...@@ -748,8 +748,7 @@ struct is_basic_json_nested_type ...@@ -748,8 +748,7 @@ struct is_basic_json_nested_type
static auto constexpr value = std::is_same<T, typename BasicJsonType::iterator>::value or static auto constexpr value = std::is_same<T, typename BasicJsonType::iterator>::value or
std::is_same<T, typename BasicJsonType::const_iterator>::value or std::is_same<T, typename BasicJsonType::const_iterator>::value or
std::is_same<T, typename BasicJsonType::reverse_iterator>::value or std::is_same<T, typename BasicJsonType::reverse_iterator>::value or
std::is_same<T, typename BasicJsonType::const_reverse_iterator>::value or std::is_same<T, typename BasicJsonType::const_reverse_iterator>::value;
std::is_same<T, typename BasicJsonType::json_pointer>::value;
}; };
template<class BasicJsonType, class CompatibleArrayType> template<class BasicJsonType, class CompatibleArrayType>
...@@ -1320,6 +1319,335 @@ struct adl_serializer ...@@ -1320,6 +1319,335 @@ struct adl_serializer
} }
}; };
/*!
@brief JSON Pointer
A JSON pointer defines a string syntax for identifying a specific value
within a JSON document. It can be used with functions `at` and
`operator[]`. Furthermore, JSON pointers are the base for JSON patches.
@sa [RFC 6901](https://tools.ietf.org/html/rfc6901)
@since version 2.0.0
*/
class json_pointer
{
/// allow basic_json to access private members
NLOHMANN_BASIC_JSON_TPL_DECLARATION
friend class basic_json;
public:
/*!
@brief create JSON pointer
Create a JSON pointer according to the syntax described in
[Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3).
@param[in] s string representing the JSON pointer; if omitted, the
empty string is assumed which references the whole JSON
value
@throw parse_error.107 if the given JSON pointer @a s is nonempty and
does not begin with a slash (`/`); see example below
@throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s
is not followed by `0` (representing `~`) or `1` (representing `/`);
see example below
@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)) {}
/*!
@brief return a string representation of the JSON pointer
@invariant For each JSON pointer `ptr`, it holds:
@code {.cpp}
ptr == json_pointer(ptr.to_string());
@endcode
@return a string representation of the JSON pointer
@liveexample{The example shows the result of `to_string`.,
json_pointer__to_string}
@since version 2.0.0
*/
std::string to_string() const noexcept
{
return std::accumulate(reference_tokens.begin(), reference_tokens.end(),
std::string{},
[](const std::string & a, const std::string & b)
{
return a + "/" + escape(b);
});
}
/// @copydoc to_string()
operator std::string() const
{
return to_string();
}
private:
/*!
@brief remove and return last reference pointer
@throw out_of_range.405 if JSON pointer has no parent
*/
std::string pop_back()
{
if (is_root())
{
JSON_THROW(
detail::out_of_range::create(405, "JSON pointer has no parent"));
}
auto last = reference_tokens.back();
reference_tokens.pop_back();
return last;
}
/// return whether pointer points to the root document
bool is_root() const
{
return reference_tokens.empty();
}
json_pointer top() const
{
if (is_root())
{
JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent"));
}
json_pointer result = *this;
result.reference_tokens = {reference_tokens[0]};
return result;
}
/*!
@brief create and return a reference to the pointed to value
@complexity Linear in the number of reference tokens.
@throw parse_error.109 if array index is not a number
@throw type_error.313 if value cannot be unflattened
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
NLOHMANN_BASIC_JSON_TPL& get_and_create(NLOHMANN_BASIC_JSON_TPL& j) const;
/*!
@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
@complexity Linear in the length of the 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
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
NLOHMANN_BASIC_JSON_TPL& get_unchecked(NLOHMANN_BASIC_JSON_TPL* ptr) const;
/*!
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
NLOHMANN_BASIC_JSON_TPL& get_checked(NLOHMANN_BASIC_JSON_TPL* ptr) const;
/*!
@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
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
const NLOHMANN_BASIC_JSON_TPL& get_unchecked(const NLOHMANN_BASIC_JSON_TPL* ptr) const;
/*!
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
const NLOHMANN_BASIC_JSON_TPL& get_checked(const NLOHMANN_BASIC_JSON_TPL* ptr) const;
/*!
@brief split the string input to reference tokens
@note This function is only called by the json_pointer constructor.
All exceptions below are documented there.
@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)
{
std::vector<std::string> result;
// special case: empty reference string -> no reference tokens
if (reference_string.empty())
{
return result;
}
// check if nonempty reference string begins with slash
if (reference_string[0] != '/')
{
JSON_THROW(detail::parse_error::create(
107, 1,
"JSON pointer must be empty or begin with '/' - was: '" +
reference_string + "'"));
}
// extract the reference tokens:
// - slash: position of the last read slash (or end of string)
// - start: position after the previous slash
for (
// search for the first slash after the first character
size_t slash = reference_string.find_first_of('/', 1),
// set the beginning of the first reference token
start = 1;
// we can stop if start == string::npos+1 = 0
start != 0;
// set the beginning of the next reference token
// (will eventually be 0 if slash == std::string::npos)
start = slash + 1,
// find next slash
slash = reference_string.find_first_of('/', start))
{
// use the text between the beginning of the reference token
// (start) and the last slash (slash).
auto reference_token = reference_string.substr(start, slash - start);
// check reference tokens are properly escaped
for (size_t pos = reference_token.find_first_of('~');
pos != std::string::npos;
pos = reference_token.find_first_of('~', pos + 1))
{
assert(reference_token[pos] == '~');
// ~ must be followed by 0 or 1
if (pos == reference_token.size() - 1 or
(reference_token[pos + 1] != '0' and
reference_token[pos + 1] != '1'))
{
JSON_THROW(detail::parse_error::create(
108, 0, "escape character '~' must be followed with '0' or '1'"));
}
}
// finally, store the reference token
unescape(reference_token);
result.push_back(reference_token);
}
return result;
}
/*!
@brief replace all occurrences of a substring by another string
@param[in,out] s the string to manipulate; changed so that all
occurrences of @a f are replaced with @a t
@param[in] f the substring to replace with @a t
@param[in] t the string to replace @a f
@pre The search string @a f must not be empty. **This precondition is
enforced with an assertion.**
@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
)
;
}
/// escape tilde and slash
static std::string escape(std::string s)
{
// escape "~"" to "~0" and "/" to "~1"
replace_substring(s, "~", "~0");
replace_substring(s, "/", "~1");
return s;
}
/// unescape tilde and slash
static void unescape(std::string& s)
{
// first transform any occurrence of the sequence '~1' to '/'
replace_substring(s, "~1", "/");
// then transform any occurrence of the sequence '~0' to '~'
replace_substring(s, "~0", "~");
}
/*!
@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`.
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
static void flatten(const std::string& reference_string,
const NLOHMANN_BASIC_JSON_TPL& value,
NLOHMANN_BASIC_JSON_TPL& result);
/*!
@param[in] value flattened JSON
@return unflattened JSON
@throw parse_error.109 if array index is not a number
@throw type_error.314 if value is not an object
@throw type_error.315 if object values are not primitive
@throw type_error.313 if value cannot be unflattened
*/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
static NLOHMANN_BASIC_JSON_TPL
unflatten(const NLOHMANN_BASIC_JSON_TPL& value);
friend bool operator==(json_pointer const& lhs,
json_pointer const& rhs) noexcept;
friend bool operator!=(json_pointer const& lhs,
json_pointer const& rhs) noexcept;
/// the reference tokens
std::vector<std::string> reference_tokens;
};
/*! /*!
@brief a class to store JSON values @brief a class to store JSON values
...@@ -1409,13 +1737,14 @@ class basic_json ...@@ -1409,13 +1737,14 @@ class basic_json
template<detail::value_t> friend struct detail::external_constructor; template<detail::value_t> friend struct detail::external_constructor;
/// workaround type for MSVC /// workaround type for MSVC
using basic_json_t = NLOHMANN_BASIC_JSON_TPL; using basic_json_t = NLOHMANN_BASIC_JSON_TPL;
friend ::nlohmann::json_pointer;
public: public:
using value_t = detail::value_t; using value_t = detail::value_t;
// forward declarations // forward declarations
template<typename U> class iter_impl; template<typename U> class iter_impl;
template<typename Base> class json_reverse_iterator; template<typename Base> class json_reverse_iterator;
class json_pointer; using json_pointer = ::nlohmann::json_pointer;
template<typename T, typename SFINAE> template<typename T, typename SFINAE>
using json_serializer = JSONSerializer<T, SFINAE>; using json_serializer = JSONSerializer<T, SFINAE>;
...@@ -13031,617 +13360,367 @@ scan_number_done: ...@@ -13031,617 +13360,367 @@ scan_number_done:
}; };
public: public:
/*! //////////////////////////
@brief JSON Pointer // JSON Pointer support //
//////////////////////////
A JSON pointer defines a string syntax for identifying a specific value
within a JSON document. It can be used with functions `at` and
`operator[]`. Furthermore, JSON pointers are the base for JSON patches.
@sa [RFC 6901](https://tools.ietf.org/html/rfc6901)
@since version 2.0.0
*/
class json_pointer
{
/// allow basic_json to access private members
friend class basic_json;
public:
/*!
@brief create JSON pointer
Create a JSON pointer according to the syntax described in
[Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3).
@param[in] s string representing the JSON pointer; if omitted, the
empty string is assumed which references the whole JSON
value
@throw parse_error.107 if the given JSON pointer @a s is nonempty and
does not begin with a slash (`/`); see example below
@throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s
is not followed by `0` (representing `~`) or `1` (representing `/`);
see example below
@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))
{}
/*!
@brief return a string representation of the JSON pointer
@invariant For each JSON pointer `ptr`, it holds:
@code {.cpp}
ptr == json_pointer(ptr.to_string());
@endcode
@return a string representation of the JSON pointer
@liveexample{The example shows the result of `to_string`.,
json_pointer__to_string}
@since version 2.0.0
*/
std::string to_string() const noexcept
{
return std::accumulate(reference_tokens.begin(),
reference_tokens.end(), std::string{},
[](const std::string & a, const std::string & b)
{
return a + "/" + escape(b);
});
}
/// @copydoc to_string()
operator std::string() const
{
return to_string();
}
private:
/*!
@brief remove and return last reference pointer
@throw out_of_range.405 if JSON pointer has no parent
*/
std::string pop_back()
{
if (is_root())
{
JSON_THROW(out_of_range::create(405, "JSON pointer has no parent"));
}
auto last = reference_tokens.back();
reference_tokens.pop_back();
return last;
}
/// return whether pointer points to the root document
bool is_root() const
{
return reference_tokens.empty();
}
json_pointer top() const
{
if (is_root())
{
JSON_THROW(out_of_range::create(405, "JSON pointer has no parent"));
}
json_pointer result = *this; /// @name JSON Pointer functions
result.reference_tokens = {reference_tokens[0]}; /// @{
return result;
}
/*! /*!
@brief create and return a reference to the pointed to value @brief access specified element via JSON Pointer
@complexity Linear in the number of reference tokens.
@throw parse_error.109 if array index is not a number
@throw type_error.313 if value cannot be unflattened
*/
reference get_and_create(reference j) const
{
pointer result = &j;
// in case no reference tokens exist, return a reference to the
// JSON value j which will be overwritten by a primitive value
for (const auto& reference_token : reference_tokens)
{
switch (result->m_type)
{
case value_t::null:
{
if (reference_token == "0")
{
// start a new array if reference token is 0
result = &result->operator[](0);
}
else
{
// start a new object otherwise
result = &result->operator[](reference_token);
}
break;
}
case value_t::object:
{
// create an entry in the object
result = &result->operator[](reference_token);
break;
}
case value_t::array:
{
// create an entry in the array
JSON_TRY
{
result = &result->operator[](static_cast<size_type>(std::stoi(reference_token)));
}
JSON_CATCH (std::invalid_argument&)
{
JSON_THROW(parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
}
break;
}
/*
The following code is only reached if there exists a
reference token _and_ the current value is primitive. In
this case, we have an error situation, because primitive
values may only occur as single value; that is, with an
empty list of reference tokens.
*/
default:
{
JSON_THROW(type_error::create(313, "invalid value to unflatten"));
}
}
}
return *result;
}
/*! Uses a JSON pointer to retrieve a reference to the respective JSON value.
@brief return a reference to the pointed to 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.
@note This version does not throw if a value is not present, but tries In particular:
to create nested values instead. For instance, calling this function - If the JSON pointer points to an object key that does not exist, it
with pointer `"/this/that"` on a null value is equivalent to calling is created an filled with a `null` value before a reference to it
`operator[]("this").operator[]("that")` on that value, effectively is returned.
changing the null value to an object. - 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 value @param[in] ptr a JSON pointer
@return reference to the JSON value pointed to by the JSON pointer @return reference to the element pointed to by @a ptr
@complexity Linear in the length of the JSON pointer. @complexity Constant.
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @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 @throw out_of_range.404 if the JSON pointer can not be resolved
*/
reference get_unchecked(pointer ptr) const
{
for (const auto& reference_token : reference_tokens)
{
// convert null values to arrays or objects before continuing
if (ptr->m_type == 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 (x >= '0' and x <= '9');
});
// change value to array for numbers or "-" or to object @liveexample{The behavior is shown in the example.,operatorjson_pointer}
// otherwise
if (nums or reference_token == "-")
{
*ptr = value_t::array;
}
else
{
*ptr = value_t::object;
}
}
switch (ptr->m_type) @since version 2.0.0
{ */
case value_t::object: reference operator[](const json_pointer& ptr)
{ {
// use unchecked object access return ptr.get_unchecked(this);
ptr = &ptr->operator[](reference_token);
break;
} }
case value_t::array: /*!
{ @brief access specified element via JSON Pointer
// error condition (cf. RFC 6901, Sect. 4)
if (reference_token.size() > 1 and reference_token[0] == '0')
{
JSON_THROW(parse_error::create(106, 0, "array index '" + reference_token + "' must not begin with '0'"));
}
if (reference_token == "-") 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
// explicitly treat "-" as index beyond the end value; no `null` values are created. In particular, the the special value
ptr = &ptr->operator[](ptr->m_value.array->size()); `-` yields an exception.
}
else
{
// convert array index to number; unchecked access
JSON_TRY
{
ptr = &ptr->operator[](static_cast<size_type>(std::stoi(reference_token)));
}
JSON_CATCH (std::invalid_argument&)
{
JSON_THROW(parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
}
}
break;
}
default: @param[in] ptr JSON pointer to the desired element
{
JSON_THROW(out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
}
return *ptr; @return const reference to the element pointed to by @a ptr
}
@complexity Constant.
/*!
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used @throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved @throw out_of_range.404 if the JSON pointer can not be resolved
@liveexample{The behavior is shown in the example.,operatorjson_pointer_const}
@since version 2.0.0
*/ */
reference get_checked(pointer ptr) const const_reference operator[](const json_pointer& ptr) const
{
for (const auto& reference_token : reference_tokens)
{
switch (ptr->m_type)
{
case value_t::object:
{ {
// note: at performs range check return ptr.get_unchecked(this);
ptr = &ptr->at(reference_token);
break;
} }
case value_t::array: /*!
{ @brief access specified element via JSON Pointer
if (reference_token == "-")
{
// "-" always fails the range check
JSON_THROW(out_of_range::create(402, "array index '-' (" +
std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// error condition (cf. RFC 6901, Sect. 4) Returns a reference to the element at with specified JSON pointer @a ptr,
if (reference_token.size() > 1 and reference_token[0] == '0') with bounds checking.
{
JSON_THROW(parse_error::create(106, 0, "array index '" + reference_token + "' must not begin with '0'"));
}
// note: at performs range check @param[in] ptr JSON pointer to the desired element
JSON_TRY
{
ptr = &ptr->at(static_cast<size_type>(std::stoi(reference_token)));
}
JSON_CATCH (std::invalid_argument&)
{
JSON_THROW(parse_error::create(109, 0, "array index '" + reference_token + "' is not a number"));
}
break;
}
default: @return reference to the element pointed to by @a ptr
{
JSON_THROW(out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
}
return *ptr; @throw parse_error.106 if an array index in the passed JSON pointer @a ptr
} begins with '0'. See example below.
/*! @throw parse_error.109 if an array index in the passed JSON pointer @a ptr
@brief return a const reference to the pointed to value is not a number. See example below.
@param[in] ptr a JSON value @throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
is out of range. See example below.
@return const reference to the JSON value pointed to by the JSON @throw out_of_range.402 if the array index '-' is used in the passed JSON
pointer pointer @a ptr. As `at` provides checked access (and no elements are
implicitly inserted), the index '-' is always invalid. See example below.
@throw parse_error.106 if an array index begins with '0' @throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
@throw parse_error.109 if an array index was not a number See example below.
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved @exceptionsafety Strong guarantee: if an exception is thrown, there are no
changes in the JSON value.
@complexity Constant.
@since version 2.0.0
@liveexample{The behavior is shown in the example.,at_json_pointer}
*/ */
const_reference get_unchecked(const_pointer ptr) const reference at(const json_pointer& ptr)
{
for (const auto& reference_token : reference_tokens)
{
switch (ptr->m_type)
{
case value_t::object:
{ {
// use unchecked object access return ptr.get_checked(this);
ptr = &ptr->operator[](reference_token);
break;
} }
case value_t::array: /*!
{ @brief access specified element via JSON Pointer
if (reference_token == "-")
{
// "-" cannot be used for const access
JSON_THROW(out_of_range::create(402, "array index '-' (" +
std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// error condition (cf. RFC 6901, Sect. 4) Returns a const reference to the element at with specified JSON pointer @a
if (reference_token.size() > 1 and reference_token[0] == '0') ptr, with bounds checking.
{
JSON_THROW(parse_error::create(106, 0, "array index '" + reference_token + "' must not begin with '0'"));
}
// use unchecked array access @param[in] ptr JSON pointer to the desired element
JSON_TRY
@return reference to the element pointed to by @a ptr
@throw parse_error.106 if an array index in the passed JSON pointer @a ptr
begins with '0'. See example below.
@throw parse_error.109 if an array index in the passed JSON pointer @a ptr
is not a number. See example below.
@throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
is out of range. See example below.
@throw out_of_range.402 if the array index '-' is used in the passed JSON
pointer @a ptr. As `at` provides checked access (and no elements are
implicitly inserted), the index '-' is always invalid. See example below.
@throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
See example below.
@exceptionsafety Strong guarantee: if an exception is thrown, there are no
changes in the JSON value.
@complexity Constant.
@since version 2.0.0
@liveexample{The behavior is shown in the example.,at_json_pointer_const}
*/
const_reference at(const json_pointer& ptr) const
{ {
ptr = &ptr->operator[](static_cast<size_type>(std::stoi(reference_token))); return ptr.get_checked(this);
} }
JSON_CATCH (std::invalid_argument&)
/*!
@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 primitive values
@note Empty objects and arrays are flattened to `null` and will not be
reconstructed correctly by the @ref unflatten() function.
@complexity Linear in the size the JSON value.
@liveexample{The following code shows how a JSON object is flattened to an
object whose keys consist of JSON pointers.,flatten}
@sa @ref unflatten() for the reverse function
@since version 2.0.0
*/
basic_json flatten() const
{ {
JSON_THROW(parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); basic_json result(value_t::object);
} json_pointer::flatten("", *this, result);
break; return result;
} }
default: /*!
{ @brief unflatten a previously flattened JSON value
JSON_THROW(out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
} 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()`.
return *ptr; @complexity Linear in the size the JSON value.
}
/*! @throw type_error.314 if value is not an object
@throw parse_error.106 if an array index begins with '0' @throw type_error.315 if object values are not primitive
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
*/
const_reference get_checked(const_pointer ptr) const
{
for (const auto& reference_token : reference_tokens)
{
switch (ptr->m_type)
{
case value_t::object:
{
// note: at performs range check
ptr = &ptr->at(reference_token);
break;
}
case value_t::array: @liveexample{The following code shows how a flattened JSON object is
{ unflattened into the original nested JSON object.,unflatten}
if (reference_token == "-")
{
// "-" always fails the range check
JSON_THROW(out_of_range::create(402, "array index '-' (" +
std::to_string(ptr->m_value.array->size()) +
") is out of range"));
}
// error condition (cf. RFC 6901, Sect. 4) @sa @ref flatten() for the reverse function
if (reference_token.size() > 1 and reference_token[0] == '0')
{
JSON_THROW(parse_error::create(106, 0, "array index '" + reference_token + "' must not begin with '0'"));
}
// note: at performs range check @since version 2.0.0
JSON_TRY */
{ basic_json unflatten() const
ptr = &ptr->at(static_cast<size_type>(std::stoi(reference_token)));
}
JSON_CATCH (std::invalid_argument&)
{ {
JSON_THROW(parse_error::create(109, 0, "array index '" + reference_token + "' is not a number")); return json_pointer::unflatten(*this);
}
break;
} }
default: /// @}
{
JSON_THROW(out_of_range::create(404, "unresolved reference token '" + reference_token + "'"));
}
}
}
return *ptr; //////////////////////////
} // JSON Patch functions //
//////////////////////////
/// @name JSON Patch functions
/// @{
/*! /*!
@brief split the string input to reference tokens @brief applies a JSON patch
@note This function is only called by the json_pointer constructor. [JSON Patch](http://jsonpatch.com) defines a JSON document structure for
All exceptions below are documented there. expressing a sequence of operations to apply to a JSON) document. With
this function, a JSON Patch is applied to the current JSON value by
executing all operations from the patch.
@throw parse_error.107 if the pointer is not empty or begins with '/' @param[in] json_patch JSON patch document
@throw parse_error.108 if character '~' is not followed by '0' or '1' @return patched document
*/
static std::vector<std::string> split(const std::string& reference_string)
{
std::vector<std::string> result;
// special case: empty reference string -> no reference tokens @note The application of a patch is atomic: Either all operations succeed
if (reference_string.empty()) and the patched document is returned or an exception is thrown. In
{ any case, the original value is not changed: the patch is applied
return result; to a copy of the value.
}
// check if nonempty reference string begins with slash @throw parse_error.104 if the JSON patch does not consist of an array of
if (reference_string[0] != '/') objects
{
JSON_THROW(parse_error::create(107, 1, "JSON pointer must be empty or begin with '/' - was: '" + reference_string + "'"));
}
// extract the reference tokens: @throw parse_error.105 if the JSON patch is malformed (e.g., mandatory
// - slash: position of the last read slash (or end of string) attributes are missing); example: `"operation add must have member path"`
// - start: position after the previous slash
for (
// search for the first slash after the first character
size_t slash = reference_string.find_first_of('/', 1),
// set the beginning of the first reference token
start = 1;
// we can stop if start == string::npos+1 = 0
start != 0;
// set the beginning of the next reference token
// (will eventually be 0 if slash == std::string::npos)
start = slash + 1,
// find next slash
slash = reference_string.find_first_of('/', start))
{
// use the text between the beginning of the reference token
// (start) and the last slash (slash).
auto reference_token = reference_string.substr(start, slash - start);
// check reference tokens are properly escaped @throw out_of_range.401 if an array index is out of range.
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 @throw out_of_range.403 if a JSON pointer inside the patch could not be
if (pos == reference_token.size() - 1 or resolved successfully in the current JSON value; example: `"key baz not
(reference_token[pos + 1] != '0' and found"`
reference_token[pos + 1] != '1'))
{
JSON_THROW(parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'"));
}
}
// finally, store the reference token @throw out_of_range.405 if JSON pointer has no parent ("add", "remove",
unescape(reference_token); "move")
result.push_back(reference_token);
}
return result; @throw other_error.501 if "test" operation was unsuccessful
}
/*! @complexity Linear in the size of the JSON value and the length of the
@brief replace all occurrences of a substring by another string JSON patch. As usually only a fraction of the JSON value is affected by
the patch, the complexity can usually be neglected.
@param[in,out] s the string to manipulate; changed so that all @liveexample{The following code shows how a JSON patch is applied to a
occurrences of @a f are replaced with @a t value.,patch}
@param[in] f the substring to replace with @a t
@param[in] t the string to replace @a f
@pre The search string @a f must not be empty. **This precondition is @sa @ref diff -- create a JSON patch by comparing two JSON values
enforced with an assertion.**
@sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902)
@sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901)
@since version 2.0.0 @since version 2.0.0
*/ */
static void replace_substring(std::string& s, basic_json patch(const basic_json& json_patch) const
const std::string& f,
const std::string& t)
{ {
assert(not f.empty()); // make a working copy to apply the patch to
basic_json result = *this;
for ( // the valid JSON Patch operations
size_t pos = s.find(f); // find first occurrence of f enum class patch_operations {add, remove, replace, move, copy, test, invalid};
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
);
}
/// escape tilde and slash const auto get_op = [](const std::string & op)
static std::string escape(std::string s)
{ {
// escape "~"" to "~0" and "/" to "~1" if (op == "add")
replace_substring(s, "~", "~0"); {
replace_substring(s, "/", "~1"); return patch_operations::add;
return s;
} }
if (op == "remove")
/// unescape tilde and slash
static void unescape(std::string& s)
{ {
// first transform any occurrence of the sequence '~1' to '/' return patch_operations::remove;
replace_substring(s, "~1", "/");
// then transform any occurrence of the sequence '~0' to '~'
replace_substring(s, "~0", "~");
} }
if (op == "replace")
/*!
@param[in] reference_string the reference string to the current value
@param[in] value the value to consider
@param[in,out] result the result object to insert values to
@note Empty objects or arrays are flattened to `null`.
*/
static void flatten(const std::string& reference_string,
const basic_json& value,
basic_json& result)
{ {
switch (value.m_type) return patch_operations::replace;
}
if (op == "move")
{ {
case value_t::array: return patch_operations::move;
}
if (op == "copy")
{ {
if (value.m_value.array->empty()) return patch_operations::copy;
}
if (op == "test")
{ {
// flatten empty array as null return patch_operations::test;
result[reference_string] = nullptr;
} }
else
return patch_operations::invalid;
};
// wrapper for "add" operation; add value at ptr
const auto operation_add = [&result](json_pointer & ptr, basic_json val)
{ {
// iterate array and use index as reference string // adding to the root of the target document means replacing it
for (size_t i = 0; i < value.m_value.array->size(); ++i) if (ptr.is_root())
{ {
flatten(reference_string + "/" + std::to_string(i), result = val;
value.m_value.array->operator[](i), result);
} }
else
{
// make sure the top element of the pointer exists
json_pointer top_pointer = ptr.top();
if (top_pointer != ptr)
{
result.at(top_pointer);
} }
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back();
basic_json& parent = result[ptr];
switch (parent.m_type)
{
case value_t::null:
case value_t::object:
{
// use operator[] to add value
parent[last_path] = val;
break; break;
} }
case value_t::object: case value_t::array:
{ {
if (value.m_value.object->empty()) if (last_path == "-")
{ {
// flatten empty object as null // special case: append to back
result[reference_string] = nullptr; parent.push_back(val);
} }
else else
{ {
// iterate object and use keys as reference string const auto idx = std::stoi(last_path);
for (const auto& element : *value.m_value.object) if (static_cast<size_type>(idx) > parent.size())
{
// avoid undefined behavior
JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range"));
}
else
{ {
flatten(reference_string + "/" + escape(element.first), // default case: insert add offset
element.second, result); parent.insert(parent.begin() + static_cast<difference_type>(idx), val);
} }
} }
break; break;
...@@ -13649,428 +13728,490 @@ scan_number_done: ...@@ -13649,428 +13728,490 @@ scan_number_done:
default: default:
{ {
// add primitive value with its reference string // if there exists a parent it cannot be primitive
result[reference_string] = value; assert(false); // LCOV_EXCL_LINE
break;
} }
} }
} }
};
/*! // wrapper for "remove" operation; remove value at ptr
@param[in] value flattened JSON const auto operation_remove = [&result](json_pointer & ptr)
{
@return unflattened JSON // get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr);
@throw parse_error.109 if array index is not a number // remove child
@throw type_error.314 if value is not an object if (parent.is_object())
@throw type_error.315 if object values are not primitive
@throw type_error.313 if value cannot be unflattened
*/
static basic_json unflatten(const basic_json& value)
{ {
if (not value.is_object()) // perform range check
auto it = parent.find(last_path);
if (it != parent.end())
{ {
JSON_THROW(type_error::create(314, "only objects can be unflattened")); parent.erase(it);
} }
else
basic_json result;
// iterate the JSON object values
for (const auto& element : *value.m_value.object)
{ {
if (not element.second.is_primitive()) JSON_THROW(out_of_range::create(403, "key '" + last_path + "' not found"));
}
}
else if (parent.is_array())
{ {
JSON_THROW(type_error::create(315, "values in object must be primitive")); // note erase performs range check
parent.erase(static_cast<size_type>(std::stoi(last_path)));
} }
};
// assign value to reference pointed to by JSON pointer; Note // type check: top level value must be an array
// that if the JSON pointer is "" (i.e., points to the whole if (not json_patch.is_array())
// value), function get_and_create returns a reference to {
// result itself. An assignment will then create a primitive JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
// value.
json_pointer(element.first).get_and_create(result) = element.second;
} }
return result; // iterate and apply the operations
} for (const auto& val : json_patch)
{
// wrapper to get a value for an operation
const auto get_value = [&val](const std::string & op,
const std::string & member,
bool string_type) -> basic_json&
{
// find value
auto it = val.m_value.object->find(member);
friend bool operator==(json_pointer const& lhs, // context-sensitive error message
json_pointer const& rhs) noexcept const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";
// check if desired value is present
if (it == val.m_value.object->end())
{ {
return lhs.reference_tokens == rhs.reference_tokens; JSON_THROW(parse_error::create(105, 0, error_msg + " must have member '" + member + "'"));
} }
friend bool operator!=(json_pointer const& lhs, // check if result is of type string
json_pointer const& rhs) noexcept if (string_type and not it->second.is_string())
{ {
return !(lhs == rhs); JSON_THROW(parse_error::create(105, 0, error_msg + " must have string member '" + member + "'"));
} }
/// the reference tokens // no error: return value
std::vector<std::string> reference_tokens {}; return it->second;
}; };
////////////////////////// // type check: every element of the array must be an object
// JSON Pointer support // if (not val.is_object())
//////////////////////////
/// @name JSON Pointer functions
/// @{
/*!
@brief access specified element via JSON Pointer
Uses a JSON pointer to retrieve a reference to the respective JSON value.
No bound checking is performed. Similar to @ref operator[](const typename
object_t::key_type&), `null` values are created in arrays and objects if
necessary.
In particular:
- If the JSON pointer points to an object key that does not exist, it
is created an filled with a `null` value before a reference to it
is returned.
- If the JSON pointer points to an array index that does not exist, it
is created an filled with a `null` value before a reference to it
is returned. All indices between the current maximum and the given
index are also filled with `null`.
- The special value `-` is treated as a synonym for the index past the
end.
@param[in] ptr a JSON pointer
@return reference to the element pointed to by @a ptr
@complexity Constant.
@throw parse_error.106 if an array index begins with '0'
@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
@liveexample{The behavior is shown in the example.,operatorjson_pointer}
@since version 2.0.0
*/
reference operator[](const json_pointer& ptr)
{ {
return ptr.get_unchecked(this); JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects"));
} }
/*! // collect mandatory members
@brief access specified element via JSON Pointer const std::string op = get_value("op", "op", true);
const std::string path = get_value(op, "path", true);
Uses a JSON pointer to retrieve a reference to the respective JSON value. json_pointer ptr(path);
No bound checking is performed. The function does not change the JSON
value; no `null` values are created. In particular, the the special value
`-` yields an exception.
@param[in] ptr JSON pointer to the desired element
@return const reference to the element pointed to by @a ptr
@complexity Constant.
@throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used
@throw out_of_range.404 if the JSON pointer can not be resolved
@liveexample{The behavior is shown in the example.,operatorjson_pointer_const}
@since version 2.0.0 switch (get_op(op))
*/
const_reference operator[](const json_pointer& ptr) const
{ {
return ptr.get_unchecked(this); case patch_operations::add:
{
operation_add(ptr, get_value("add", "value", false));
break;
} }
/*! case patch_operations::remove:
@brief access specified element via JSON Pointer
Returns a reference to the element at with specified JSON pointer @a ptr,
with bounds checking.
@param[in] ptr JSON pointer to the desired element
@return reference to the element pointed to by @a ptr
@throw parse_error.106 if an array index in the passed JSON pointer @a ptr
begins with '0'. See example below.
@throw parse_error.109 if an array index in the passed JSON pointer @a ptr
is not a number. See example below.
@throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
is out of range. See example below.
@throw out_of_range.402 if the array index '-' is used in the passed JSON
pointer @a ptr. As `at` provides checked access (and no elements are
implicitly inserted), the index '-' is always invalid. See example below.
@throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
See example below.
@exceptionsafety Strong guarantee: if an exception is thrown, there are no
changes in the JSON value.
@complexity Constant.
@since version 2.0.0
@liveexample{The behavior is shown in the example.,at_json_pointer}
*/
reference at(const json_pointer& ptr)
{ {
return ptr.get_checked(this); operation_remove(ptr);
break;
} }
/*! case patch_operations::replace:
@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
@throw parse_error.106 if an array index in the passed JSON pointer @a ptr
begins with '0'. See example below.
@throw parse_error.109 if an array index in the passed JSON pointer @a ptr
is not a number. See example below.
@throw out_of_range.401 if an array index in the passed JSON pointer @a ptr
is out of range. See example below.
@throw out_of_range.402 if the array index '-' is used in the passed JSON
pointer @a ptr. As `at` provides checked access (and no elements are
implicitly inserted), the index '-' is always invalid. See example below.
@throw out_of_range.404 if the JSON pointer @a ptr can not be resolved.
See example below.
@exceptionsafety Strong guarantee: if an exception is thrown, there are no
changes in the JSON value.
@complexity Constant.
@since version 2.0.0
@liveexample{The behavior is shown in the example.,at_json_pointer_const}
*/
const_reference at(const json_pointer& ptr) const
{ {
return ptr.get_checked(this); // the "path" location must exist - use at()
result.at(ptr) = get_value("replace", "value", false);
break;
} }
/*! case patch_operations::move:
@brief return flattened JSON value {
const std::string from_path = get_value("move", "from", true);
json_pointer from_ptr(from_path);
The function creates a JSON object whose keys are JSON pointers (see [RFC // the "from" location must exist - use at()
6901](https://tools.ietf.org/html/rfc6901)) and whose values are all basic_json v = result.at(from_ptr);
primitive. The original JSON value can be restored using the @ref
unflatten() function.
@return an object that maps JSON pointers to primitive values // The move operation is functionally identical to a
// "remove" operation on the "from" location, followed
// immediately by an "add" operation at the target
// location with the value that was just removed.
operation_remove(from_ptr);
operation_add(ptr, v);
break;
}
@note Empty objects and arrays are flattened to `null` and will not be case patch_operations::copy:
reconstructed correctly by the @ref unflatten() function. {
const std::string from_path = get_value("copy", "from", true);
const json_pointer from_ptr(from_path);
@complexity Linear in the size the JSON value. // the "from" location must exist - use at()
result[ptr] = result.at(from_ptr);
break;
}
@liveexample{The following code shows how a JSON object is flattened to an case patch_operations::test:
object whose keys consist of JSON pointers.,flatten} {
bool success = false;
JSON_TRY
{
// check if "value" matches the one at "path"
// the "path" location must exist - use at()
success = (result.at(ptr) == get_value("test", "value", false));
}
JSON_CATCH (out_of_range&)
{
// ignore out of range errors: success remains false
}
@sa @ref unflatten() for the reverse function // throw an exception if test fails
if (not success)
{
JSON_THROW(other_error::create(501, "unsuccessful: " + val.dump()));
}
@since version 2.0.0 break;
*/ }
basic_json flatten() const
case patch_operations::invalid:
{ {
basic_json result(value_t::object); // op must be "add", "remove", "replace", "move", "copy", or
json_pointer::flatten("", *this, result); // "test"
JSON_THROW(parse_error::create(105, 0, "operation value '" + op + "' is invalid"));
}
}
}
return result; return result;
} }
/*! /*!
@brief unflatten a previously flattened JSON value @brief creates a diff as a JSON patch
The function restores the arbitrary nesting of a JSON value that has been Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can
flattened before using the @ref flatten() function. The JSON value must be changed into the value @a target by calling @ref patch function.
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 @invariant For two JSON values @a source and @a target, the following code
yields always `true`:
@code {.cpp}
source.patch(diff(source, target)) == target;
@endcode
@note Empty objects and arrays are flattened by @ref flatten() to `null` @note Currently, only `remove`, `add`, and `replace` operations are
values and can not unflattened to their original type. Apart from generated.
this example, for a JSON value `j`, the following is always true:
`j == j.flatten().unflatten()`.
@complexity Linear in the size the JSON value. @param[in] source JSON value to compare from
@param[in] target JSON value to compare against
@param[in] path helper value to create JSON pointers
@throw type_error.314 if value is not an object @return a JSON patch to convert the @a source to @a target
@throw type_error.315 if object values are not primitive
@liveexample{The following code shows how a flattened JSON object is @complexity Linear in the lengths of @a source and @a target.
unflattened into the original nested JSON object.,unflatten}
@sa @ref flatten() for the reverse function @liveexample{The following code shows how a JSON patch is created as a
diff for two JSON values.,diff}
@sa @ref patch -- apply a JSON patch
@sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902)
@since version 2.0.0 @since version 2.0.0
*/ */
basic_json unflatten() const static basic_json diff(const basic_json& source,
const basic_json& target,
const std::string& path = "")
{ {
return json_pointer::unflatten(*this); // the patch
} basic_json result(value_t::array);
/// @} // if the values are the same, return empty patch
if (source == target)
{
return result;
}
////////////////////////// if (source.type() != target.type())
// JSON Patch functions // {
////////////////////////// // different types: replace value
result.push_back(
{
{"op", "replace"},
{"path", path},
{"value", target}
});
}
else
{
switch (source.type())
{
case value_t::array:
{
// first pass: traverse common elements
size_t i = 0;
while (i < source.size() and i < target.size())
{
// recursive call to compare array values at index i
auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i));
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
++i;
}
/// @name JSON Patch functions // i now reached the end of at least one array
/// @{ // in a second pass, traverse the remaining elements
/*! // remove my remaining elements
@brief applies a JSON patch const auto end_index = static_cast<difference_type>(result.size());
while (i < source.size())
{
// add operations in reverse order to avoid invalid
// indices
result.insert(result.begin() + end_index, object(
{
{"op", "remove"},
{"path", path + "/" + std::to_string(i)}
}));
++i;
}
[JSON Patch](http://jsonpatch.com) defines a JSON document structure for // add other remaining elements
expressing a sequence of operations to apply to a JSON) document. With while (i < target.size())
this function, a JSON Patch is applied to the current JSON value by {
executing all operations from the patch. result.push_back(
{
{"op", "add"},
{"path", path + "/" + std::to_string(i)},
{"value", target[i]}
});
++i;
}
@param[in] json_patch JSON patch document break;
@return patched document }
@note The application of a patch is atomic: Either all operations succeed case value_t::object:
and the patched document is returned or an exception is thrown. In {
any case, the original value is not changed: the patch is applied // first pass: traverse this object's elements
to a copy of the value. for (auto it = source.begin(); it != source.end(); ++it)
{
// escape the key name to be used in a JSON patch
const auto key = json_pointer::escape(it.key());
@throw parse_error.104 if the JSON patch does not consist of an array of if (target.find(it.key()) != target.end())
objects {
// recursive call to compare object values at key it
auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key);
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
}
else
{
// found a key that is not in o -> remove it
result.push_back(object(
{
{"op", "remove"},
{"path", path + "/" + key}
}));
}
}
@throw parse_error.105 if the JSON patch is malformed (e.g., mandatory // second pass: traverse other object's elements
attributes are missing); example: `"operation add must have member path"` for (auto it = target.begin(); it != target.end(); ++it)
{
if (source.find(it.key()) == source.end())
{
// found a key that is not in this -> add it
const auto key = json_pointer::escape(it.key());
result.push_back(
{
{"op", "add"},
{"path", path + "/" + key},
{"value", it.value()}
});
}
}
@throw out_of_range.401 if an array index is out of range. break;
}
@throw out_of_range.403 if a JSON pointer inside the patch could not be default:
resolved successfully in the current JSON value; example: `"key baz not {
found"` // both primitive type: replace value
result.push_back(
{
{"op", "replace"},
{"path", path},
{"value", target}
});
break;
}
}
}
@throw out_of_range.405 if JSON pointer has no parent ("add", "remove", return result;
"move") }
@throw other_error.501 if "test" operation was unsuccessful /// @}
};
@complexity Linear in the size of the JSON value and the length of the /////////////
JSON patch. As usually only a fraction of the JSON value is affected by // presets //
the patch, the complexity can usually be neglected. /////////////
@liveexample{The following code shows how a JSON patch is applied to a /*!
value.,patch} @brief default JSON class
@sa @ref diff -- create a JSON patch by comparing two JSON values This type is the default specialization of the @ref basic_json class which
uses the standard template types.
@sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) @since version 1.0.0
@sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901) */
using json = basic_json<>;
@since version 2.0.0 //////////////////
*/ // json_pointer //
basic_json patch(const basic_json& json_patch) const //////////////////
{
// make a working copy to apply the patch to
basic_json result = *this;
// the valid JSON Patch operations NLOHMANN_BASIC_JSON_TPL_DECLARATION
enum class patch_operations {add, remove, replace, move, copy, test, invalid}; NLOHMANN_BASIC_JSON_TPL&
json_pointer::get_and_create(NLOHMANN_BASIC_JSON_TPL& j) const
{
using size_type = typename NLOHMANN_BASIC_JSON_TPL::size_type;
auto result = &j;
const auto get_op = [](const std::string & op) // 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)
{ {
if (op == "add") switch (result->m_type)
{ {
return patch_operations::add; case detail::value_t::null:
{
if (reference_token == "0")
{
// start a new array if reference token is 0
result = &result->operator[](0);
} }
if (op == "remove") else
{ {
return patch_operations::remove; // start a new object otherwise
result = &result->operator[](reference_token);
} }
if (op == "replace") break;
}
case detail::value_t::object:
{ {
return patch_operations::replace; // create an entry in the object
result = &result->operator[](reference_token);
break;
} }
if (op == "move")
case detail::value_t::array:
{ {
return patch_operations::move; // create an entry in the array
JSON_TRY
{
result = &result->operator[](
static_cast<size_type>(std::stoi(reference_token)));
} }
if (op == "copy") JSON_CATCH(std::invalid_argument&)
{ {
return patch_operations::copy; JSON_THROW(detail::parse_error::create(
109, 0, "array index '" + reference_token + "' is not a number"));
} }
if (op == "test") break;
}
/*
The following code is only reached if there exists a
reference token _and_ the current value is primitive. In
this case, we have an error situation, because primitive
values may only occur as single value; that is, with an
empty list of reference tokens.
*/
default:
{ {
return patch_operations::test; JSON_THROW(detail::type_error::create(313, "invalid value to unflatten"));
}
}
} }
return patch_operations::invalid; return *result;
}; }
// wrapper for "add" operation; add value at ptr NLOHMANN_BASIC_JSON_TPL_DECLARATION
const auto operation_add = [&result](json_pointer & ptr, basic_json val) NLOHMANN_BASIC_JSON_TPL&
json_pointer::get_unchecked(NLOHMANN_BASIC_JSON_TPL* ptr) const
{
using size_type = typename NLOHMANN_BASIC_JSON_TPL::size_type;
for (const auto& reference_token : reference_tokens)
{ {
// adding to the root of the target document means replacing it // convert null values to arrays or objects before continuing
if (ptr.is_root()) if (ptr->m_type == detail::value_t::null)
{ {
result = val; // check if reference token is a number
const bool nums =
std::all_of(reference_token.begin(), reference_token.end(),
[](const char x)
{
return (x >= '0' and x <= '9');
});
// change value to array for numbers or "-" or to object
// otherwise
if (nums or reference_token == "-")
{
*ptr = detail::value_t::array;
} }
else else
{ {
// make sure the top element of the pointer exists *ptr = detail::value_t::object;
json_pointer top_pointer = ptr.top(); }
if (top_pointer != ptr)
{
result.at(top_pointer);
} }
// get reference to parent of JSON pointer ptr switch (ptr->m_type)
const auto last_path = ptr.pop_back();
basic_json& parent = result[ptr];
switch (parent.m_type)
{ {
case value_t::null: case detail::value_t::object:
case value_t::object:
{ {
// use operator[] to add value // use unchecked object access
parent[last_path] = val; ptr = &ptr->operator[](reference_token);
break; break;
} }
case value_t::array: case detail::value_t::array:
{ {
if (last_path == "-") // error condition (cf. RFC 6901, Sect. 4)
if (reference_token.size() > 1 and reference_token[0] == '0')
{ {
// special case: append to back JSON_THROW(detail::parse_error::create(106, 0,
parent.push_back(val); "array index '" + reference_token +
"' must not begin with '0'"));
}
if (reference_token == "-")
{
// explicitly treat "-" as index beyond the end
ptr = &ptr->operator[](ptr->m_value.array->size());
} }
else else
{ {
const auto idx = std::stoi(last_path); // convert array index to number; unchecked access
if (static_cast<size_type>(idx) > parent.size()) JSON_TRY
{ {
// avoid undefined behavior ptr = &ptr->operator[](
JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range")); static_cast<size_type>(std::stoi(reference_token)));
} }
else JSON_CATCH(std::invalid_argument&)
{ {
// default case: insert add offset JSON_THROW(detail::parse_error::create(
parent.insert(parent.begin() + static_cast<difference_type>(idx), val); 109, 0, "array index '" + reference_token + "' is not a number"));
} }
} }
break; break;
...@@ -14078,351 +14219,286 @@ scan_number_done: ...@@ -14078,351 +14219,286 @@ scan_number_done:
default: default:
{ {
// if there exists a parent it cannot be primitive JSON_THROW(detail::out_of_range::create(
assert(false); // LCOV_EXCL_LINE 404, "unresolved reference token '" + reference_token + "'"));
} }
} }
} }
};
// wrapper for "remove" operation; remove value at ptr return *ptr;
const auto operation_remove = [&result](json_pointer & ptr) }
{
// get reference to parent of JSON pointer ptr
const auto last_path = ptr.pop_back();
basic_json& parent = result.at(ptr);
// remove child NLOHMANN_BASIC_JSON_TPL_DECLARATION
if (parent.is_object()) NLOHMANN_BASIC_JSON_TPL&
{ json_pointer::get_checked(NLOHMANN_BASIC_JSON_TPL* ptr) const
// perform range check {
auto it = parent.find(last_path); using size_type = typename NLOHMANN_BASIC_JSON_TPL::size_type;
if (it != parent.end()) for (const auto& reference_token : reference_tokens)
{
parent.erase(it);
}
else
{ {
JSON_THROW(out_of_range::create(403, "key '" + last_path + "' not found")); switch (ptr->m_type)
}
}
else if (parent.is_array())
{ {
// note erase performs range check case detail::value_t::object:
parent.erase(static_cast<size_type>(std::stoi(last_path)));
}
};
// type check: top level value must be an array
if (not json_patch.is_array())
{ {
JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects")); // note: at performs range check
ptr = &ptr->at(reference_token);
break;
} }
// iterate and apply the operations case detail::value_t::array:
for (const auto& val : json_patch)
{
// wrapper to get a value for an operation
const auto get_value = [&val](const std::string & op,
const std::string & member,
bool string_type) -> basic_json&
{
// find value
auto it = val.m_value.object->find(member);
// context-sensitive error message
const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'";
// check if desired value is present
if (it == val.m_value.object->end())
{ {
JSON_THROW(parse_error::create(105, 0, error_msg + " must have member '" + member + "'")); if (reference_token == "-")
}
// check if result is of type string
if (string_type and not it->second.is_string())
{ {
JSON_THROW(parse_error::create(105, 0, error_msg + " must have string member '" + member + "'")); // "-" 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"));
} }
// no error: return value // error condition (cf. RFC 6901, Sect. 4)
return it->second; if (reference_token.size() > 1 and reference_token[0] == '0')
};
// type check: every element of the array must be an object
if (not val.is_object())
{ {
JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects")); JSON_THROW(detail::parse_error::create(106, 0,
"array index '" + reference_token +
"' must not begin with '0'"));
} }
// collect mandatory members // note: at performs range check
const std::string op = get_value("op", "op", true); JSON_TRY
const std::string path = get_value(op, "path", true);
json_pointer ptr(path);
switch (get_op(op))
{
case patch_operations::add:
{ {
operation_add(ptr, get_value("add", "value", false)); ptr = &ptr->at(static_cast<size_type>(std::stoi(reference_token)));
break;
} }
JSON_CATCH(std::invalid_argument&)
case patch_operations::remove:
{ {
operation_remove(ptr); JSON_THROW(detail::parse_error::create(
break; 109, 0, "array index '" + reference_token + "' is not a number"));
} }
case patch_operations::replace:
{
// the "path" location must exist - use at()
result.at(ptr) = get_value("replace", "value", false);
break; break;
} }
case patch_operations::move: default:
{ {
const std::string from_path = get_value("move", "from", true); JSON_THROW(detail::out_of_range::create(
json_pointer from_ptr(from_path); 404, "unresolved reference token '" + reference_token + "'"));
}
// the "from" location must exist - use at() }
basic_json v = result.at(from_ptr);
// The move operation is functionally identical to a
// "remove" operation on the "from" location, followed
// immediately by an "add" operation at the target
// location with the value that was just removed.
operation_remove(from_ptr);
operation_add(ptr, v);
break;
} }
case patch_operations::copy: return *ptr;
{ }
const std::string from_path = get_value("copy", "from", true);
const json_pointer from_ptr(from_path);
// the "from" location must exist - use at() NLOHMANN_BASIC_JSON_TPL_DECLARATION
result[ptr] = result.at(from_ptr); const NLOHMANN_BASIC_JSON_TPL&
json_pointer::get_unchecked(const NLOHMANN_BASIC_JSON_TPL* ptr) const
{
using size_type = typename NLOHMANN_BASIC_JSON_TPL::size_type;
for (const auto& reference_token : reference_tokens)
{
switch (ptr->m_type)
{
case detail::value_t::object:
{
// use unchecked object access
ptr = &ptr->operator[](reference_token);
break; break;
} }
case patch_operations::test: case detail::value_t::array:
{ {
bool success = false; if (reference_token == "-")
JSON_TRY
{ {
// check if "value" matches the one at "path" // "-" cannot be used for const access
// the "path" location must exist - use at() JSON_THROW(detail::out_of_range::create(
success = (result.at(ptr) == get_value("test", "value", false)); 402,
"array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range"));
} }
JSON_CATCH (out_of_range&)
// error condition (cf. RFC 6901, Sect. 4)
if (reference_token.size() > 1 and reference_token[0] == '0')
{ {
// ignore out of range errors: success remains false JSON_THROW(detail::parse_error::create(106, 0,
"array index '" + reference_token +
"' must not begin with '0'"));
} }
// throw an exception if test fails // use unchecked array access
if (not success) JSON_TRY
{ {
JSON_THROW(other_error::create(501, "unsuccessful: " + val.dump())); ptr = &ptr->operator[](
static_cast<size_type>(std::stoi(reference_token)));
}
JSON_CATCH(std::invalid_argument&)
{
JSON_THROW(detail::parse_error::create(
109, 0, "array index '" + reference_token + "' is not a number"));
} }
break; break;
} }
case patch_operations::invalid: default:
{ {
// op must be "add", "remove", "replace", "move", "copy", or JSON_THROW(detail::out_of_range::create(
// "test" 404, "unresolved reference token '" + reference_token + "'"));
JSON_THROW(parse_error::create(105, 0, "operation value '" + op + "' is invalid"));
} }
} }
} }
return result; return *ptr;
} }
/*!
@brief creates a diff as a JSON patch
Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can
be changed into the value @a target by calling @ref patch function.
@invariant For two JSON values @a source and @a target, the following code
yields always `true`:
@code {.cpp}
source.patch(diff(source, target)) == target;
@endcode
@note Currently, only `remove`, `add`, and `replace` operations are
generated.
@param[in] source JSON value to compare from
@param[in] target JSON value to compare against
@param[in] path helper value to create JSON pointers
@return a JSON patch to convert the @a source to @a target
@complexity Linear in the lengths of @a source and @a target.
@liveexample{The following code shows how a JSON patch is created as a
diff for two JSON values.,diff}
@sa @ref patch -- apply a JSON patch
@sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902)
@since version 2.0.0 NLOHMANN_BASIC_JSON_TPL_DECLARATION
*/ const NLOHMANN_BASIC_JSON_TPL&
static basic_json diff(const basic_json& source, json_pointer::get_checked(const NLOHMANN_BASIC_JSON_TPL* ptr) const
const basic_json& target, {
const std::string& path = "") using size_type = typename NLOHMANN_BASIC_JSON_TPL::size_type;
for (const auto& reference_token : reference_tokens)
{ {
// the patch switch (ptr->m_type)
basic_json result(value_t::array);
// if the values are the same, return empty patch
if (source == target)
{ {
return result; case detail::value_t::object:
{
// note: at performs range check
ptr = &ptr->at(reference_token);
break;
} }
if (source.type() != target.type()) case detail::value_t::array:
{ {
// different types: replace value if (reference_token == "-")
result.push_back(
{ {
{"op", "replace"}, // "-" always fails the range check
{"path", path}, JSON_THROW(detail::out_of_range::create(
{"value", target} 402,
}); "array index '-' (" + std::to_string(ptr->m_value.array->size()) +
") is out of range"));
} }
else
{ // error condition (cf. RFC 6901, Sect. 4)
switch (source.type()) if (reference_token.size() > 1 and reference_token[0] == '0')
{
case value_t::array:
{
// first pass: traverse common elements
size_t i = 0;
while (i < source.size() and i < target.size())
{ {
// recursive call to compare array values at index i JSON_THROW(detail::parse_error::create(106, 0,
auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i)); "array index '" + reference_token +
result.insert(result.end(), temp_diff.begin(), temp_diff.end()); "' must not begin with '0'"));
++i;
} }
// i now reached the end of at least one array // note: at performs range check
// in a second pass, traverse the remaining elements JSON_TRY
// remove my remaining elements
const auto end_index = static_cast<difference_type>(result.size());
while (i < source.size())
{ {
// add operations in reverse order to avoid invalid ptr = &ptr->at(static_cast<size_type>(std::stoi(reference_token)));
// indices }
result.insert(result.begin() + end_index, object( JSON_CATCH(std::invalid_argument&)
{ {
{"op", "remove"}, JSON_THROW(detail::parse_error::create(
{"path", path + "/" + std::to_string(i)} 109, 0, "array index '" + reference_token + "' is not a number"));
})); }
++i; break;
} }
// add other remaining elements default:
while (i < target.size())
{
result.push_back(
{ {
{"op", "add"}, JSON_THROW(detail::out_of_range::create(
{"path", path + "/" + std::to_string(i)}, 404, "unresolved reference token '" + reference_token + "'"));
{"value", target[i]} }
});
++i;
} }
break;
} }
case value_t::object: return *ptr;
}
NLOHMANN_BASIC_JSON_TPL_DECLARATION
void json_pointer::flatten(const std::string& reference_string,
const NLOHMANN_BASIC_JSON_TPL& value,
NLOHMANN_BASIC_JSON_TPL& result)
{
switch (value.m_type)
{ {
// first pass: traverse this object's elements case detail::value_t::array:
for (auto it = source.begin(); it != source.end(); ++it)
{ {
// escape the key name to be used in a JSON patch if (value.m_value.array->empty())
const auto key = json_pointer::escape(it.key());
if (target.find(it.key()) != target.end())
{ {
// recursive call to compare object values at key it // flatten empty array as null
auto temp_diff = diff(it.value(), target[it.key()], path + "/" + key); result[reference_string] = nullptr;
result.insert(result.end(), temp_diff.begin(), temp_diff.end());
} }
else else
{ {
// found a key that is not in o -> remove it // iterate array and use index as reference string
result.push_back(object( for (size_t i = 0; i < value.m_value.array->size(); ++i)
{ {
{"op", "remove"}, flatten(reference_string + "/" + std::to_string(i),
{"path", path + "/" + key} value.m_value.array->operator[](i), result);
})); }
} }
break;
} }
// second pass: traverse other object's elements case detail::value_t::object:
for (auto it = target.begin(); it != target.end(); ++it)
{ {
if (source.find(it.key()) == source.end()) if (value.m_value.object->empty())
{ {
// found a key that is not in this -> add it // flatten empty object as null
const auto key = json_pointer::escape(it.key()); result[reference_string] = nullptr;
result.push_back( }
else
{ {
{"op", "add"}, // iterate object and use keys as reference string
{"path", path + "/" + key}, for (const auto& element : *value.m_value.object)
{"value", it.value()} {
}); flatten(reference_string + "/" + escape(element.first), element.second,
result);
} }
} }
break; break;
} }
default: default:
{ {
// both primitive type: replace value // add primitive value with its reference string
result.push_back( result[reference_string] = value;
{
{"op", "replace"},
{"path", path},
{"value", target}
});
break; break;
} }
} }
} }
return result; NLOHMANN_BASIC_JSON_TPL_DECLARATION
NLOHMANN_BASIC_JSON_TPL
json_pointer::unflatten(const NLOHMANN_BASIC_JSON_TPL& value)
{
if (not value.is_object())
{
JSON_THROW(detail::type_error::create(314, "only objects can be unflattened"));
} }
/// @} NLOHMANN_BASIC_JSON_TPL result;
};
///////////// // iterate the JSON object values
// presets // for (const auto& element : *value.m_value.object)
///////////// {
if (not element.second.is_primitive())
{
JSON_THROW(detail::type_error::create(315, "values in object must be primitive"));
}
/*! // assign value to reference pointed to by JSON pointer; Note
@brief default JSON class // 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;
}
This type is the default specialization of the @ref basic_json class which return result;
uses the standard template types. }
@since version 1.0.0 inline bool operator==(json_pointer const& lhs, json_pointer const& rhs) noexcept
*/ {
using json = basic_json<>; return lhs.reference_tokens == rhs.reference_tokens;
}
inline bool operator!=(json_pointer const& lhs, json_pointer const& rhs) noexcept
{
return !(lhs == rhs);
}
} // namespace nlohmann } // namespace nlohmann
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment