From 5b90f3c93b4445bea9c2587bba67b5430a3d5f8f Mon Sep 17 00:00:00 2001 From: George Evmenov Date: Sun, 18 Oct 2026 05:08:11 +0300 Subject: [PATCH] Add opt-in FailFast processor for object field errors --- benchmarks/all/parser_inlining_cereal.cpp | 15 +- benchmarks/all/parser_inlining_json.cpp | 17 ++- benchmarks/all/parser_inlining_msgpack.cpp | 15 +- benchmarks/all/parser_inlining_yas.cpp | 30 ++++ docs/content/processors.md | 19 +++ include/rfl.hpp | 1 + include/rfl/FailFast.hpp | 20 +++ include/rfl/internal/fail_fast_v.hpp | 26 ++++ include/rfl/parsing/FieldErrors.hpp | 48 ++++++ include/rfl/parsing/NamedTupleParser.hpp | 43 ++++-- include/rfl/parsing/ViewReader.hpp | 13 +- include/rfl/parsing/ViewReaderWithDefault.hpp | 11 +- ...ReaderWithDefaultAndStrippedFieldNames.hpp | 11 +- .../ViewReaderWithStrippedFieldNames.hpp | 11 +- tests/cereal/test_fail_fast.cpp | 29 ++++ tests/check_fail_fast.hpp | 89 +++++++++++ tests/generic/test_fail_fast.cpp | 22 +++ tests/json/test_fail_fast.cpp | 144 ++++++++++++++++++ tests/msgpack/test_fail_fast.cpp | 29 ++++ tests/yas/test_fail_fast.cpp | 29 ++++ 20 files changed, 581 insertions(+), 41 deletions(-) create mode 100644 benchmarks/all/parser_inlining_yas.cpp create mode 100644 include/rfl/FailFast.hpp create mode 100644 include/rfl/internal/fail_fast_v.hpp create mode 100644 include/rfl/parsing/FieldErrors.hpp create mode 100644 tests/cereal/test_fail_fast.cpp create mode 100644 tests/check_fail_fast.hpp create mode 100644 tests/generic/test_fail_fast.cpp create mode 100644 tests/json/test_fail_fast.cpp create mode 100644 tests/msgpack/test_fail_fast.cpp create mode 100644 tests/yas/test_fail_fast.cpp diff --git a/benchmarks/all/parser_inlining_cereal.cpp b/benchmarks/all/parser_inlining_cereal.cpp index 83ada2287..b99bbe8fc 100644 --- a/benchmarks/all/parser_inlining_cereal.cpp +++ b/benchmarks/all/parser_inlining_cereal.cpp @@ -1,13 +1,18 @@ +#include #include #include "parser_inlining.hpp" namespace parser_inlining { -template +template void BM_parser_inlining_cereal(benchmark::State& state) { run( state, [&](const auto& input) { return rfl::cereal::write(input); }, [&](const auto& bytes) { - return rfl::cereal::read>(bytes); + if constexpr (FailFast) { + return rfl::cereal::read, rfl::FailFast>(bytes); + } else { + return rfl::cereal::read>(bytes); + } }); } BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record2) @@ -16,4 +21,10 @@ BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record2) BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record16) ->Arg(100000) ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record2, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_cereal, Record16, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); } // namespace parser_inlining diff --git a/benchmarks/all/parser_inlining_json.cpp b/benchmarks/all/parser_inlining_json.cpp index c1dca3037..9a2942651 100644 --- a/benchmarks/all/parser_inlining_json.cpp +++ b/benchmarks/all/parser_inlining_json.cpp @@ -1,12 +1,19 @@ +#include #include #include "parser_inlining.hpp" namespace parser_inlining { -template +template void BM_parser_inlining_json(benchmark::State& state) { run( state, [&](const auto& input) { return rfl::json::write(input); }, - [&](const auto& bytes) { return rfl::json::read>(bytes); }); + [&](const auto& bytes) { + if constexpr (FailFast) { + return rfl::json::read, rfl::FailFast>(bytes); + } else { + return rfl::json::read>(bytes); + } + }); } BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record2) ->Arg(100000) @@ -14,4 +21,10 @@ BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record2) BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record16) ->Arg(100000) ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record2, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_json, Record16, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); } // namespace parser_inlining diff --git a/benchmarks/all/parser_inlining_msgpack.cpp b/benchmarks/all/parser_inlining_msgpack.cpp index c702a65a4..9ae1ba8ff 100644 --- a/benchmarks/all/parser_inlining_msgpack.cpp +++ b/benchmarks/all/parser_inlining_msgpack.cpp @@ -1,13 +1,18 @@ +#include #include #include "parser_inlining.hpp" namespace parser_inlining { -template +template void BM_parser_inlining_msgpack(benchmark::State& state) { run( state, [&](const auto& input) { return rfl::msgpack::write(input); }, [&](const auto& bytes) { - return rfl::msgpack::read>(bytes); + if constexpr (FailFast) { + return rfl::msgpack::read, rfl::FailFast>(bytes); + } else { + return rfl::msgpack::read>(bytes); + } }); } BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record2) @@ -16,4 +21,10 @@ BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record2) BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record16) ->Arg(100000) ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record2, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_msgpack, Record16, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); } // namespace parser_inlining diff --git a/benchmarks/all/parser_inlining_yas.cpp b/benchmarks/all/parser_inlining_yas.cpp new file mode 100644 index 000000000..65091ac58 --- /dev/null +++ b/benchmarks/all/parser_inlining_yas.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "parser_inlining.hpp" +namespace parser_inlining { +template +void BM_parser_inlining_yas(benchmark::State& state) { + run( + state, [&](const auto& input) { return rfl::yas::write(input); }, + [&](const auto& bytes) { + if constexpr (FailFast) { + return rfl::yas::read, rfl::FailFast>(bytes); + } else { + return rfl::yas::read>(bytes); + } + }); +} +BENCHMARK_TEMPLATE(BM_parser_inlining_yas, Record2) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_yas, Record16) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_yas, Record2, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_parser_inlining_yas, Record16, true) + ->Arg(100000) + ->Unit(benchmark::kMillisecond); +} // namespace parser_inlining diff --git a/docs/content/processors.md b/docs/content/processors.md index 25198d83c..64c75dd52 100644 --- a/docs/content/processors.md +++ b/docs/content/processors.md @@ -40,6 +40,7 @@ reflect-cpp currently supports the following processors: - `rfl::DefaultIfMissing` - `rfl::DurationsAsCount` - `rfl::EnumNamesOnly` +- `rfl::FailFast` - `rfl::NoExtraFields` - `rfl::NoFieldNames` - `rfl::NoOptionals` @@ -47,6 +48,24 @@ reflect-cpp currently supports the following processors: - `rfl::SnakeCaseToCamelCase` - `rfl::SnakeCaseToPascalCase` +### `rfl::FailFast` + +By default, reflect-cpp collects errors from multiple object fields. +`rfl::FailFast` reports the first field error and skips decoding the remaining +fields of that object. It also applies to nested structs and named tuples, +including structs stored in containers, across serialization formats. + +```cpp +const auto result = rfl::yas::read(bytes); +``` + +This reduces error bookkeeping and can improve successful read performance. +It does not change the serialized representation. Existing calls without the +processor retain aggregated errors. Readers may still traverse input after a +field failure, and document parsing or structural errors can take precedence. +Variant matching and other parsing outside object fields keep their existing +behavior. + ### `rfl::AddStructName` It is also possible to add the struct name as an additional field, like this: diff --git a/include/rfl.hpp b/include/rfl.hpp index e3ec7d806..2663929a3 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -24,6 +24,7 @@ #include "rfl/DurationsAsCount.hpp" #include "rfl/EnumNamesOnly.hpp" #include "rfl/ExtraFields.hpp" +#include "rfl/FailFast.hpp" #include "rfl/Field.hpp" #include "rfl/Flatten.hpp" #include "rfl/Generic.hpp" diff --git a/include/rfl/FailFast.hpp b/include/rfl/FailFast.hpp new file mode 100644 index 000000000..18f7e6706 --- /dev/null +++ b/include/rfl/FailFast.hpp @@ -0,0 +1,20 @@ +#ifndef RFL_FAILFAST_HPP_ +#define RFL_FAILFAST_HPP_ + +#include + +namespace rfl { + +/// Report only the first object field error and skip decoding later fields. +/// For example: rfl::json::read(input). +/// Readers may still traverse the input after a field error. +struct FailFast { + template + static auto process(auto&& _named_tuple) { + return std::forward(_named_tuple); + } +}; + +} // namespace rfl + +#endif diff --git a/include/rfl/internal/fail_fast_v.hpp b/include/rfl/internal/fail_fast_v.hpp new file mode 100644 index 000000000..6a6ba835e --- /dev/null +++ b/include/rfl/internal/fail_fast_v.hpp @@ -0,0 +1,26 @@ +#ifndef RFL_INTERNAL_FAILFAST_HPP_ +#define RFL_INTERNAL_FAILFAST_HPP_ + +#include + +#include "../FailFast.hpp" +#include "../Processors.hpp" + +namespace rfl::internal { + +template +struct fail_fast : std::false_type {}; + +template <> +struct fail_fast : std::true_type {}; + +template +struct fail_fast> + : std::bool_constant<(fail_fast::value || ...)> {}; + +template +inline constexpr bool fail_fast_v = fail_fast>::value; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/parsing/FieldErrors.hpp b/include/rfl/parsing/FieldErrors.hpp new file mode 100644 index 000000000..37bcde887 --- /dev/null +++ b/include/rfl/parsing/FieldErrors.hpp @@ -0,0 +1,48 @@ +#ifndef RFL_PARSING_FIELDERRORS_HPP_ +#define RFL_PARSING_FIELDERRORS_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "../Result.hpp" +#include "../internal/fail_fast_v.hpp" + +namespace rfl::parsing { + +class FirstFieldError { + public: + bool should_stop() const { return error_.has_value(); } + + void reserve(std::size_t) {} + + void emplace_back(std::string message) { + if (!error_) error_.emplace(std::move(message)); + } + + std::optional error() && { return std::move(error_); } + + private: + std::optional error_; +}; + +template +using FieldErrors = + std::conditional_t, FirstFieldError, + std::vector>; + +template +bool should_stop_on_field_error(const FieldErrors& errors) { + if constexpr (internal::fail_fast_v) { + return errors.should_stop(); + } else { + return false; + } +} + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index 14d3159ce..2587c4014 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -23,6 +23,7 @@ #include "../num_fields.hpp" #include "../to_view.hpp" #include "AreReaderAndWriter.hpp" +#include "FieldErrors.hpp" #include "Parent.hpp" #include "Parser_base.hpp" #include "ViewReader.hpp" @@ -325,7 +326,8 @@ struct NamedTupleParser { static RFL_ALWAYS_INLINE void handle_one_missing_field( const std::array& _found, const NamedTupleType& _view, std::array* _set, - std::vector* _errors) noexcept { + FieldErrors* _errors) noexcept { + if (should_stop_on_field_error(*_errors)) return; using FieldType = internal::nth_element_t<_i, FieldTypes...>; using ValueType = std::remove_reference_t< std::remove_pointer_t>; @@ -360,7 +362,7 @@ struct NamedTupleParser { template static RFL_ALWAYS_INLINE void handle_missing_fields( const std::array& _found, const NamedTupleType& _view, - std::array* _set, std::vector* _errors, + std::array* _set, FieldErrors* _errors, std::integer_sequence) noexcept { (handle_one_missing_field<_is>(_found, _view, _set, _errors), ...); } @@ -383,7 +385,7 @@ struct NamedTupleParser { found.fill(false); auto set = std::array(); set.fill(false); - std::vector errors; + FieldErrors errors; const auto reader = ViewReaderType(&_r, _view, &found, &set, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); @@ -396,18 +398,24 @@ struct NamedTupleParser { return std::make_pair(set, err); } } - handle_missing_fields(found, *_view, &set, &errors, - std::make_integer_sequence()); - if (errors.size() != 0) { - return std::make_pair(set, to_single_error_message(errors)); + if (!should_stop_on_field_error(errors)) { + handle_missing_fields(found, *_view, &set, &errors, + std::make_integer_sequence()); + } + if constexpr (internal::fail_fast_v) { + return std::make_pair(set, std::move(errors).error()); + } else { + if (errors.size() != 0) { + return std::make_pair(set, to_single_error_message(errors)); + } + return std::make_pair(set, std::optional()); } - return std::make_pair(set, std::optional()); } static std::optional read_object_or_array_with_default( const R& _r, const InputObjectOrArrayType& _obj_or_arr, NamedTupleType* _view) noexcept { - std::vector errors; + FieldErrors errors; const auto reader = ViewReaderWithDefaultType(&_r, _view, &errors); if constexpr (_no_field_names) { const auto err = _r.read_array(reader, _obj_or_arr); @@ -422,14 +430,19 @@ struct NamedTupleParser { } if constexpr (internal::has_default_val_v && !internal::default_if_missing_v) { - handle_missing_fields(reader.found(), *_view, nullptr, &errors, - std::make_integer_sequence()); + if (!should_stop_on_field_error(errors)) { + handle_missing_fields(reader.found(), *_view, nullptr, &errors, + std::make_integer_sequence()); + } } - if (errors.size() != 0) { - return to_single_error_message(errors); + if constexpr (internal::fail_fast_v) { + return std::move(errors).error(); + } else { + if (errors.size() != 0) { + return to_single_error_message(errors); + } + return std::nullopt; } - - return std::nullopt; } }; diff --git a/include/rfl/parsing/ViewReader.hpp b/include/rfl/parsing/ViewReader.hpp index 63a318835..1ae0c02c0 100644 --- a/include/rfl/parsing/ViewReader.hpp +++ b/include/rfl/parsing/ViewReader.hpp @@ -6,12 +6,12 @@ #include #include #include -#include #include "../Tuple.hpp" #include "../internal/always_inline.hpp" #include "../internal/is_array.hpp" #include "../internal/no_extra_fields_v.hpp" +#include "FieldErrors.hpp" #include "Parser_base.hpp" #include "schemaful/IsSchemafulReader.hpp" @@ -33,10 +33,10 @@ class ViewReader { * @param _found A boolean array indicating which fields have been found. * @param _set A boolean array indicating which fields have been successfully * set. - * @param _errors The vector to collect errors in. + * @param _errors The field error collector. */ ViewReader(const R* _r, ViewType* _view, std::array* _found, - std::array* _set, std::vector* _errors) + std::array* _set, FieldErrors* _errors) : r_(_r), view_(_view), found_(_found), set_(_set), errors_(_errors) {} ~ViewReader() = default; @@ -48,6 +48,7 @@ class ViewReader { * @param _var The input variable to read from. */ void read(const std::string_view& _name, const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return; assign_to_matching_field(*r_, _name, _var, view_, errors_, found_, set_, std::make_integer_sequence()); } @@ -59,6 +60,7 @@ class ViewReader { * @param _var The input variable to read from. */ void read(const int _index, const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return; assign_to_matching_field(*r_, _index, _var, view_, errors_, found_, set_, std::make_integer_sequence()); } @@ -66,6 +68,7 @@ class ViewReader { // Positional readers already know the field at compile time. template void read(std::integral_constant, const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return; assign_to_matching_field(*r_, i, _var, view_, errors_, found_, set_, std::integer_sequence()); } @@ -216,8 +219,8 @@ class ViewReader { /// because we have to trigger the destructors manually. std::array* set_; - /// Collects any errors we may have come across. - std::vector* errors_; + /// Stores field errors according to the selected processor policy. + FieldErrors* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithDefault.hpp b/include/rfl/parsing/ViewReaderWithDefault.hpp index 654f38eb3..2cad93817 100644 --- a/include/rfl/parsing/ViewReaderWithDefault.hpp +++ b/include/rfl/parsing/ViewReaderWithDefault.hpp @@ -6,12 +6,12 @@ #include #include #include -#include #include "../Ref.hpp" #include "../Tuple.hpp" #include "../internal/is_array.hpp" #include "../internal/no_extra_fields_v.hpp" +#include "FieldErrors.hpp" #include "Parser_base.hpp" namespace rfl::parsing { @@ -29,10 +29,10 @@ class ViewReaderWithDefault { * * @param _r The reader to use. * @param _view The view to read into. - * @param _errors The vector to collect errors in. + * @param _errors The field error collector. */ ViewReaderWithDefault(const R* _r, ViewType* _view, - std::vector* _errors) + FieldErrors* _errors) : r_(_r), view_(_view), errors_(_errors) { found_->fill(false); } @@ -53,6 +53,7 @@ class ViewReaderWithDefault { * @param _var The input variable to read from. */ void read(const std::string_view& _name, const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return; assign_to_matching_field(*r_, _name, _var, view_, errors_, found_.get(), std::make_integer_sequence()); } @@ -175,8 +176,8 @@ class ViewReaderWithDefault { /// Indicates that a certain field has been found. rfl::Ref> found_; - /// Collects any errors we may have come across. - std::vector* errors_; + /// Stores field errors according to the selected processor policy. + FieldErrors* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index e5d20e150..086ed839d 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -6,11 +6,11 @@ #include #include #include -#include #include "../Result.hpp" #include "../Tuple.hpp" #include "../internal/is_array.hpp" +#include "FieldErrors.hpp" #include "Parser_base.hpp" namespace rfl::parsing { @@ -28,10 +28,10 @@ class ViewReaderWithDefaultAndStrippedFieldNames { * * @param _r The reader to use. * @param _view The view to read into. - * @param _errors The vector to collect errors in. + * @param _errors The field error collector. */ ViewReaderWithDefaultAndStrippedFieldNames(const R* _r, ViewType* _view, - std::vector* _errors) + FieldErrors* _errors) : i_(0), r_(_r), view_(_view), errors_(_errors) {} ~ViewReaderWithDefaultAndStrippedFieldNames() = default; @@ -55,6 +55,7 @@ class ViewReaderWithDefaultAndStrippedFieldNames { * @return An optional error. */ std::optional read(const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return std::nullopt; if (i_ == size_) { std::stringstream stream; stream << "Expected a maximum of " << std::to_string(size_) @@ -144,8 +145,8 @@ class ViewReaderWithDefaultAndStrippedFieldNames { /// The underlying view. ViewType* view_; - /// Collects any errors we may have come across. - std::vector* errors_; + /// Stores field errors according to the selected processor policy. + FieldErrors* errors_; }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp index 160641341..498d4cc6d 100644 --- a/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithStrippedFieldNames.hpp @@ -6,11 +6,11 @@ #include #include #include -#include #include "../Result.hpp" #include "../Tuple.hpp" #include "../internal/is_array.hpp" +#include "FieldErrors.hpp" #include "Parser_base.hpp" namespace rfl::parsing { @@ -31,12 +31,12 @@ class ViewReaderWithStrippedFieldNames { * @param _found A boolean array indicating which fields have been found. * @param _set A boolean array indicating which fields have been successfully * set. - * @param _errors The vector to collect errors in. + * @param _errors The field error collector. */ ViewReaderWithStrippedFieldNames(const R* _r, ViewType* _view, std::array* _found, std::array* _set, - std::vector* _errors) + FieldErrors* _errors) : i_(0), r_(_r), view_(_view), @@ -53,6 +53,7 @@ class ViewReaderWithStrippedFieldNames { * @return An optional error. */ std::optional read(const InputVarType& _var) const { + if (should_stop_on_field_error(*errors_)) return std::nullopt; if (i_ == size_) { std::stringstream stream; stream << "Expected a maximum of " << std::to_string(size_) @@ -153,8 +154,8 @@ class ViewReaderWithStrippedFieldNames { /// because we have to trigger the destructors manually. std::array* set_; - /// Collects any errors we may have come across. - std::vector* errors_; + /// Stores field errors according to the selected processor policy. + FieldErrors* errors_; }; } // namespace rfl::parsing diff --git a/tests/cereal/test_fail_fast.cpp b/tests/cereal/test_fail_fast.cpp new file mode 100644 index 000000000..208c35a95 --- /dev/null +++ b/tests/cereal/test_fail_fast.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../check_fail_fast.hpp" + +TEST(cereal, test_fail_fast_round_trip) { + test_fail_fast::check_round_trip( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} + +TEST(cereal, test_fail_fast_truncated_input) { + test_fail_fast::check_truncated( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} + +TEST(cereal, test_fail_fast_error_policy) { + test_fail_fast::check_error_policy( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} diff --git a/tests/check_fail_fast.hpp b/tests/check_fail_fast.hpp new file mode 100644 index 000000000..57c996155 --- /dev/null +++ b/tests/check_fail_fast.hpp @@ -0,0 +1,89 @@ +#ifndef RFL_TESTS_CHECK_FAIL_FAST_HPP_ +#define RFL_TESTS_CHECK_FAIL_FAST_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_fail_fast { + +struct Detail { + std::string label; + std::vector values; + std::optional weight; + bool operator==(const Detail&) const = default; +}; + +struct Record { + const std::uint64_t id; + std::vector details; + bool operator==(const Record&) const = default; +}; + +struct InvalidRecord { + int first; + int second; +}; + +struct ValidatedRecord { + rfl::Validator> first; + rfl::Validator> second; +}; + +template +void check_error_policy(Write write, ReadDefault read_default, + ReadFailFast read_fail_fast) { + const auto bytes = write(InvalidRecord{-1, -2}); + const auto all_errors = + read_default.template operator()(bytes); + ASSERT_FALSE(all_errors); + const auto all_message = std::string(all_errors.error().what()); + EXPECT_NE(all_message.find("Found 2 errors:"), std::string::npos); + EXPECT_NE(all_message.find("field 'first'"), std::string::npos); + EXPECT_NE(all_message.find("field 'second'"), std::string::npos); + + const auto first_error = + read_fail_fast.template operator()(bytes); + ASSERT_FALSE(first_error); + const auto first_message = std::string(first_error.error().what()); + EXPECT_NE(first_message.find("field 'first'"), std::string::npos); + EXPECT_EQ(first_message.find("field 'second'"), std::string::npos); + EXPECT_EQ(first_message.find("Found 2 errors:"), std::string::npos); +} + +// Read ordinary output with FailFast, including nontrivial and const fields. +template +void check_round_trip(Write write, Read read) { + const std::vector input{ + {17, {{"first", {1, 2, 3}, 0.5}, {"second", {}, std::nullopt}}}, + {42, {}}}; + const auto bytes = write(input); + const auto result = + read.template operator()>(bytes); + ASSERT_TRUE(result) << result.error().what(); + EXPECT_EQ(*result, input); + EXPECT_EQ(write(*result), bytes); +} + +template +void check_truncated(Write write, Read read) { + const std::vector input{{17, {{"item", {1, 2}, 0.5}}}}; + const auto bytes = write(input); + for (std::size_t size = 0; size < bytes.size(); ++size) { + auto truncated = bytes; + truncated.resize(size); + const auto result = + read.template operator()>(truncated); + EXPECT_FALSE(result) << "Accepted prefix of length " << size; + } +} + +} // namespace test_fail_fast + +#endif diff --git a/tests/generic/test_fail_fast.cpp b/tests/generic/test_fail_fast.cpp new file mode 100644 index 000000000..3f1a9a03a --- /dev/null +++ b/tests/generic/test_fail_fast.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +#include "../check_fail_fast.hpp" + +TEST(generic, test_fail_fast_round_trip) { + test_fail_fast::check_round_trip( + [](const auto& input) { return rfl::to_generic(input); }, + [](const auto& bytes) { + return rfl::from_generic(bytes); + }); +} + +TEST(generic, test_fail_fast_error_policy) { + test_fail_fast::check_error_policy( + [](const auto& input) { return rfl::to_generic(input); }, + [](const auto& bytes) { return rfl::from_generic(bytes); }, + [](const auto& bytes) { + return rfl::from_generic(bytes); + }); +} diff --git a/tests/json/test_fail_fast.cpp b/tests/json/test_fail_fast.cpp new file mode 100644 index 000000000..ccfd386fa --- /dev/null +++ b/tests/json/test_fail_fast.cpp @@ -0,0 +1,144 @@ +#include +#include + +#include "../check_fail_fast.hpp" + +TEST(json, test_fail_fast_round_trip) { + test_fail_fast::check_round_trip( + [](const auto& input) { return rfl::json::write(input); }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} + +namespace { +struct TwoFields { + int first; + int second; +}; + +struct Counted { + using ReflectionType = int; + static inline int live = 0; + static inline int constructed = 0; + int value; + explicit Counted(int v) : value(v) { + ++live; + ++constructed; + } + Counted(Counted&& other) noexcept : value(other.value) { ++live; } + ~Counted() { --live; } + int reflection() const { return value; } +}; + +struct OwningRecord { + Counted first; + int invalid; + Counted skipped; +}; +} // namespace + +TEST(json, test_fail_fast_first_error) { + const auto result = rfl::json::read( + R"({"first":"bad","second":"also bad"})"); + ASSERT_FALSE(result); + EXPECT_EQ(result.error().what(), + "Failed to parse field 'first': Could not cast to int."); + + const auto missing = rfl::json::read("{}"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().what(), "Field named 'first' not found."); +} + +TEST(json, test_fail_fast_partial_object_cleanup) { + Counted::constructed = 0; + const auto result = rfl::json::read( + R"({"first":1,"invalid":"bad","skipped":2})"); + ASSERT_FALSE(result); + EXPECT_EQ(Counted::constructed, 1); + EXPECT_EQ(Counted::live, 0); +} + +TEST(json, test_fail_fast_processors) { + using Ps = rfl::Processors; + const auto result = rfl::json::read(R"(["bad","bad"])"); + ASSERT_FALSE(result); + EXPECT_EQ(result.error().what(), + "Failed to parse field 'first': Could not cast to int."); + + const auto defaults = + rfl::json::read( + R"({"first":7})"); + ASSERT_TRUE(defaults); + EXPECT_EQ(defaults->first, 7); + EXPECT_EQ(defaults->second, 0); + + const auto extra = + rfl::json::read( + R"({"extra":0,"first":"bad","second":"bad"})"); + ASSERT_FALSE(extra); + EXPECT_EQ(extra.error().what(), "Value named 'extra' not used."); +} + +TEST(json, test_fail_fast_defaults_and_stripped_names) { + const auto bad = + rfl::json::read(R"(["bad","also bad"])"); + ASSERT_FALSE(bad); + EXPECT_EQ(bad.error().what(), + "Failed to parse field 'first': Could not cast to int."); + const auto good = + rfl::json::read("[7]"); + ASSERT_TRUE(good); + EXPECT_EQ(good->first, 7); + EXPECT_EQ(good->second, 0); +} + +TEST(json, test_fail_fast_nested_error) { + struct Nested { + TwoFields object; + int other; + }; + const auto result = rfl::json::read( + R"({"object":{"first":"bad","second":"bad"},"other":"bad"})"); + ASSERT_FALSE(result); + EXPECT_EQ(result.error().what(), + "Failed to parse field 'object': Failed to parse field 'first': " + "Could not cast to int."); +} + +TEST(json, test_fail_fast_error_policy) { + test_fail_fast::check_error_policy( + [](const auto& input) { return rfl::json::write(input); }, + [](const auto& bytes) { return rfl::json::read(bytes); }, + [](const auto& bytes) { + return rfl::json::read(bytes); + }); +} + +TEST(json, test_fail_fast_default_value_missing_fields) { + struct WithDefault { + int first; + int second; + rfl::DefaultVal label = "default"; + }; + const auto result = rfl::json::read("{}"); + ASSERT_FALSE(result); + EXPECT_EQ(result.error().what(), "Field named 'first' not found."); + + const auto valid = + rfl::json::read(R"({"first":1,"second":2})"); + ASSERT_TRUE(valid); + EXPECT_EQ(valid->label.value(), "default"); +} + +TEST(json, test_fail_fast_named_tuple) { + using Named = + rfl::NamedTuple, rfl::Field<"second", int>>; + const auto result = rfl::json::read( + R"({"first":"bad","second":"also bad"})"); + ASSERT_FALSE(result); + EXPECT_EQ(result.error().what(), + "Failed to parse field 'first': Could not cast to int."); +} diff --git a/tests/msgpack/test_fail_fast.cpp b/tests/msgpack/test_fail_fast.cpp new file mode 100644 index 000000000..191500abd --- /dev/null +++ b/tests/msgpack/test_fail_fast.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../check_fail_fast.hpp" + +TEST(msgpack, test_fail_fast_round_trip) { + test_fail_fast::check_round_trip( + [](const auto& input) { return rfl::msgpack::write(input); }, + [](const auto& bytes) { + return rfl::msgpack::read(bytes); + }); +} + +TEST(msgpack, test_fail_fast_truncated_input) { + test_fail_fast::check_truncated( + [](const auto& input) { return rfl::msgpack::write(input); }, + [](const auto& bytes) { + return rfl::msgpack::read(bytes); + }); +} + +TEST(msgpack, test_fail_fast_error_policy) { + test_fail_fast::check_error_policy( + [](const auto& input) { return rfl::msgpack::write(input); }, + [](const auto& bytes) { return rfl::msgpack::read(bytes); }, + [](const auto& bytes) { + return rfl::msgpack::read(bytes); + }); +} diff --git a/tests/yas/test_fail_fast.cpp b/tests/yas/test_fail_fast.cpp new file mode 100644 index 000000000..125e17a83 --- /dev/null +++ b/tests/yas/test_fail_fast.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include "../check_fail_fast.hpp" + +TEST(yas, test_fail_fast_round_trip) { + test_fail_fast::check_round_trip( + [](const auto& input) { return rfl::yas::write(input); }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +} + +TEST(yas, test_fail_fast_truncated_input) { + test_fail_fast::check_truncated( + [](const auto& input) { return rfl::yas::write(input); }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +} + +TEST(yas, test_fail_fast_error_policy) { + test_fail_fast::check_error_policy( + [](const auto& input) { return rfl::yas::write(input); }, + [](const auto& bytes) { return rfl::yas::read(bytes); }, + [](const auto& bytes) { + return rfl::yas::read(bytes); + }); +}