Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/content/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,55 @@ if (my_result) {
}
```

## Locating errors

`.what()` returns a message meant to be read by humans. When an error summarizes several failures, for example because more than one field is invalid, the message lists all of them:

```
Found 2 errors:
1) Failed to parse field 'inputFile': Size validation failed: Value expected to be greater than or equal to 5, but got 4.
2) Field named 'outputFile' not found.
```

If your program needs to know where each failure occurred, for instance to show each message next to the right input in a form, use `.details()` instead. It returns a `std::vector<rfl::ErrorDetail>` with one entry per failure:

```cpp
struct ErrorDetail {
using PathSegment = std::variant<std::string, size_t>;

// Field names and array indices, starting from the root.
std::vector<PathSegment> path;

// The message, without the "Failed to parse field '...': " prefixes.
std::string message;
};
```

Field names are spelled as they appear in the input, that is, after processors such as `rfl::SnakeCaseToCamelCase` or `rfl::SnakeCaseToKebabCase` (which `rfl::cli::read` uses) have been applied. This means the paths line up with the JSON schema generated by `rfl::json::to_schema`.

```cpp
struct FileSpec {
rfl::Validator<std::string, rfl::Size<rfl::Minimum<5>>> input_file;
std::string output_file;
};

struct Request {
std::vector<FileSpec> files;
};

const auto result = rfl::json::read<Request, rfl::SnakeCaseToCamelCase>(
R"({"files":[{"inputFile":"a.txt","outputFile":"a.out"},{"inputFile":"b"}]})");

for (const auto& detail : result.error().details()) {
// First iteration: path = {"files", 1, "inputFile"},
// message = "Size validation failed: ..."
// Second iteration: path = {"files", 1, "outputFile"},
// message = "Field named 'outputFile' not found."
}
```

An error that does not belong to any particular field, such as a syntax error in the document, is reported as a single detail with an empty path. Untagged variants are also reported as a single detail at the location of the variant, because reflect-cpp cannot know which alternative the input was meant to be. For `rfl::TaggedUnion`, the tag determines the alternative, so the paths extend into the fields of that alternative.

## `rfl::error`

`rfl::error("error message")` is a simple utility function for `rfl::Unexpected<Error>(Error("error message"))`.
Expand Down
6 changes: 6 additions & 0 deletions include/rfl/Result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace rfl {

using Error = rfl::internal::Error;
using ErrorDetail = rfl::internal::ErrorDetail;

/// To be returned when there is nothing to return, but there might be an error.
struct Nothing {};
Expand Down Expand Up @@ -50,6 +51,11 @@ inline Unexpected<Error> error(const Error& _err) {
return Unexpected<Error>(_err);
}

/// Shorthand for unexpected error.
inline Unexpected<Error> error(Error&& _err) {
return Unexpected<Error>(std::move(_err));
}

} // namespace rfl

#ifdef REFLECTCPP_USE_STD_EXPECTED
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/avro/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ struct Reader {
"): " + avro_strerror());
}
if constexpr (requires { _array_reader.reserve(size); }) {
if (const auto err = _array_reader.reserve(size)) {
if (auto err = _array_reader.reserve(size)) {
return err;
}
}
Expand All @@ -233,7 +233,7 @@ struct Reader {
return Error(std::string(__FUNCTION__) + " error(" +
std::to_string(res) + "): " + avro_strerror());
}
const auto err = _array_reader.read(InputVarType{&element});
auto err = _array_reader.read(InputVarType{&element});
if (err) {
return err;
}
Expand Down
2 changes: 1 addition & 1 deletion include/rfl/boost_serialization/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ struct Reader {
const auto size = _map.size;
for (size_t i = 0; i < size; ++i) {
const auto var = InputVarType{_map.ar};
if (const auto err = _reader.read(var, var)) {
if (auto err = _reader.read(var, var)) {
return err;
}
}
Expand Down
2 changes: 1 addition & 1 deletion include/rfl/bson/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ struct Reader {
if (bson_init_static(&b, doc.data, doc.data_len)) {
if (bson_iter_init(&iter, &b)) {
while (bson_iter_next(&iter)) {
const auto err = _array_reader.read(to_input_var(&iter));
auto err = _array_reader.read(to_input_var(&iter));
if (err) {
return err;
}
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/capnproto/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ class RFL_API Reader {
std::optional<Error> read_array(const ArrayReader& _array_reader,
const InputArrayType& _arr) const noexcept {
if constexpr (requires { _array_reader.reserve(_arr.val_.size()); }) {
if (const auto err = _array_reader.reserve(_arr.val_.size())) {
if (auto err = _array_reader.reserve(_arr.val_.size())) {
return err;
}
}
for (auto element : _arr.val_) {
const auto err = _array_reader.read(InputVarType{std::move(element)});
auto err = _array_reader.read(InputVarType{std::move(element)});
if (err) {
return err;
}
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/cbor/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,12 @@ class Reader {
std::optional<Error> read_array(const ArrayReader& _array_reader,
const InputArrayType& _arr) const noexcept {
if constexpr (requires { _array_reader.reserve(_arr.val_->size()); }) {
if (const auto err = _array_reader.reserve(_arr.val_->size())) {
if (auto err = _array_reader.reserve(_arr.val_->size())) {
return err;
}
}
for (auto& val : _arr.val_->array_range()) {
const auto err = _array_reader.read(InputVarType{&val});
auto err = _array_reader.read(InputVarType{&val});
if (err) {
return err;
}
Expand Down
2 changes: 1 addition & 1 deletion include/rfl/cereal/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ struct Reader {
(*_map.archive_)(::cereal::make_size_tag(size));
for (size_t i = 0; i < size; ++i) {
const auto var = InputVarType{_map.archive_};
if (const auto err = _reader.read(var, var)) {
if (auto err = _reader.read(var, var)) {
return err;
}
}
Expand Down
2 changes: 1 addition & 1 deletion include/rfl/cli/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ struct Reader {
const InputArrayType& _arr
) const noexcept {
for (const auto& val : _arr.values) {
const auto err = _array_reader.read(
auto err = _array_reader.read(
InputVarType{nullptr, "", val});
if (err) {
return err;
Expand Down
2 changes: 1 addition & 1 deletion include/rfl/env/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ struct Reader {
if (is_empty(var)) {
break;
}
const auto err = _array_reader.read(var);
auto err = _array_reader.read(var);
if (err) {
return err;
}
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/flexbuf/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ struct Reader {
const InputArrayType& _arr) const noexcept {
const auto size = _arr.size();
if constexpr (requires { _array_reader.reserve(size); }) {
if (const auto err = _array_reader.reserve(size)) {
if (auto err = _array_reader.reserve(size)) {
return err;
}
}
for (size_t i = 0; i < size; ++i) {
const auto err = _array_reader.read(InputVarType(_arr[i]));
auto err = _array_reader.read(InputVarType(_arr[i]));
if (err) {
return err;
}
Expand Down
4 changes: 2 additions & 2 deletions include/rfl/generic/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ struct Reader {
std::optional<Error> read_array(const ArrayReader& _array_reader,
const InputArrayType& _arr) const noexcept {
if constexpr (requires { _array_reader.reserve(_arr.size()); }) {
if (const auto err = _array_reader.reserve(_arr.size())) {
if (auto err = _array_reader.reserve(_arr.size())) {
return err;
}
}
for (const auto& v : _arr) {
const auto err = _array_reader.read(InputVarType(v));
auto err = _array_reader.read(InputVarType(v));
if (err) {
return err;
}
Expand Down
112 changes: 112 additions & 0 deletions include/rfl/internal/Error.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
#ifndef RFL_INTERNAL_ERROR_HPP_
#define RFL_INTERNAL_ERROR_HPP_

#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <variant>
#include <vector>

#include "cold.hpp"

namespace rfl::internal {

/// Describes a single failure contained in an Error: where it occurred and
/// what went wrong.
struct ErrorDetail {
/// A field name or an array index.
using PathSegment = std::variant<std::string, size_t>;

/// The location of the failure, starting from the root of the value being
/// read. Field names are spelled as they appear in the input, that is, after
/// processors such as rfl::SnakeCaseToCamelCase have been applied. An empty
/// path refers to the root value itself.
std::vector<PathSegment> path;

/// The message describing the failure, without the "Failed to parse field
/// '...': " prefixes that Error::what() contains for every level of nesting.
std::string message;

bool operator==(const ErrorDetail&) const = default;
};

/// Defines the error class to be returned when something went wrong.
/// This is used throughout reflect-cpp for error handling.
class Error {
Expand All @@ -20,6 +46,15 @@ class Error {
/// @param _what The error message describing what went wrong (will be moved)
Error(std::string&& _what) : what_(std::move(_what)) {}

/// Constructs an error with a message and the individual failures it
/// summarizes.
/// @param _what The error message describing what went wrong
/// @param _details The individual failures, see details()
Error(std::string _what, std::vector<ErrorDetail> _details)
: what_(std::move(_what)),
details_(_details.empty() ? OptionalDetails()
: OptionalDetails(std::move(_details))) {}

/// Destructor.
~Error() = default;

Expand Down Expand Up @@ -47,9 +82,86 @@ class Error {
/// from state
std::string what() && { return std::move(what_); }

/// Returns the individual failures summarized by what(), each with the
/// location at which it occurred. An error that was constructed from a
/// message only is reported as a single failure at the root.
std::vector<ErrorDetail> details() const& {
if (!details_.has_value()) {
return {ErrorDetail{.path = {}, .message = what_}};
}
return *details_;
}

/// Moves the individual failures out of the Error object and leaves it in a
/// moved from state.
std::vector<ErrorDetail> details() && {
if (!details_.has_value()) {
return {ErrorDetail{.path = {}, .message = std::move(what_)}};
}
return std::move(*details_);
}

private:
/// Either nothing or a heap-allocated vector of details, deep-copied along
/// with the Error. Holding nothing is the common case: it means that the
/// Error is just its message, and it does not allocate.
///
/// Errors are created and destroyed in many places, mostly as part of
/// rfl::Result. Deleting and copying the details out of line keeps that code
/// small, which matters for the performance of successful reads.
class OptionalDetails {
public:
/// Holds nothing.
OptionalDetails() = default;

/// Holds a heap-allocated vector containing _details.
explicit OptionalDetails(std::vector<ErrorDetail>&& _details)
: ptr_(new std::vector<ErrorDetail>(std::move(_details))) {}

/// Holds a copy of the details _other holds, if any.
OptionalDetails(const OptionalDetails& _other) : ptr_(copy(_other.ptr_)) {}

OptionalDetails(OptionalDetails&&) noexcept = default;

/// Holds a copy of the details _other holds, if any.
OptionalDetails& operator=(const OptionalDetails& _other) {
ptr_ = copy(_other.ptr_);
return *this;
}

OptionalDetails& operator=(OptionalDetails&&) noexcept = default;

bool has_value() const { return ptr_ != nullptr; }

/// Must only be called if has_value() is true.
const std::vector<ErrorDetail>& operator*() const { return *ptr_; }

/// Must only be called if has_value() is true.
std::vector<ErrorDetail>& operator*() { return *ptr_; }

private:
struct Delete {
RFL_COLD void operator()(std::vector<ErrorDetail>* _ptr) const {
delete _ptr;
}
};

using Ptr = std::unique_ptr<std::vector<ErrorDetail>, Delete>;

RFL_COLD static Ptr copy(const Ptr& _ptr) {
return _ptr ? Ptr(new std::vector<ErrorDetail>(*_ptr)) : nullptr;
}

Ptr ptr_;
};

private:
/// Documents what went wrong
std::string what_;

/// The individual failures summarized by what_. Holds nothing, unless what_
/// was assembled from failures at nested locations.
OptionalDetails details_;
};

} // namespace rfl::internal
Expand Down
15 changes: 15 additions & 0 deletions include/rfl/internal/cold.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef RFL_INTERNAL_COLD_HPP_
#define RFL_INTERNAL_COLD_HPP_

/// Marks a function that is rarely called, such as one that only runs when
/// something went wrong. Such functions are kept out of the code of their
/// callers, so that they do not slow down the common case.
#if defined(_MSC_VER)
#define RFL_COLD __declspec(noinline)
#elif defined(__GNUC__) || defined(__clang__)
#define RFL_COLD __attribute__((noinline, cold))
#else
#define RFL_COLD
#endif

#endif
4 changes: 2 additions & 2 deletions include/rfl/json/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ struct Reader {
const InputArrayType& _arr) const noexcept {
const auto size = yyjson_arr_size(_arr.val_);
if constexpr (requires { _array_reader.reserve(size); }) {
if (const auto err = _array_reader.reserve(size)) {
if (auto err = _array_reader.reserve(size)) {
return err;
}
}
yyjson_val* val;
yyjson_arr_iter iter;
yyjson_arr_iter_init(_arr.val_, &iter);
while ((val = yyjson_arr_iter_next(&iter))) {
const auto err = _array_reader.read(InputVarType(val));
auto err = _array_reader.read(InputVarType(val));
if (err) {
return err;
}
Expand Down
6 changes: 3 additions & 3 deletions include/rfl/msgpack/Reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,12 @@ struct Reader {
std::optional<Error> read_array(const ArrayReader& _array_reader,
const InputArrayType& _arr) const noexcept {
if constexpr (requires { _array_reader.reserve(_arr.size); }) {
if (const auto err = _array_reader.reserve(_arr.size)) {
if (auto err = _array_reader.reserve(_arr.size)) {
return err;
}
}
for (uint32_t i = 0; i < _arr.size; ++i) {
const auto err = _array_reader.read(_arr.ptr[i]);
auto err = _array_reader.read(_arr.ptr[i]);
if (err) {
return err;
}
Expand All @@ -189,7 +189,7 @@ struct Reader {
std::optional<Error> read_typed_map(
const MapReader& _reader, const InputObjectType& _map) const noexcept {
for (uint32_t i = 0; i < _map.size; ++i) {
if (const auto err = _reader.read(_map.ptr[i].key, _map.ptr[i].val)) {
if (auto err = _reader.read(_map.ptr[i].key, _map.ptr[i].val)) {
return err;
}
}
Expand Down
Loading
Loading