#2 refactor: change from using throws to using expected in situations...

Atvērta
sjjaffe vēlas sapludināt 25 revīzijas no sjjaffe/refactor/expected uz sjjaffe/feat/format-matcher

+ 5 - 4
include/jvalidate/adapter.h

@@ -12,6 +12,7 @@
 #include <utility>
 #include <vector>
 
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/enum.h>
 #include <jvalidate/forward.h>
@@ -390,13 +391,13 @@ public:
 };
 
 template <typename JSON>
-bool load_file(std::filesystem::path const & path, JSON & out, std::string & error) noexcept {
+auto load_file(std::filesystem::path const & path, JSON & out) noexcept
+    -> detail::expected<void, std::string> {
   std::ifstream in(path);
   if (in.bad()) {
-    error = "file error";
-    return false;
+    return detail::unexpected("file error");
   }
-  return load_stream(in, out, error);
+  return load_stream(in, out);
 }
 }
 

+ 8 - 3
include/jvalidate/adapters/jsoncpp.h

@@ -8,6 +8,7 @@
 #include <json/value.h>
 
 #include <jvalidate/adapter.h>
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/detail/simple_adapter.h>
 #include <jvalidate/enum.h>
@@ -16,9 +17,13 @@
 
 namespace jvalidate::adapter {
 template <>
-inline bool load_stream(std::istream & in, Json::Value & out, std::string & error) noexcept {
-  Json::CharReaderBuilder const builder;
-  return Json::parseFromStream(builder, in, &out, &error);
+inline auto load_stream(std::istream & in, Json::Value & out) noexcept
+    -> detail::expected<void, std::string> {
+  Json::CharReaderBuilder builder;
+  if (std::string error; not Json::parseFromStream(builder, in, &out, &error)) {
+    return detail::unexpected(error);
+  }
+  return {};
 }
 
 template <typename JSON> class JsonCppAdapter;

+ 7 - 6
include/jvalidate/compat/curl.h

@@ -7,6 +7,7 @@
 #include <curl/curl.h>
 
 #include <jvalidate/adapter.h>
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/forward.h>
 #include <jvalidate/uri.h>
 
@@ -19,7 +20,8 @@ inline size_t transfer_to_buffer(char * data, size_t size, size_t nmemb, void *
 }
 
 template <typename JSON>
-bool curl_get(jvalidate::URI const & uri, JSON & out, std::string & error) noexcept {
+auto curl_get(jvalidate::URI const & uri, JSON & out) noexcept
+    -> detail::expected<void, std::string> {
   using jvalidate::adapter::load_file;
   using jvalidate::adapter::load_stream;
   if (uri.scheme().starts_with("http")) {
@@ -34,15 +36,14 @@ bool curl_get(jvalidate::URI const & uri, JSON & out, std::string & error) noexc
       curl_easy_cleanup(curl);
 
       if (res == CURLE_OK) {
-        return load_stream(ss, out, error);
+        return load_stream(ss, out);
       }
     }
-    return false;
+    return detail::unexpected("curl error");
   }
   if (uri.scheme() == "file") {
-    return load_file(uri.resource(), out, error);
+    return load_file(uri.resource(), out);
   }
-  error = "unknown scheme";
-  return false;
+  return detail::unexpected("unknown scheme");
 }
 }

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

@@ -0,0 +1,351 @@
+#pragma once
+// NOLINTBEGIN(readability-identifier-naming)
+
+#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>
+    requires(not std::same_as<E, unexpected<E>>)
+  constexpr explicit unexpected(Err && err) : error_(std::forward<Err>(err)) {}
+  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> init, Args &&... args)
+      : error_{init, 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 const & lhs, unexpected<E2> const & rhs) {
+    return lhs.error() == rhs.error();
+  }
+
+  friend constexpr void swap(unexpected & lhs, unexpected & rhs) noexcept(noexcept(lhs.swap(rhs))) {
+    return lhs.swap(rhs);
+  }
+
+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:
+  explicit bad_expected_access(E error) : error_(std::move(error)) {}
+
+  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>>
+    requires(not std::same_as<U, expected<T, E>>)
+  constexpr explicit(!std::is_convertible_v<U, T>) expected(U && val)
+      : value_(std::in_place_index<0>, std::forward<U>(val)) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<const G &, E>) expected(unexpected<G> const & exp)
+      : value_(std::in_place_index<1>, exp.error()) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<G, E>) expected(unexpected<G> && exp)
+      : value_(std::in_place_index<1>, std::move(exp).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> init, Args &&... args)
+      : value_(std::in_place_index<0>, init, 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> init, Args &&... args)
+      : value_(std::in_place_index<1>, init, 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 & {
+    if (has_value()) [[likely]] {
+      return operator*();
+    }
+    throw bad_expected_access(error());
+  }
+
+  constexpr T & value() & {
+    if (has_value()) [[likely]] {
+      return operator*();
+    }
+    throw bad_expected_access(std::as_const(error()));
+  }
+
+  constexpr T && value() && {
+    if (has_value()) [[likely]] {
+      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 && func) & {
+    using G = std::invoke_result_t<F, value_type &>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)(**this));
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && func) const & {
+    using G = std::invoke_result_t<F, value_type const &>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)(**this));
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && func) && {
+    using G = std::invoke_result_t<F, value_type>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)(*std::move(*this)));
+    }
+    return expected<G, error_type>(unexpect, std::move(*this).error());
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) & {
+    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>(func)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) 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>(func)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) && {
+    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>(func)(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 & exp)
+      : value_(exp.error()) {}
+
+  template <typename G>
+  constexpr explicit(!std::is_convertible_v<G, E>) expected(unexpected<G> && exp)
+      : value_(std::move(exp).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> init, Args &&... args)
+      : value_(std::in_place, init, 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 (has_value()) [[likely]] {
+      return operator*();
+    }
+    throw bad_expected_access(error());
+  }
+
+  void value() && noexcept {
+    if (has_value()) [[likely]] {
+      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 && func) & {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)());
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && func) const & {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)());
+    }
+    return expected<G, error_type>(unexpect, error());
+  }
+
+  template <typename F> constexpr auto transform(F && func) && {
+    using G = std::invoke_result_t<F>;
+    if (has_value()) {
+      return expected<G, error_type>(std::forward<F>(func)());
+    }
+    return expected<G, error_type>(unexpect, std::move(*this).error());
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) & {
+    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>(func)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) 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>(func)(error()));
+  }
+
+  template <typename F> constexpr auto transform_error(F && func) && {
+    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>(func)(std::move(*this).error()));
+  }
+
+private:
+  std::optional<E> value_;
+};
+}
+#endif
+
+namespace jvalidate::detail {
+inline std::string to_message(std::errc ecode) { return std::make_error_code(ecode).message(); }
+}
+// NOLINTEND(readability-identifier-naming)

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

@@ -3,6 +3,8 @@
 #include <iostream> // IWYU pragma: keep
 #include <sstream>  // IWYU pragma: keep
 
+#include <jvalidate/_macro.h>
+
 #if defined(JVALIDATE_USE_EXCEPTIONS)
 /**
  * @brief Throw an exception after construcing the error message.
@@ -73,3 +75,12 @@
  * If the condition is FALSE, then the other params are used to produce errors.
  */
 #define EXPECT(condition) EXPECT_M(condition, #condition " at " __FILE__ ":" << __LINE__)
+
+/**
+ * @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)                                                        \
+  JVALIDATE_RETURN_UNLESS(exp.has_value(), jvalidate::detail::unexpected(std::move(exp).error()))

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

@@ -13,11 +13,12 @@
 #include <concepts>
 #include <cstdint>
 #include <limits>
-#include <stdexcept>
-#include <string>
 #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
@@ -41,17 +42,22 @@ inline bool fits_in_integer(double number) {
 // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
 inline bool fits_in_integer(uint64_t number) { return (number & 0x8000'0000'0000'0000) == 0; }
 
-// NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
-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 ParseIntegerArgs {
+  int base = 10; // NOLINT(cppcoreguidelines-avoid-magic-numbers)
+  out<size_t> read = discard_out;
+};
 
+template <std::integral T>
+static expected<T, std::errc> parse_integer(std::string_view in, ParseIntegerArgs 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;

+ 65 - 55
include/jvalidate/detail/pointer.h

@@ -8,7 +8,6 @@
 #include <stdexcept> // IWYU pragma: keep
 #include <string>
 #include <string_view>
-#include <utility>
 #include <variant>
 #include <vector>
 
@@ -36,77 +35,88 @@ struct parent_t {};        // NOLINT(readability-identifier-naming)
 constexpr parent_t parent; // NOLINT(readability-identifier-naming)
 
 class Pointer {
+private:
+  using Token = std::variant<std::string, size_t>;
+
 public:
   Pointer() = default;
-  explicit(false) Pointer(std::vector<std::variant<std::string, size_t>> const & tokens)
-      : tokens_(tokens) {}
+  explicit(false) 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.
    */
-  explicit(false) 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) {
-        tokens_.emplace_back(from_str<size_t>(in));
-        return;
-      }
-
-      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 const enc = std::string_view(in).substr(i + 1, 2);
-          // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
-          in.replace(i, 3, 1, from_str<char>(enc, 16));
-          continue;
-        }
-        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_.emplace_back(std::move(in));
-    };
-
     // JSON-Pointers are required to start with a '/'.
-    EXPECT_M(path.starts_with('/'), "Missing leading '/' in JSON Pointer: " << path);
+    JVALIDATE_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 pos = path.find('/'); pos != std::string::npos;
          path.remove_prefix(pos + 1), pos = path.find('/')) {
-      append_with_parse(std::string(path.substr(0, pos)));
+      expected token = parse_token(std::string(path.substr(0, pos)));
+      JVALIDATE_PROPIGATE_UNEXPECTED(token);
+      rval.tokens_.push_back(*token);
     }
 
-    append_with_parse(std::string(path));
+    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);
+    }
+
+    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);
+        } else {
+          return unexpected(to_message(code.error()));
+        }
+        continue;
+      }
+      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;
   }
 
   /**
@@ -233,6 +243,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

@@ -129,7 +129,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();
     }
   }
 

+ 18 - 7
include/jvalidate/detail/relative_pointer.h

@@ -6,6 +6,7 @@
 #include <string_view>
 #include <variant>
 
+#include <jvalidate/compat/expected.h>
 #include <jvalidate/detail/expect.h>
 #include <jvalidate/detail/number.h>
 #include <jvalidate/detail/pointer.h>
@@ -28,24 +29,34 @@ namespace jvalidate::detail {
  */
 class RelativePointer {
 public:
-  explicit(false) 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();
     }
 
+    RelativePointer rval;
     if (size_t const pos = path.find('/'); pos != std::string_view::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;
+      rval.requests_key_ = true;
       path.remove_suffix(1);
     }
 
-    EXPECT_M(path == "0" || not path.starts_with("0"), "Cannot zero-prefix a relative pointer");
-    // Will throw an exception if path contains any non-numeric characters, or
+    if (path.starts_with('0') && path != "0") {
+      return unexpected("Cannot zero-prefix a relative pointer");
+    }
+
+    size_t read = 0;
+    expected parent_steps = parse_integer<size_t>(path, {.read = read});
+    // Will return unexpected 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);
+    JVALIDATE_PROPIGATE_UNEXPECTED(parent_steps.transform_error(to_message));
+    rval.parent_steps_ = *parent_steps;
+    return rval;
   }
 
   /**

+ 1 - 1
include/jvalidate/document_cache.h

@@ -65,7 +65,7 @@ public:
     }
 
     auto [it, created] = cache_.try_emplace(uri);
-    if (created && not resolve_(uri, it->second, error)) {
+    if (created && not resolve_(uri, it->second)) {
       // Doing it this way skips out on a move operation for the JSON object,
       // which could be useful if someone is using a legacy JSON object type.
       // Since std::map promises stability we don't need to concern ourselves

+ 1 - 4
include/jvalidate/format.h

@@ -714,10 +714,7 @@ template <typename CharT> inline bool email(std::basic_string_view<CharT> em) {
 }
 
 template <typename T> inline bool ctor_as_valid(std::string_view str) {
-  try {
-    [[maybe_unused]] auto _ = T(str);
-    return true;
-  } catch (std::exception const &) { return false; }
+  return T::parse(str).has_value();
 }
 
 #if JVALIDATE_HAS_IDNA

+ 8 - 2
include/jvalidate/forward.h

@@ -13,6 +13,8 @@
 #include <utility>
 #include <variant>
 
+#include <jvalidate/compat/expected.h>
+
 #define DISCARD1_IMPL(_, ...) __VA_ARGS__
 #define DISCARD1(...) DISCARD1_IMPL(__VA_ARGS__)
 
@@ -22,6 +24,9 @@
 
 namespace jvalidate::detail {
 }
+namespace jvalidate::adapter::detail {
+using namespace jvalidate::detail;
+}
 namespace jvalidate::format::detail {
 using namespace jvalidate::detail;
 }
@@ -47,7 +52,7 @@ template <typename V> struct AdapterTraits<V const> : AdapterTraits<V> {};
 
 template <typename JSON> using AdapterFor = AdapterTraits<JSON>::template Adapter<JSON>;
 template <typename JSON>
-bool load_stream(std::istream & in, JSON & out, std::string & error) noexcept;
+auto load_stream(std::istream & in, JSON & out) noexcept -> detail::expected<void, std::string>;
 }
 
 namespace jvalidate::schema {
@@ -208,7 +213,8 @@ template <Adapter Root, RegexEngine RE, typename ExtensionVisitor> class Validat
 template <RegexEngine RE, typename ExtensionVisitor> class Validator;
 
 template <Adapter A>
-using URIResolver = bool (*)(URI const &, typename A::value_type &, std::string &) noexcept;
+using URIResolver = auto (*)(URI const &, typename A::value_type &) noexcept
+    -> detail::expected<void, std::string>;
 }
 
 namespace jvalidate::extension {

+ 5 - 6
src/validate.cxx

@@ -64,12 +64,12 @@ int main(int argc, char const * const * argv) { // NOLINT
 
   Json::Value jschema;
   Json::Value jobject;
-  if (std::string error; !load_file(args.schema, jschema, error)) {
-    std::cerr << "Error loading schema: " << error << "\n";
+  if (auto result = load_file(args.schema, jschema); not result) {
+    std::cerr << "Error loading schema: " << result.error() << "\n";
     return EXIT_FAILURE;
   }
-  if (std::string error; !load_file(args.object, jobject, error)) {
-    std::cerr << "Error loading schema: " << error << "\n";
+  if (auto result = load_file(args.object, jobject); not result) {
+    std::cerr << "Error loading schema: " << result.error() << "\n";
     return EXIT_FAILURE;
   }
 
@@ -93,8 +93,7 @@ int main(int argc, char const * const * argv) { // NOLINT
   {
     std::stringstream ss;
     ss << result;
-    std::string _;
-    load_stream(ss, json, _);
+    load_stream(ss, json);
   }
 
   std::map<std::string, Json::Value> because;

+ 26 - 34
tests/detail_test.cxx

@@ -25,8 +25,8 @@ using JsonCppAdapter = jvalidate::adapter::JsonCppAdapter<Json::Value const>;
 using jvalidate::adapter::Type;
 
 using testing::Eq;
+using testing::HasSubstr;
 using testing::Test;
-using testing::ThrowsMessage;
 using testing::Types;
 using testing::VariantWith;
 
@@ -54,65 +54,58 @@ TEST(PointerTest, BackCoercesIntToString) {
 TEST(PointerTest, BackIsEmptySafe) { EXPECT_THAT(""_jptr.back(), ""); }
 
 TEST(PointerTest, ForbidsBadTilde) {
-  EXPECT_NO_THROW("/~1"_jptr);
-  EXPECT_THROW("/~2"_jptr, std::runtime_error);
+  EXPECT_THAT(Pointer::parse("/~1"), Expected(testing::_));
+  EXPECT_THAT(Pointer::parse("/~2"), Unexpected(HasSubstr("illegal tilde")));
 }
 
 TEST(PointerTest, CanConcatenate) { EXPECT_THAT("/A"_jptr / "/B"_jptr, "/A/B"_jptr); }
 
 TEST(PointerTest, CanGoToParent) { EXPECT_THAT("/A/B"_jptr / parent, "/A"_jptr); }
 
-TEST(PointerTest, Print) { EXPECT_THAT(testing::PrintToString(Pointer("/B/0/A")), "/B/0/A"); }
+TEST(PointerTest, Print) { EXPECT_THAT(testing::PrintToString("/B/0/A"_jptr), "/B/0/A"); }
 
 TEST(RelatvivePointerTest, CannotPrefixWithZero) {
-  EXPECT_THROW(RelativePointer("01"), std::runtime_error);
+  EXPECT_THAT(RelativePointer::parse("01"), Unexpected("Cannot zero-prefix a relative pointer"));
 }
 
 TEST(RelatvivePointerTest, Print) {
-  EXPECT_THAT(testing::PrintToString(RelativePointer("0")), "0");
-  EXPECT_THAT(testing::PrintToString(RelativePointer("1#")), "1#");
-  EXPECT_THAT(testing::PrintToString(RelativePointer("1/B/0/A")), "1/B/0/A");
+  EXPECT_THAT(testing::PrintToString("0"_relptr), "0");
+  EXPECT_THAT(testing::PrintToString("1#"_relptr), "1#");
+  EXPECT_THAT(testing::PrintToString("1/B/0/A"_relptr), "1/B/0/A");
 }
 
 TEST(RelatvivePointerTest, ZeroIsHere) {
   Json::Value json;
   json["A"] = 1;
-  RelativePointer const rel("0");
-  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
-              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["A"])))
-      << rel;
+  EXPECT_THAT("0"_relptr.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["A"])));
 }
 
 TEST(RelatvivePointerTest, CanWalkBackwards) {
   Json::Value json;
   json["A"] = 1;
-  RelativePointer const rel("1");
-  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
-              VariantWith<JsonCppAdapter>(JsonCppAdapter(json)))
-      << rel;
+  EXPECT_THAT("1"_relptr.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json)));
 }
 
 TEST(RelatvivePointerTest, CanFetchKey) {
   Json::Value json;
   json["A"] = 1;
-  RelativePointer const rel("0#");
-  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)), VariantWith<std::string>(Eq("A")))
-      << rel;
+  EXPECT_THAT("0#"_relptr.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<std::string>(Eq("A")));
 }
 
 TEST(RelatvivePointerTest, CanGoUpAndDown) {
   Json::Value json;
   json["A"] = 1;
   json["B"] = 2;
-  RelativePointer const rel("1/B");
-  EXPECT_THAT(rel.inspect("/A"_jptr, JsonCppAdapter(json)),
-              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["B"])))
-      << rel;
+  EXPECT_THAT("1/B"_relptr.inspect("/A"_jptr, JsonCppAdapter(json)),
+              VariantWith<JsonCppAdapter>(JsonCppAdapter(json["B"])));
 }
 
 TEST(ReferenceTest, Print) {
   EXPECT_THAT(testing::PrintToString(Reference(jvalidate::URI("file://path/to/document.json"),
-                                               Anchor("Anchor"), Pointer("/key/1/id"))),
+                                               Anchor("Anchor"), "/key/1/id"_jptr)),
               "file://path/to/document.json#Anchor/key/1/id");
 }
 
@@ -153,30 +146,29 @@ TEST(NumberTest, DoubleOutOfIntegerRange) {
   EXPECT_FALSE(fits_in_integer(-10000000000000000000.0));
 }
 
-TYPED_TEST(NumberFromStrTest, NumberParsesIntegers) { EXPECT_THAT(from_str<TypeParam>("10"), 10); }
+TYPED_TEST(NumberFromStrTest, NumberParsesIntegers) {
+  EXPECT_THAT(parse_integer<TypeParam>("10"), Expected(10));
+}
 
 TYPED_TEST(NumberFromStrTest, NumberParsesNegativeIntegers) {
   if constexpr (std::is_signed_v<TypeParam>) {
-    EXPECT_THAT(from_str<TypeParam>("-10"), -10);
+    EXPECT_THAT(parse_integer<TypeParam>("-10"), Expected(-10));
   } else {
-    EXPECT_THAT([] { from_str<TypeParam>("-10"); },
-                ThrowsMessage<std::runtime_error>("Invalid argument"));
+    EXPECT_THAT(parse_integer<TypeParam>("-10"), Unexpected(std::errc::invalid_argument));
   }
 }
 
 TYPED_TEST(NumberFromStrTest, ThrowsOnPlusSign) {
-  EXPECT_THAT([] { from_str<TypeParam>("+10"); },
-              ThrowsMessage<std::runtime_error>("Invalid argument"));
+  EXPECT_THAT(parse_integer<TypeParam>("+10"), Unexpected(std::errc::invalid_argument));
 }
 
 TYPED_TEST(NumberFromStrTest, ThrowsOnTooManyChars) {
-  EXPECT_THAT([] { from_str<TypeParam>("10 coconuts"); },
-              ThrowsMessage<std::runtime_error>("NaN: 10 coconuts"));
+  EXPECT_THAT(parse_integer<TypeParam>("10 coconuts"), Unexpected(std::errc::result_out_of_range));
 }
 
 TYPED_TEST(NumberFromStrTest, ThrowsOnOutOfRange) {
-  EXPECT_THAT([] { from_str<TypeParam>("99999999999999999999999999999"); },
-              ThrowsMessage<std::runtime_error>("Result too large"));
+  EXPECT_THAT(parse_integer<TypeParam>("99999999999999999999999999999"),
+              Unexpected(std::errc::result_out_of_range));
 }
 
 TEST(StringAdapterTest, IsStringy) {

+ 2 - 1
tests/extension_test.cxx

@@ -22,7 +22,8 @@ using jvalidate::constraint::ExtensionConstraint;
 using testing::Not;
 
 struct IsKeyOfConstraint : jvalidate::extension::ConstraintBase<IsKeyOfConstraint> {
-  explicit IsKeyOfConstraint(std::string_view ptr) : ptr(ptr) {
+  explicit 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");
   }

+ 20 - 1
tests/matchers.h

@@ -2,13 +2,18 @@
 #include <gtest/gtest.h>
 
 #include <jvalidate/detail/pointer.h>
+#include <jvalidate/detail/relative_pointer.h>
 #include <jvalidate/validation_result.h>
 
 #include <json/reader.h>
 #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 auto operator""_relptr(char const * data, size_t len) {
+  return jvalidate::detail::RelativePointer::parse(std::string_view{data, len}).value();
 }
 
 inline Json::Value operator""_json(char const * data, size_t len) {
@@ -26,6 +31,20 @@ inline Json::Value operator""_json(char const * data, size_t len) {
   return value;
 }
 
+MATCHER_P(Expected, m, "") {
+  if (arg.has_value()) {
+    return testing::ExplainMatchResult(m, arg.value(), result_listener);
+  }
+  return false;
+}
+
+MATCHER_P(Unexpected, m, "") {
+  if (not arg.has_value()) {
+    return testing::ExplainMatchResult(m, arg.error(), result_listener);
+  }
+  return false;
+}
+
 MATCHER(Valid, "") { return arg.valid(); }
 
 MATCHER_P(HasAnnotationsFor, doc_path, "") { return arg.has(doc_path); }

+ 4 - 6
tests/selfvalidate_test.cxx

@@ -30,14 +30,13 @@ using testing::TestWithParam;
 
 using jvalidate::adapter::load_file;
 
-bool load_external_for_test(jvalidate::URI const & uri, Json::Value & out,
-                            std::string & error) noexcept {
+auto load_external_for_test(jvalidate::URI const & uri, Json::Value & out) noexcept {
   constexpr std::string_view g_fake_url = "localhost:1234/";
   if (uri.scheme().starts_with("http") && uri.resource().starts_with(g_fake_url)) {
     std::string_view path = uri.resource().substr(g_fake_url.size());
-    return load_file(JSONSchemaTestSuiteDir() / "remotes" / path, out, error);
+    return load_file(JSONSchemaTestSuiteDir() / "remotes" / path, out);
   }
-  return jvalidate::curl_get(uri, out, error);
+  return jvalidate::curl_get(uri, out);
 }
 
 class JsonSchemaTest : public TestWithParam<SchemaParams> {
@@ -101,8 +100,7 @@ TEST_P(JsonSchemaTest, TestSuite) {
   auto const & [version, file] = GetParam();
   Json::Value spec;
 
-  std::string error;
-  EXPECT_TRUE(load_file(file, spec, error)) << error;
+  EXPECT_TRUE(load_file(file, spec));
 
   bool is_format = file.string().find("optional/format") != std::string::npos;
   for (auto const & suite : spec) {