|
|
@@ -2,13 +2,16 @@
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <cassert>
|
|
|
+#include <charconv>
|
|
|
#include <iostream>
|
|
|
#include <string>
|
|
|
#include <string_view>
|
|
|
+#include <system_error>
|
|
|
#include <variant>
|
|
|
#include <vector>
|
|
|
|
|
|
#include <jvalidate/compat/compare.h>
|
|
|
+#include <jvalidate/compat/expected.h>
|
|
|
#include <jvalidate/forward.h>
|
|
|
|
|
|
namespace jvalidate::detail {
|
|
|
@@ -30,9 +33,12 @@ struct parent_t {};
|
|
|
constexpr parent_t parent;
|
|
|
|
|
|
class Pointer {
|
|
|
+private:
|
|
|
+ using Token = std::variant<std::string, size_t>;
|
|
|
+
|
|
|
public:
|
|
|
Pointer() = default;
|
|
|
- Pointer(std::vector<std::variant<std::string, size_t>> const & tokens) : tokens_(tokens) {}
|
|
|
+ Pointer(std::vector<Token> const & tokens) : tokens_(tokens) {}
|
|
|
|
|
|
/**
|
|
|
* @brief Parse a JSON-Pointer from a serialized JSON-Pointer-String. In
|
|
|
@@ -46,41 +52,6 @@ public:
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- auto append_with_parse = [this](std::string in) {
|
|
|
- // Best-guess that the input token text represents a numeric value.
|
|
|
- // Technically - this could mean that we have an object key that is also
|
|
|
- // a number (e.g. the jsonized form of map<int, T>), but we can generally
|
|
|
- // assume that we are not going to use those kinds of paths in a reference
|
|
|
- // field. Therefore we don't need to include any clever tricks for storage
|
|
|
- if (not in.empty() && in.find_first_not_of("0123456789") == std::string::npos) {
|
|
|
- return tokens_.push_back(std::stoull(in));
|
|
|
- }
|
|
|
-
|
|
|
- for (size_t i = 0; i < in.size(); ++i) {
|
|
|
- // Allow URL-Escaped characters (%\x\x) to be turned into their
|
|
|
- // matching ASCII characters. This allows passing abnormal chars other
|
|
|
- // than '/' and '~' to be handled in all contexts.
|
|
|
- // TODO(samjaffe): Only do this if enc is hex-like (currently throws?)
|
|
|
- if (in[i] == '%') {
|
|
|
- char const enc[3] = {in[i + 1], in[i + 2]};
|
|
|
- in.replace(i, 3, 1, char(std::stoi(enc, nullptr, 16)));
|
|
|
- } else if (in[i] != '~') {
|
|
|
- // Not a special char-sequence, does not need massaging
|
|
|
- continue;
|
|
|
- }
|
|
|
- // In order to properly support '/' inside the property name of an
|
|
|
- // object, we must escape it. The designers of the JSON-Pointer RFC
|
|
|
- // chose to use '~' as a special signifier. Mapping '~0' to '~', and
|
|
|
- // '~1' to '/'.
|
|
|
- if (in[i + 1] == '0') {
|
|
|
- in.replace(i, 2, 1, '~');
|
|
|
- } else if (in[i + 1] == '1') {
|
|
|
- in.replace(i, 2, 1, '/');
|
|
|
- }
|
|
|
- }
|
|
|
- tokens_.push_back(std::move(in));
|
|
|
- };
|
|
|
-
|
|
|
// JSON-Pointers are required to start with a '/' although we only enforce
|
|
|
// that rule in Reference.
|
|
|
path.remove_prefix(1);
|
|
|
@@ -90,10 +61,60 @@ public:
|
|
|
// easy.
|
|
|
for (size_t p = path.find('/'); p != std::string::npos;
|
|
|
path.remove_prefix(p + 1), p = path.find('/')) {
|
|
|
- append_with_parse(std::string(path.substr(0, p)));
|
|
|
+ tokens_.push_back(parse_token(std::string(path.substr(0, p))).value());
|
|
|
}
|
|
|
|
|
|
- append_with_parse(std::string(path));
|
|
|
+ tokens_.push_back(parse_token(std::string(path)).value());
|
|
|
+ }
|
|
|
+
|
|
|
+ template <typename T>
|
|
|
+ static expected<T, std::string> parse_integer(std::string_view in, int base = 10) {
|
|
|
+ T rval = 0;
|
|
|
+ auto [ptr, ec] = std::from_chars(in.begin(), in.end(), rval, base);
|
|
|
+ if (ec != std::errc{}) {
|
|
|
+ return unexpected(std::make_error_code(ec).message());
|
|
|
+ }
|
|
|
+ return rval;
|
|
|
+ }
|
|
|
+
|
|
|
+ static expected<Token, std::string> parse_token(std::string in) {
|
|
|
+ // Best-guess that the input token text represents a numeric value.
|
|
|
+ // Technically - this could mean that we have an object key that is also
|
|
|
+ // a number (e.g. the jsonized form of map<int, T>), but we can generally
|
|
|
+ // assume that we are not going to use those kinds of paths in a reference
|
|
|
+ // field. Therefore we don't need to include any clever tricks for storage
|
|
|
+ if (not in.empty() && in.find_first_not_of("0123456789") == std::string::npos) {
|
|
|
+ return parse_integer<size_t>(in);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (size_t i = 0; i < in.size(); ++i) {
|
|
|
+ // Allow URL-Escaped characters (%\x\x) to be turned into their
|
|
|
+ // matching ASCII characters. This allows passing abnormal chars other
|
|
|
+ // than '/' and '~' to be handled in all contexts.
|
|
|
+ // TODO(samjaffe): Only do this if enc is hex-like (currently throws?)
|
|
|
+ if (in[i] == '%') {
|
|
|
+ if (auto code = parse_integer<char>(std::string_view(in).substr(i + 1, 2), 16)) {
|
|
|
+ in.replace(i, 3, 1, *code);
|
|
|
+ } else {
|
|
|
+ return code.error();
|
|
|
+ }
|
|
|
+ } else if (in[i] != '~') {
|
|
|
+ // Not a special char-sequence, does not need massaging
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // In order to properly support '/' inside the property name of an
|
|
|
+ // object, we must escape it. The designers of the JSON-Pointer RFC
|
|
|
+ // chose to use '~' as a special signifier. Mapping '~0' to '~', and
|
|
|
+ // '~1' to '/'.
|
|
|
+ if (in[i + 1] == '0') {
|
|
|
+ in.replace(i, 2, 1, '~');
|
|
|
+ } else if (in[i + 1] == '1') {
|
|
|
+ in.replace(i, 2, 1, '/');
|
|
|
+ } else {
|
|
|
+ // return unexpected("illegal tilde '" + in.substr(i, 2) + "'");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return in;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -217,6 +238,6 @@ public:
|
|
|
auto operator<=>(Pointer const &) const = default;
|
|
|
|
|
|
private:
|
|
|
- std::vector<std::variant<std::string, size_t>> tokens_{};
|
|
|
+ std::vector<Token> tokens_{};
|
|
|
};
|
|
|
}
|