瀏覽代碼

Merge branch 'refactor/expected' into feat/format-matcher-with-expect

Sam Jaffe 2 周之前
父節點
當前提交
b557d86fac

+ 348 - 0
include/jvalidate/compat/expected.h

@@ -0,0 +1,348 @@
+#pragma once
+
+#include <type_traits>
+
+#ifdef __cpp_lib_expected
+#if __cpp_lib_expected >= 202211L
+#include <expected>
+
+namespace jvalidate::detail {
+using std::expected;
+using std::unexpected;
+inline constexpr std::unexpect_t unexpect{};
+}
+#endif
+#else
+#include <initializer_list>
+#include <optional>
+#include <utility>
+#include <variant>
+
+#include <jvalidate/detail/expect.h>
+
+namespace jvalidate::detail {
+inline constexpr struct unexpect_t {
+} unexpect{};
+
+template <typename E> class unexpected {
+public:
+  template <typename Err = E>
+  constexpr explicit unexpected(Err && e) : error_(std::forward<Err>(e)) {}
+  template <typename... Args>
+  constexpr explicit unexpected(std::in_place_t, Args &&... args)
+      : error_{std::forward<Args>(args)...} {}
+  template <typename U, typename... Args>
+  constexpr explicit unexpected(std::in_place_t, std::initializer_list<U> il, Args &&... args)
+      : error_{il, std::forward<Args>(args)...} {}
+
+  constexpr const E & error() const & noexcept { return error_; }
+  constexpr E & error() & noexcept { return error_; }
+  constexpr E && error() && noexcept { return std::move(error_); }
+
+  constexpr void swap(unexpected & other) noexcept(std::is_nothrow_swappable_v<E>) {
+    using std::swap;
+    swap(error(), other.error());
+  }
+
+  template <typename E2> friend constexpr bool operator==(unexpected & x, unexpected<E2> & y) {
+    return x.error() == y.error();
+  }
+
+  friend constexpr void swap(unexpected & x, unexpected & y) noexcept(noexcept(x.swap(y))) {
+    return x.swap(y);
+  }
+
+private:
+  E error_;
+};
+
+template <typename E> unexpected(E) -> unexpected<E>;
+
+template <typename E> class bad_expected_access;
+template <> class bad_expected_access<void> : public std::exception {
+public:
+  char const * what() const noexcept override { return "unexpected"; }
+};
+
+template <typename E> class bad_expected_access : public bad_expected_access<void> {
+public:
+  bad_expected_access(E error) : error_(std::move(error)) {}
+
+  char const * what() const noexcept override { return error_.c_str(); }
+
+  constexpr const E & error() const & noexcept { return error_; }
+  constexpr E & error() & noexcept { return error_; }
+  constexpr E && error() && noexcept { return std::move(error_); }
+
+private:
+  E error_;
+};
+
+template <typename T, typename E> class expected {
+public:
+  using value_type = T;
+  using error_type = E;
+  using unexpected_type = unexpected<E>;
+  template <typename U> using rebind = expected<U, error_type>;
+
+public:
+  constexpr expected() = default;
+
+  template <typename U, typename G>
+  constexpr explicit(!std::is_convertible_v<const U &, T> && !std::is_convertible_v<const G &, E>)
+      expected(expected<U, G> const & other) {
+    if (other.has_value()) {
+      value_.template emplace<0>(*other);
+    } else {
+      value_.template emplace<1>(other.error());
+    }
+  }
+
+  template <typename U, typename G>
+  constexpr explicit(!std::is_convertible_v<U, T> && !std::is_convertible_v<G, E>)
+      expected(expected<U, G> && other) {
+    if (other.has_value()) {
+      value_.template emplace<0>(*std::move(other));
+    } else {
+      value_.template emplace<1>(std::move(other).error());
+    }
+  }
+
+  template <typename U = std::remove_cv_t<T>>
+  constexpr explicit(!std::is_convertible_v<U, T>) expected(U && v)
+      : value_(std::in_place_index<0>, std::forward<T>(v)) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<const G &, E>) expected(unexpected<G> const & e)
+      : value_(std::in_place_index<1>, e.error()) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<G, E>) expected(unexpected<G> && e)
+      : value_(std::in_place_index<1>, std::move(e).error()) {}
+
+  template <typename... Args>
+  constexpr explicit expected(std::in_place_t, Args &&... args)
+      : value_(std::in_place_index<0>, std::forward<Args>(args)...) {}
+
+  template <typename U, typename... Args>
+  constexpr explicit expected(std::in_place_t, std::initializer_list<U> il, Args &&... args)
+      : value_(std::in_place_index<0>, il, std::forward<Args>(args)...) {}
+
+  template <typename... Args>
+  constexpr explicit expected(unexpect_t, Args &&... args)
+      : value_(std::in_place_index<1>, std::forward<Args>(args)...) {}
+
+  template <typename U, typename... Args>
+  constexpr explicit expected(unexpect_t, std::initializer_list<U> il, Args &&... args)
+      : value_(std::in_place_index<1>, il, std::forward<Args>(args)...) {}
+
+  constexpr explicit operator bool() const noexcept { return has_value(); }
+  constexpr bool has_value() const noexcept { return value_.index() == 0; }
+
+  constexpr const T * operator->() const noexcept { return std::get_if<0>(&value_); }
+  constexpr T * operator->() noexcept { return std::get_if<0>(&value_); }
+
+  constexpr const T & operator*() const & noexcept { return *std::get_if<0>(&value_); }
+  constexpr T & operator*() & noexcept { return *std::get_if<0>(&value_); }
+  constexpr T && operator*() && noexcept { return std::move(*std::get_if<0>(&value_)); }
+
+  constexpr const T & value() const & noexcept {
+    if (JVALIDATE_LIKELY(has_value())) {
+      return operator*();
+    }
+    throw bad_expected_access(error());
+  }
+
+  constexpr T & value() & noexcept {
+    if (JVALIDATE_LIKELY(has_value())) {
+      return operator*();
+    }
+    throw bad_expected_access(std::as_const(error()));
+  }
+
+  constexpr T && value() && noexcept {
+    if (JVALIDATE_LIKELY(has_value())) {
+      return std::move(*this).operator*();
+    }
+    throw bad_expected_access(std::move(error()));
+  }
+
+  constexpr const E & error() const & noexcept { return *std::get_if<1>(&value_); }
+  constexpr E & error() & noexcept { return *std::get_if<1>(&value_); }
+  constexpr E && error() && noexcept { return std::move(*std::get_if<1>(&value_)); }
+
+  template <typename F> constexpr auto transform(F && f) & {
+    using G = std::invoke_result_t<F, value_type &>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)(**this));
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && f) const & {
+    using G = std::invoke_result_t<F, value_type const &>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)(**this));
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && f) && {
+    using G = std::invoke_result_t<F, value_type>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)(*std::move(*this)));
+    }
+    return expected<G, error_type>(unexpect, std::move(*this).error());
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) & {
+    using G = std::invoke_result_t<F, E &>;
+    if (has_value()) {
+      return expected<value_type, G>(**this);
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) const & {
+    using G = std::invoke_result_t<F, E const &>;
+    if (has_value()) {
+      return expected<value_type, G>(**this);
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) && {
+    using G = std::invoke_result_t<F, E>;
+    if (has_value()) {
+      return expected<value_type, G>(*std::move(*this));
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(std::move(*this).error()));
+  }
+
+private:
+  std::variant<T, E> value_;
+};
+
+template <typename E> class expected<void, E> {
+public:
+  using value_type = void;
+  using error_type = E;
+  using unexpected_type = unexpected<E>;
+  template <typename U> using rebind = expected<U, error_type>;
+
+public:
+  constexpr expected() = default;
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<const G &, E>)
+      expected(expected<void, G> const & other) {
+    if (not other.has_value()) {
+      *this = other.error();
+    }
+  }
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<G, E>) expected(expected<void, G> && other) {
+    if (not other.has_value()) {
+      *this = std::move(other).error();
+    }
+  }
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<const G &, E>) expected(unexpected<G> const & e)
+      : value_(std::in_place_index<1>, e.error()) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<G, E>) expected(unexpected<G> && e)
+      : value_(std::in_place_index<1>, std::move(e).error()) {}
+
+  constexpr explicit expected(std::in_place_t) {}
+
+  template <typename... Args>
+  constexpr explicit expected(unexpect_t, Args &&... args)
+      : value_(std::in_place, std::forward<Args>(args)...) {}
+
+  template <typename U, typename... Args>
+  constexpr explicit expected(unexpect_t, std::initializer_list<U> il, Args &&... args)
+      : value_(std::in_place, il, std::forward<Args>(args)...) {}
+
+  constexpr explicit operator bool() const noexcept { return has_value(); }
+  constexpr bool has_value() const noexcept { return not value_.has_value(); }
+
+  constexpr void operator*() const noexcept {}
+
+  void value() const & noexcept {
+    if (JVALIDATE_LIKELY(has_value())) {
+      return operator*();
+    }
+    throw bad_expected_access(error());
+  }
+
+  void value() && noexcept {
+    if (JVALIDATE_LIKELY(has_value())) {
+      return operator*();
+    }
+    throw bad_expected_access(std::move(error()));
+  }
+
+  constexpr const E & error() const & noexcept { return *value_; }
+  constexpr E & error() & noexcept { return *value_; }
+  constexpr E && error() && noexcept { return std::move(*value_); }
+
+  template <typename F> constexpr auto transform(F && f) & {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)());
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && f) const & {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)());
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && f) && {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(f)());
+    }
+    return expected<G, error_type>(unexpect, std::move(*this).error());
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) & {
+    using G = std::invoke_result_t<F, E &>;
+    if (has_value()) {
+      return expected<value_type, G>(**this);
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) const & {
+    using G = std::invoke_result_t<F, E const &>;
+    if (has_value()) {
+      return expected<value_type, G>(**this);
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && f) && {
+    using G = std::invoke_result_t<F, E>;
+    if (has_value()) {
+      return expected<value_type, G>(*std::move(*this));
+    }
+    return expected<value_type, G>(unexpect, std::forward<F>(f)(std::move(*this).error()));
+  }
+
+private:
+  std::optional<E> value_;
+};
+}
+#endif
+
+namespace jvalidate::detail {
+inline std::string to_message(std::errc ec) { return std::make_error_code(ec).message(); }
+}

+ 9 - 0
include/jvalidate/detail/expect.h

@@ -97,3 +97,12 @@
   if (JVALIDATE_UNLIKELY(!(condition))) {                                                          \
     return __VA_ARGS__;                                                                            \
   }
+
+/**
+ * @brief Assert that a std::expected (or jvalidate::detail::expected) contains
+ * a real value, or else return the unexpected (error).
+ *
+ * @param exp The std::expected
+ */
+#define JVALIDATE_PROPIGATE_UNEXPECTED(exp)                                                        \
+  RETURN_UNLESS(exp.has_value(), jvalidate::detail::unexpected(std::move(exp).error()))

+ 15 - 7
include/jvalidate/detail/number.h

@@ -11,10 +11,12 @@
 #include <charconv>
 #include <cmath>
 #include <limits>
-#include <stdexcept>
 #include <string_view>
 #include <system_error>
 
+#include <jvalidate/compat/expected.h>
+#include <jvalidate/detail/out.h>
+
 namespace jvalidate::detail {
 /**
  * @brief Determine if a floating point number is actually an integer (in the
@@ -37,16 +39,22 @@ inline bool fits_in_integer(double number) {
  */
 inline bool fits_in_integer(uint64_t number) { return (number & 0x8000'0000'0000'0000) == 0; }
 
-template <std::integral I> I from_str(std::string_view str, int base = 10) {
-  I rval;
-  auto [end, ec] = std::from_chars(str.begin(), str.end(), rval, base);
+struct parse_integer_args {
+  int base = 10;
+  out<size_t> read = discard_out;
+};
 
+template <std::integral T>
+static expected<T, std::errc> parse_integer(std::string_view in, parse_integer_args args = {}) {
+  T rval = 0;
+  auto [ptr, ec] = std::from_chars(in.begin(), in.end(), rval, args.base);
+  args.read = ptr - in.begin();
   if (ec != std::errc{}) {
-    throw std::runtime_error(std::make_error_code(ec).message());
+    return unexpected(ec);
   }
 
-  if (end != str.end()) {
-    throw std::runtime_error("NaN: " + std::string(str));
+  if (ptr != in.end()) {
+    return unexpected(std::errc::result_out_of_range);
   }
 
   return rval;

+ 67 - 51
include/jvalidate/detail/pointer.h

@@ -2,14 +2,17 @@
 
 #include <algorithm>
 #include <cassert>
+#include <charconv>
 #include <iostream>
+#include <jvalidate/detail/expect.h>
 #include <string>
 #include <string_view>
+#include <system_error>
 #include <variant>
 #include <vector>
 
 #include <jvalidate/compat/compare.h>
-#include <jvalidate/detail/expect.h>
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/forward.h>
 
@@ -32,73 +35,86 @@ 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
-   * principle, this should either be a factory function returning an optional/
-   * throwing on error - but we'll generously assume that all JSON-Pointers are
-   * valid - and therefore that an invalidly formatter pointer string will
-   * point to somewhere non-existant (since it will be used in schema handling)
+   * @brief Parse a JSON-Pointer from a serialized JSON-Pointer-String.
+   *
+   * @param path A string representation of the Pointer
+   *
+   * @returns A Pointer, if the string is considered valid, else an error
+   * message describing the problem.
    */
-  Pointer(std::string_view path) {
+  static expected<Pointer, std::string> parse(std::string_view path) {
     if (path.empty()) {
-      return;
+      return Pointer();
     }
 
-    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(from_str<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] == '%') {
-          std::string_view enc = std::string_view(in).substr(i + 1, 2);
-          in.replace(i, 3, 1, from_str<char>(enc, 16));
-          continue;
-        } 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 {
-          JVALIDATE_THROW(std::runtime_error, "Illegal ~ code");
-        }
-      }
-      tokens_.push_back(std::move(in));
-    };
-
     // JSON-Pointers are required to start with a '/'.
-    EXPECT_M(path.starts_with('/'), "Missing leading '/' in JSON Pointer: " << path);
+    RETURN_UNLESS(path.starts_with('/'), unexpected("Missing leading '/' in JSON Pointer"));
     path.remove_prefix(1);
     // The rules of JSON-Pointer is that if a token were to contain a '/' as a
     // strict character: then that character would be escaped, using the above
     // rules. We take advantage of string_view's sliding view to make iteration
     // easy.
+    Pointer rval;
     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)));
+      expected token = parse_token(std::string(path.substr(0, p)));
+      JVALIDATE_PROPIGATE_UNEXPECTED(token);
+      rval.tokens_.push_back(*token);
+    }
+
+    expected token = parse_token(std::string(path));
+    JVALIDATE_PROPIGATE_UNEXPECTED(token);
+    rval.tokens_.push_back(*token);
+
+    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).transform_error(to_message);
     }
 
-    append_with_parse(std::string(path));
+    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.
+      if (in[i] == '%') {
+        if (expected code = parse_integer<char>(in.substr(i + 1, 2), {.base = 16})) {
+          in.replace(i, 3, 1, *code);
+          continue;
+        } else {
+          return unexpected(to_message(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;
   }
 
   /**
@@ -222,6 +238,6 @@ public:
   auto operator<=>(Pointer const &) const = default;
 
 private:
-  std::vector<std::variant<std::string, size_t>> tokens_{};
+  std::vector<Token> tokens_{};
 };
 }

+ 1 - 1
include/jvalidate/detail/reference.h

@@ -126,7 +126,7 @@ public:
     size_t pointer_start = 0;
     root_ = RootReference(ref, pointer_start);
     if (pointer_start != std::string::npos) {
-      pointer_ = ref.substr(pointer_start);
+      pointer_ = Pointer::parse(ref.substr(pointer_start)).value();
     }
   }
 

+ 22 - 8
include/jvalidate/detail/relative_pointer.h

@@ -4,6 +4,7 @@
 #include <string>
 #include <string_view>
 
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/detail/pointer.h>
@@ -26,24 +27,37 @@ namespace jvalidate::detail {
  */
 class RelativePointer {
 public:
-  RelativePointer(std::string_view path) {
+  static expected<RelativePointer, std::string> parse(std::string_view path) {
     if (path == "0") { // A literal RelativePointer of "0" simply means "here"
-      return;
+      return RelativePointer();
     }
 
-    if (size_t pos = path.find('/'); pos != path.npos) {
+    RelativePointer rval;
+    if (auto pos = path.find('/'); pos != path.npos) {
       // Handle the JSON-Pointer version
-      pointer_ = Pointer(path.substr(pos));
+      expected ptr = Pointer::parse(path.substr(pos));
+      JVALIDATE_PROPIGATE_UNEXPECTED(ptr);
+      rval.pointer_ = *std::move(ptr);
       path.remove_suffix(path.size() - pos);
-    } else if (path.ends_with('#')) {
-      requests_key_ = true;
+    } else if (path.back() == '#') {
+      rval.requests_key_ = true;
       path.remove_suffix(1);
     }
 
-    EXPECT_M(path == "0" || not path.starts_with("0"), "Cannot zero-prefix a relative pointer");
+    if (path.find_first_not_of("0123456789") != std::string_view::npos) {
+      return unexpected("RelativePointer must end in a pointer, or a '#'");
+    }
+
+    size_t read = 0;
+    expected parent_steps = parse_integer<size_t>(path, {.read = read});
+
+    JVALIDATE_PROPIGATE_UNEXPECTED(parent_steps.transform_error(to_message));
+    EXPECT_M(read == path.size(), "Extra chars in RelativePointer");
+
     // Will throw an exception if path contains any non-numeric characters, or
     // if path represents a number that is out of the bounds of a size_t.
-    parent_steps_ = from_str<size_t>(path);
+    rval.parent_steps_ = *parent_steps;
+    return rval;
   }
 
   /**

+ 2 - 1
tests/extension_test.cxx

@@ -20,7 +20,8 @@ using jvalidate::constraint::ExtensionConstraint;
 using testing::Not;
 
 struct IsKeyOfConstraint : jvalidate::extension::ConstraintBase<IsKeyOfConstraint> {
-  IsKeyOfConstraint(std::string_view ptr) : ptr(ptr) {
+  IsKeyOfConstraint(std::string_view ptr)
+      : ptr(jvalidate::detail::RelativePointer::parse(ptr).value()) {
     EXPECT_M(ptr.find('/') != std::string_view::npos,
              "IsKeyOfConstraint requires a value-relative-pointer, not a key-relative-pointer");
   }

+ 1 - 1
tests/matchers.h

@@ -10,7 +10,7 @@
 #include <json/value.h>
 
 inline auto operator""_jptr(char const * data, size_t len) {
-  return jvalidate::detail::Pointer(std::string_view{data, len});
+  return jvalidate::detail::Pointer::parse(std::string_view{data, len}).value();
 }
 
 inline Json::Value operator""_json(char const * data, size_t len) {