Explorar el Código

refactor: switch Pointer to use expected under-the-hood, preserve API

Sam Jaffe hace 3 meses
padre
commit
b9102750ce
Se han modificado 1 ficheros con 60 adiciones y 39 borrados
  1. 60 39
      include/jvalidate/detail/pointer.h

+ 60 - 39
include/jvalidate/detail/pointer.h

@@ -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_{};
 };
 }