diff --git a/docs/content/default_val.md b/docs/content/default_val.md index ced77a07..6b39decb 100644 --- a/docs/content/default_val.md +++ b/docs/content/default_val.md @@ -52,6 +52,52 @@ const auto homer = rfl::json::read(R"(["Homer"])").va // homer.town == "" ``` +## Fields without a valid default value + +A struct that contains an `rfl::DefaultVal` is default-constructed before it is read, so that the fields missing from the input keep their default values. The fields found in the input are then replaced by the values read from it. + +Some fields have no valid default value, for instance a validator that an empty vector does not satisfy: + +```cpp +using Foods = rfl::Validator, rfl::Size>>; + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; +}; + +const auto homer = rfl::json::read(R"({"favorite_foods":["donuts","pizza"]})"); +``` + +`Person{}` throws, but `read` then constructs the struct again with the check of such a validator deferred until the input has been read. If `favorite_foods` is in the input, its invalid default value has been replaced. If it is missing, you get the error `Field named 'favorite_foods' not found.` Outside of `read`, `Person{}` still throws. + +A field whose default value is invalid must not be missing, even where a missing field would otherwise get its default value, that is, for an `rfl::DefaultVal` or with the `rfl::DefaultIfMissing` processor: + +``` +Field named 'favorite_foods' not found, and its default value is invalid: Size validation failed: Value expected to be greater than or equal to 1, but got 0. +``` + +Only a validator that is default-constructed in place inside the struct is deferred, also inside nested structs, `std::optional`, `std::variant`, `std::array` or `rfl::Variant`. While the struct is constructed, it holds its invalid value, which a default member initializer that reads it would see. A default member initializer that catches the exception of such a validator in order to fall back to another value might not get the exception either, depending on whether the compiler constructs the validator directly inside the struct. The struct cannot be read, and you get the error below, if the invalid default value is constructed elsewhere and then moved into the struct (as in `rfl::DefaultVal favorite_foods = Foods();` or inside `rfl::Tuple`), if it is on the heap (as in `std::vector(1)`), or if `Person{}` throws for another reason: + +``` +Could not default-construct Person, which is required because it contains rfl::DefaultVal. Give every field a valid default value. Original error: Size validation failed: Value expected to be greater than or equal to 1, but got 0. +``` + +One case is not detected: if a default member initializer copies a validator whose default value is invalid, as in `rfl::DefaultVal other_foods = favorite_foods;`, the copy is not checked, and it keeps the invalid value if `other_foods` is missing from the input. + +In all of these cases, you can give the field a valid default value: + +```cpp +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods = std::vector({"none"}); +}; +``` + +The field is still required. The default value is only used for a missing field if you pass the `rfl::DefaultIfMissing` processor. + +All of this applies to every struct that is read with the `rfl::DefaultIfMissing` processor, whether it contains an `rfl::DefaultVal` or not. The JSON schema of a struct that cannot be default-constructed contains no default values. + ## When to use Use `rfl::DefaultVal` when you want a field to be optional at the input side but still available as a value on the resulting object (no std::optional or pointer indirection). It is particularly useful for fields with sensible defaults (for example, a common last name, a default configuration value, or empty containers/strings). diff --git a/docs/content/processors.md b/docs/content/processors.md index 64c75dd5..014c44da 100644 --- a/docs/content/processors.md +++ b/docs/content/processors.md @@ -310,6 +310,13 @@ have gotten had you read the following JSON string: Because you have not passed a default value to town, the default value of the type is used instead. +Note that the struct is default-constructed before it is read. A field +whose default value is invalid, such as a validator that the default value +of its type does not satisfy, can be read, but it must not be missing: you +get the error `Field named '...' not found, and its default value is +invalid: ...`. The documentation on `rfl::DefaultVal` explains this and the +cases in which such a struct cannot be read at all. + ### `rfl::DurationsAsCount` By default, `std::chrono::duration` types are written as an object containing diff --git a/include/rfl/DefaultVal.hpp b/include/rfl/DefaultVal.hpp index 630a3f5b..d3c2edc4 100644 --- a/include/rfl/DefaultVal.hpp +++ b/include/rfl/DefaultVal.hpp @@ -63,7 +63,8 @@ struct DefaultVal { /// @param _value The value to convert and store (will be moved) template requires(std::is_convertible_v) - DefaultVal(U&& _value) noexcept : value_(std::forward(_value)) {} + DefaultVal(U&& _value) noexcept(std::is_nothrow_constructible_v) + : value_(std::forward(_value)) {} /// Constructs from a DefaultVal with compatible type. /// @tparam U Type convertible to T diff --git a/include/rfl/Validator.hpp b/include/rfl/Validator.hpp index 9be58bf6..0cacc59d 100644 --- a/include/rfl/Validator.hpp +++ b/include/rfl/Validator.hpp @@ -9,7 +9,9 @@ #include "AllOf.hpp" #include "Result.hpp" +#include "internal/DeferredDefaults.hpp" #include "internal/HasValidation.hpp" +#include "internal/cold.hpp" namespace rfl { @@ -52,8 +54,10 @@ struct Validator { } /// Default constructor - validates a default-constructed T. - /// @throws std::exception if validation of the default value fails - Validator() : value_(ValidationType::validate(T()).value()) {} + /// @throws std::exception if validation of the default value fails, unless + /// the parser is default-constructing a struct to read into, see + /// internal::DeferredDefaults. + Validator() : value_(valid_or_deferred(this)) {} /// Move constructor. /// @param _other The validator to move from @@ -197,6 +201,25 @@ struct Validator { Validator(AlreadyValidated, T&& _value) : value_(std::move(_value)) {} + /// Returns the validated default value. If it is invalid, this throws, + /// unless the parser defers its validation, see internal::DeferredDefaults. + /// _self is the Validator under construction. It is not a pointer to const, + /// because GCC would then warn that it is used uninitialized. + static T valid_or_deferred(void* _self) { + auto res = ValidationType::validate(T()); + if (!res) [[unlikely]] { + return deferred_or_throw(_self, res.error()); + } + return *std::move(res); + } + + RFL_COLD static T deferred_or_throw(void* _self, const Error& _err) { + if (!internal::DeferredDefaults::defer(_self, _err)) { + Result(error(_err)).value(); + } + return T(); + } + /// Wraps the result of a validation without throwing if it failed. static Result wrap_validated(Result&& _res) { return std::move(_res).transform( diff --git a/include/rfl/Variant.hpp b/include/rfl/Variant.hpp index 018edca7..e9ab9d77 100644 --- a/include/rfl/Variant.hpp +++ b/include/rfl/Variant.hpp @@ -45,11 +45,10 @@ class Variant { public: /// @brief Default-constructs the Variant, initializing it with the first /// alternative type. - /// @details The first alternative type is default-constructed and stored in - /// the variant. + /// @details The first alternative type is default-constructed in place. Variant() : index_(IndexType()), data_(DataType()) { using FirstAlternative = internal::nth_element_t<0, AlternativeTypes...>; - move_from_type(FirstAlternative()); + new (data_.data()) FirstAlternative(); } /// @brief Copy-constructs the Variant from another Variant. diff --git a/include/rfl/internal/DeferredDefaults.hpp b/include/rfl/internal/DeferredDefaults.hpp new file mode 100644 index 00000000..5ac3f3df --- /dev/null +++ b/include/rfl/internal/DeferredDefaults.hpp @@ -0,0 +1,82 @@ +#ifndef RFL_INTERNAL_DEFERREDDEFAULTS_HPP_ +#define RFL_INTERNAL_DEFERREDDEFAULTS_HPP_ + +#include +#include +#include + +#include "../Result.hpp" + +namespace rfl::internal { + +/// Parser::read_struct_with_default default-constructs the struct it reads +/// into, to obtain the default values of the fields that are missing from the +/// input. If an rfl::Validator in it has an invalid default value, that +/// throws, even if the input contains the field. The parser then constructs +/// the struct again while a DeferredDefaults is active on this thread. A +/// Validator inside the struct then keeps its invalid default value instead +/// of throwing and is recorded here, so that the parser can check after +/// reading that the value has been replaced by one from the input. Validators +/// anywhere else, such as temporaries or on the heap, throw as usual. +class DeferredDefaults { + public: + struct Record { + /// The address of the Validator. + const void* address; + + /// Why its default value is invalid. + Error error; + }; + + /// Makes _deferred the active DeferredDefaults of this thread for as long + /// as the Scope lives. + class Scope { + public: + explicit Scope(DeferredDefaults* _deferred) noexcept : prev_(active()) { + active() = _deferred; + } + + ~Scope() { active() = prev_; } + + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + + private: + DeferredDefaults* prev_; + }; + + /// _begin and _size describe the storage of the struct. + DeferredDefaults(const void* _begin, size_t _size) noexcept + : begin_(static_cast(_begin)), + end_(begin_ + _size) {} + + /// Called by rfl::Validator() if its default value is invalid. Returns + /// whether the Validator keeps the value instead of throwing. + static bool defer(const void* _address, const Error& _error) { + auto* deferred = active(); + const auto* p = static_cast(_address); + const auto less = std::less(); + if (!deferred || less(p, deferred->begin_) || !less(p, deferred->end_)) { + return false; + } + deferred->records_.push_back(Record{_address, _error}); + return true; + } + + const std::vector& records() const noexcept { return records_; } + + private: + static DeferredDefaults*& active() noexcept { + static thread_local DeferredDefaults* deferred = nullptr; + return deferred; + } + + private: + const unsigned char* begin_; + const unsigned char* end_; + std::vector records_; +}; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/parsing/NamedTupleParser.hpp b/include/rfl/parsing/NamedTupleParser.hpp index c4e2fd31..0d1d4a31 100644 --- a/include/rfl/parsing/NamedTupleParser.hpp +++ b/include/rfl/parsing/NamedTupleParser.hpp @@ -2,6 +2,7 @@ #define RFL_PARSING_NAMEDTUPLEPARSER_HPP_ #include +#include #include #include #include @@ -176,23 +177,24 @@ struct NamedTupleParser { * @param _r The reader to use. * @param _var The input variable to read from. * @param _view The view to read into. + * @param _found If not null, receives which fields the input contains. * @return An optional error. */ static std::optional read_view_with_default( - const R& _r, const InputVarType& _var, - NamedTuple* _view) noexcept { + const R& _r, const InputVarType& _var, NamedTuple* _view, + std::array* _found = nullptr) noexcept { if constexpr (_no_field_names) { auto arr = _r.to_array(_var); if (!arr) [[unlikely]] { return arr.error(); } - return read_object_or_array_with_default(_r, *arr, _view); + return read_object_or_array_with_default(_r, *arr, _view, _found); } else { auto obj = _r.to_object(_var); if (!obj) [[unlikely]] { return obj.error(); } - return read_object_or_array_with_default(_r, *obj, _view); + return read_object_or_array_with_default(_r, *obj, _view, _found); } } @@ -353,11 +355,26 @@ struct NamedTupleParser { make_nested_error(std::string(current_name), stream.str())); } else if constexpr (!internal::has_default_val_v) { - if constexpr (!std::is_const_v) { - ::new (rfl::get<_i>(_view)) ValueType(); + using NonConstT = std::remove_const_t; + const auto ptr = const_cast(rfl::get<_i>(_view)); + if constexpr (std::is_nothrow_default_constructible_v) { + ::new (ptr) NonConstT(); } else { - using NonConstT = std::remove_const_t; - ::new (const_cast(rfl::get<_i>(_view))) NonConstT(); + // The default value can throw, for instance a Validator whose + // default value is invalid. + try { + ::new (ptr) NonConstT(); + } catch (std::exception& e) { + constexpr auto current_name = + internal::nth_element_t<_i, FieldTypes...>::name(); + _errors->emplace_back(make_nested_error( + std::string(current_name), + "Field named '" + std::string(current_name) + + "' not found, and its default value could not be " + "constructed: " + + e.what())); + return; + } } std::get<_i>(*_set) = true; } @@ -420,7 +437,7 @@ struct NamedTupleParser { static std::optional read_object_or_array_with_default( const R& _r, const InputObjectOrArrayType& _obj_or_arr, - NamedTupleType* _view) noexcept { + NamedTupleType* _view, std::array* _found) noexcept { FieldErrors errors; const auto reader = ViewReaderWithDefaultType(&_r, _view, &errors); if constexpr (_no_field_names) { @@ -434,6 +451,9 @@ struct NamedTupleParser { return err; } } + if (_found) [[unlikely]] { + *_found = reader.found(); + } if constexpr (internal::has_default_val_v && !internal::default_if_missing_v) { if (!should_stop_on_field_error(errors)) { diff --git a/include/rfl/parsing/ParserSkip.hpp b/include/rfl/parsing/ParserSkip.hpp index 6d748ba0..5b31767f 100644 --- a/include/rfl/parsing/ParserSkip.hpp +++ b/include/rfl/parsing/ParserSkip.hpp @@ -1,6 +1,7 @@ #ifndef RFL_PARSING_PARSER_SKIP_HPP_ #define RFL_PARSING_PARSER_SKIP_HPP_ +#include #include #include @@ -39,8 +40,14 @@ struct ParserSkip { static Result> read(const R& _r, const InputVarType& _var) noexcept { if constexpr (_skip_deserialization) { - return internal::Skip( - std::remove_cvref_t()); + // The default value can throw, for instance a Validator whose default + // value is invalid. + try { + return internal::Skip( + std::remove_cvref_t()); + } catch (std::exception& e) { + return error(e.what()); + } } else { const auto to_skip = [&](auto&& _t) { return internal::Skip( diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index f17c13e1..20a25610 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -1,13 +1,23 @@ #ifndef RFL_PARSING_PARSER_DEFAULT_HPP_ #define RFL_PARSING_PARSER_DEFAULT_HPP_ +#include +#include +#include #include +#include +#include +#include +#include +#include #include #include "../Result.hpp" #include "../always_false.hpp" +#include "../internal/DeferredDefaults.hpp" #include "../internal/HasFromReflectionMethod.hpp" #include "../internal/always_inline.hpp" +#include "../internal/cold.hpp" #include "../internal/default_if_missing_v.hpp" #include "../internal/has_default_val_v.hpp" #include "../internal/has_reflection_method_v.hpp" @@ -23,6 +33,7 @@ #include "../internal/to_ptr_named_tuple.hpp" #include "../to_view.hpp" #include "AreReaderAndWriter.hpp" +#include "FieldErrors.hpp" #include "MapParser.hpp" #include "Parent.hpp" #include "ParserArray.hpp" @@ -69,6 +80,8 @@ #include "schema/Type.hpp" #include "schemaful/IsSchemafulReader.hpp" #include "schemaful/IsSchemafulWriter.hpp" +#include "to_single_error_message.hpp" +#include "wrap_error.hpp" namespace rfl::parsing { @@ -663,11 +676,19 @@ struct Parser { } else { using NamedTupleType = internal::processed_t; if constexpr (internal::has_default_val_v) { - auto t = U{}; - auto view = ProcessorsType::template process(to_view(t)); - (*_definitions)[name] = - Parser::to_schema( - _definitions, &view); + try { + auto t = U{}; + auto view = ProcessorsType::template process(to_view(t)); + (*_definitions)[name] = + Parser::to_schema( + _definitions, &view); + } catch (std::exception&) { + // A field has no valid default value, such as a Validator, so we + // leave out the default values. + (*_definitions)[name] = + Parser::to_schema( + _definitions); + } } else { (*_definitions)[name] = Parser::to_schema( @@ -719,23 +740,144 @@ struct Parser { /// so we only use it when the DefaultIfMissing preprocessor is added. static Result read_struct_with_default(const R& _r, const InputVarType& _var) { + // T is constructed in place, so that the Validators that deferred records + // stay where they are: a trivially copyable T returned from a function + // may be copied. + alignas(T) unsigned char buf[sizeof(T)]; + internal::DeferredDefaults deferred(&buf, sizeof(T)); + std::unique_ptr t; + try { + t.reset(default_construct(&buf, &deferred)); + } catch (std::exception& e) { + return error(default_construction_error(e.what())); + } try { - auto t = T{}; // This might fail, for instance if the default value does - // not satisfy the validator, but in that case we will just - // return the error. - auto view = ProcessorsType::template process(to_view(t)); + auto view = ProcessorsType::template process(to_view(*t)); using ViewType = decltype(view); + auto found = std::array{}; + const bool any_deferred = !deferred.records().empty(); auto err = Parser::read_view_with_default( - _r, _var, &view); + _r, _var, &view, any_deferred ? &found : nullptr); + if (!err && any_deferred) [[unlikely]] { + err = check_deferred_defaults( + deferred, view, found, + std::make_index_sequence()); + } if (err) [[unlikely]] { return error(std::move(*err)); } - return t; + return std::move(*t); } catch (std::exception& e) { return error(e.what()); } } + + struct Destroy { + void operator()(T* _ptr) const { _ptr->~T(); } + }; + + /// Constructs T{} in _buf. If that throws, for instance because the default + /// value of a Validator is invalid, we try again with such Validators + /// deferred to _deferred (see internal::DeferredDefaults). + static T* default_construct(void* _buf, + internal::DeferredDefaults* _deferred) { + // Whether that has been necessary before, so that the first try would + // throw again. This is only a shortcut, so relaxed is enough. + static std::atomic defer_right_away = false; + if (!defer_right_away.load(std::memory_order_relaxed)) { + try { + return ::new (_buf) T{}; + } catch (std::exception&) { + } + } + const auto scope = internal::DeferredDefaults::Scope(_deferred); + auto* ptr = ::new (_buf) T{}; + if (!_deferred->records().empty()) { + defer_right_away.store(true, std::memory_order_relaxed); + } + return ptr; + } + + /// Returns an error if an invalid default value that has been deferred is + /// still part of the result. This relies on every field that the input + /// contains having been assigned a value that was read as a whole (see + /// ViewReaderWithDefault and ViewReaderWithDefaultAndStrippedFieldNames), + /// which replaces any invalid default value inside it. A field that is + /// missing from the input keeps its default value. + template + static std::optional check_deferred_defaults( + const internal::DeferredDefaults& _deferred, const ViewType& _view, + const std::array& _found, + std::index_sequence<_is...>) { + constexpr size_t none = sizeof...(_is); + constexpr auto names = std::array{ + tuple_element_t<_is, typename ViewType::Fields>::name()...}; + FieldErrors errors; + auto reported = std::array{}; + for (const auto& record : _deferred.records()) { + const auto* address = static_cast(record.address); + // [[no_unique_address]] lets a field be placed in the tail padding of + // another one, so the address can be inside both. It then belongs to + // the one that starts later. + size_t field = none; + const unsigned char* field_begin = nullptr; + const auto find_field = [&]( + std::integral_constant) { + using Type = + typename tuple_element_t<_i, typename ViewType::Fields>::Type; + if constexpr (std::is_pointer_v && + !std::is_empty_v>) { + const auto* begin = + internal::ptr_cast(rfl::get<_i>(_view)); + const auto* end = begin + sizeof(std::remove_pointer_t); + const auto less = std::less(); + if (!less(address, begin) && less(address, end) && + (field == none || less(field_begin, begin))) { + field = _i; + field_begin = begin; + } + } + }; + (find_field(std::integral_constant{}), ...); + if (field == none) [[unlikely]] { + // Not inside any field, so we cannot tell what happened to it. + return default_construction_error(record.error.what().c_str()); + } + if (!_found[field] && !reported[field]) { + reported[field] = true; + const auto name = std::string(names[field]); + errors.emplace_back(make_nested_error( + name, "Field named '" + name + + "' not found, and its default value is invalid: " + + record.error.what())); + if (should_stop_on_field_error(errors)) { + break; + } + } + } + if constexpr (internal::fail_fast_v) { + return std::move(errors).error(); + } else { + if (errors.empty()) { + return std::nullopt; + } + return to_single_error(std::move(errors)); + } + } + + /// The error returned by read_struct_with_default when T{} throws. On its + /// own, the exception reads as if the input was wrong, so we name the struct + /// and explain why it was default-constructed. + RFL_COLD static Error default_construction_error(const char* _what) { + const std::string reason = internal::has_default_val_v + ? "it contains rfl::DefaultVal" + : "it is read with rfl::DefaultIfMissing"; + return Error( + "Could not default-construct " + type_name_t().str() + + ", which is required because " + reason + + ". Give every field a valid default value. Original error: " + _what); + } }; } // namespace rfl::parsing diff --git a/include/rfl/parsing/TupleReader.hpp b/include/rfl/parsing/TupleReader.hpp index bc48a870..dab0c7fd 100644 --- a/include/rfl/parsing/TupleReader.hpp +++ b/include/rfl/parsing/TupleReader.hpp @@ -1,6 +1,7 @@ #ifndef RFL_PARSING_TUPLEREADER_HPP_ #define RFL_PARSING_TUPLEREADER_HPP_ +#include #include #include #include @@ -80,9 +81,25 @@ class TupleReader { static_cast(_i), "Field " + std::to_string(_i) + " was required, but missing."); return; - } else { + } else if constexpr (std::is_nothrow_default_constructible_v< + CurrentType>) { ::new (&(rfl::get<_i>(*tuple_))) CurrentType(); ++num_set_; + } else { + // The default value can throw, for instance a Validator whose + // default value is invalid. + try { + ::new (&(rfl::get<_i>(*tuple_))) CurrentType(); + ++num_set_; + } catch (std::exception& e) { + *_err = make_nested_error( + static_cast(_i), + "Field " + std::to_string(_i) + + " was missing, and its default value could not be " + "constructed: " + + e.what()); + return; + } } } handle_missing_fields_impl<_i + 1>(_err); diff --git a/include/rfl/parsing/ViewReaderWithDefault.hpp b/include/rfl/parsing/ViewReaderWithDefault.hpp index 5faf08f2..5616b481 100644 --- a/include/rfl/parsing/ViewReaderWithDefault.hpp +++ b/include/rfl/parsing/ViewReaderWithDefault.hpp @@ -87,6 +87,8 @@ class ViewReaderWithDefault { _errors->emplace_back(wrap_field_error(name, std::move(res).error())); return; } + // Parser::check_deferred_defaults relies on the field being replaced as + // a whole. if constexpr (std::is_pointer_v) { move_to(rfl::get(*_view), &(*res)); } else { diff --git a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp index 26438026..766be47f 100644 --- a/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp +++ b/include/rfl/parsing/ViewReaderWithDefaultAndStrippedFieldNames.hpp @@ -90,6 +90,8 @@ class ViewReaderWithDefaultAndStrippedFieldNames { _errors->emplace_back(wrap_field_error(name, std::move(res).error())); return; } + // Parser::check_deferred_defaults relies on the field being replaced as + // a whole. if constexpr (std::is_pointer_v) { move_to(rfl::get(*_view), &(*res)); } else { diff --git a/tests/cli/test_default_val_with_validator.cpp b/tests/cli/test_default_val_with_validator.cpp new file mode 100644 index 00000000..d3fe17e5 --- /dev/null +++ b/tests/cli/test_default_val_with_validator.cpp @@ -0,0 +1,55 @@ +#include + +#include +#include +#include +#include + +namespace test_default_val_with_validator { + +using Path = std::vector; + +std::vector paths(const rfl::Error& _err) { + std::vector result; + for (const auto& detail : _err.details()) { + result.push_back(detail.path); + } + return result; +} + +using Positive = rfl::Validator>; + +struct Config { + rfl::DefaultVal port = 8080; + rfl::Validator>> host_name; + rfl::DefaultVal max_retries; +}; + +TEST(cli, test_default_val_with_validator) { + const char* args[] = {"program", "--host-name=srv", "--max-retries=3"}; + const auto res = rfl::cli::read(3, const_cast(args)); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->port.value(), 8080); + EXPECT_EQ(res->host_name.value(), "srv"); + EXPECT_EQ(res->max_retries.value().value(), 3); +} + +TEST(cli, test_default_val_with_validator_invalid_default) { + const char* args[] = {"program", "--host-name=srv"}; + const auto res = rfl::cli::read(2, const_cast(args)); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 'max-retries' not found, and its default value is " + "invalid: Value expected to be greater than or equal to 1, but " + "got 0."); + EXPECT_EQ(paths(res.error()), std::vector({{"max-retries"}})); +} + +TEST(cli, test_default_val_with_validator_required) { + const char* args[] = {"program", "--max-retries=3"}; + const auto res = rfl::cli::read(2, const_cast(args)); + ASSERT_FALSE(res); + EXPECT_EQ(paths(res.error()), std::vector({{"host-name"}})); +} + +} // namespace test_default_val_with_validator diff --git a/tests/generic/test_default_val_with_validator.cpp b/tests/generic/test_default_val_with_validator.cpp new file mode 100644 index 00000000..d150c07f --- /dev/null +++ b/tests/generic/test_default_val_with_validator.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include +#include + +namespace test_default_val_with_validator { + +using Path = std::vector; + +std::vector paths(const rfl::Error& _err) { + std::vector result; + for (const auto& detail : _err.details()) { + result.push_back(detail.path); + } + return result; +} + +using Foods = + rfl::Validator, rfl::Size>>; +using Positive = rfl::Validator>; + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; + rfl::DefaultVal age; +}; + +rfl::Generic foods() { + return rfl::Generic( + rfl::Generic::Array({rfl::Generic(std::string("donuts"))})); +} + +TEST(generic, test_default_val_with_validator) { + auto obj = rfl::Generic::Object(); + obj["favorite_foods"] = foods(); + obj["age"] = rfl::Generic(int64_t{39}); + const auto res = rfl::from_generic(rfl::Generic(std::move(obj))); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->name.value(), "Homer Simpson"); + EXPECT_EQ(res->favorite_foods->at(0), "donuts"); + EXPECT_EQ(res->age.value().value(), 39); +} + +TEST(generic, test_default_val_with_validator_invalid_default) { + auto obj = rfl::Generic::Object(); + obj["favorite_foods"] = foods(); + const auto res = rfl::from_generic(rfl::Generic(std::move(obj))); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 'age' not found, and its default value is invalid: " + "Value expected to be greater than or equal to 1, but got 0."); + EXPECT_EQ(paths(res.error()), std::vector({{"age"}})); +} + +TEST(generic, test_default_val_with_validator_default_if_missing) { + auto obj = rfl::Generic::Object(); + obj["age"] = rfl::Generic(int64_t{3}); + const auto res = rfl::from_generic( + rfl::Generic(std::move(obj))); + ASSERT_FALSE(res); + EXPECT_EQ(paths(res.error()), std::vector({{"favorite_foods"}})); +} + +} // namespace test_default_val_with_validator diff --git a/tests/json/test_default_construct_error.cpp b/tests/json/test_default_construct_error.cpp new file mode 100644 index 00000000..e2586d8f --- /dev/null +++ b/tests/json/test_default_construct_error.cpp @@ -0,0 +1,151 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_default_construct_error { + +using Foods = + rfl::Validator, rfl::Size>>; + +// The invalid default values are constructed outside of the struct and then +// moved into it, so reading cannot defer them (see +// test_default_val_with_validator_limits.cpp). +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::DefaultVal favorite_foods = Foods(); +}; + +struct PersonWithValidDefault { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods = std::vector({"none"}); +}; + +struct Pet { + std::string name; + std::optional favorite_foods = Foods(); +}; + +struct Family { + std::string town; + Person head; +}; + +inline int no_default_port() { throw std::runtime_error("No default port"); } + +struct Server { + rfl::DefaultVal host = "localhost"; + int port = no_default_port(); +}; + +bool contains(const std::string& _str, const std::string& _substr) { + return _str.find(_substr) != std::string::npos; +} + +const std::string validation_error = + "Size validation failed: Value expected to be greater than or equal to 1, " + "but got 0."; + +TEST(json, test_default_construct_error_with_default_val) { + const auto res = + rfl::json::read(R"({"favorite_foods":["donuts","pizza"]})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Person")) << what; + EXPECT_TRUE(contains(what, "rfl::DefaultVal")) << what; + EXPECT_TRUE(contains(what, "valid default value")) << what; + EXPECT_TRUE(contains(what, validation_error)) << what; +} + +TEST(json, test_default_construct_error_with_default_if_missing) { + const auto json_string = R"({"name":"Bart","favorite_foods":["pizza"]})"; + + EXPECT_TRUE(rfl::json::read(json_string) && true); + + const auto res = rfl::json::read(json_string); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Pet")) << what; + EXPECT_TRUE(contains(what, "rfl::DefaultIfMissing")) << what; + EXPECT_TRUE(contains(what, validation_error)) << what; +} + +TEST(json, test_default_construct_error_in_nested_struct) { + const auto res = rfl::json::read( + R"({"town":"Springfield","head":{"favorite_foods":["donuts"]}})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "Failed to parse field 'head': ")) << what; + + const auto details = res.error().details(); + ASSERT_EQ(details.size(), 1); + EXPECT_EQ(details[0].path, + std::vector({"head"})); + EXPECT_TRUE(contains(details[0].message, "default-construct")); + EXPECT_TRUE(contains(details[0].message, "Person")); + EXPECT_TRUE(contains(details[0].message, validation_error)); +} + +TEST(json, test_default_construct_error_without_validator) { + const auto res = rfl::json::read(R"({"port":8080})"); + + ASSERT_FALSE(res && true); + const auto what = res.error().what(); + EXPECT_TRUE(contains(what, "default-construct")) << what; + EXPECT_TRUE(contains(what, "Server")) << what; + EXPECT_TRUE(contains(what, "No default port")) << what; +} + +TEST(json, test_default_construct_error_valid_default) { + const auto homer = rfl::json::read( + R"({"favorite_foods":["donuts","pizza"]})") + .value(); + + EXPECT_EQ(homer.name.value(), "Homer Simpson"); + EXPECT_EQ(homer.favorite_foods.value(), + std::vector({"donuts", "pizza"})); + + // The initializer does not make the field optional. + const auto missing = rfl::json::read(R"({})"); + + ASSERT_FALSE(missing && true); + EXPECT_EQ(missing.error().what(), "Field named 'favorite_foods' not found."); + + // With rfl::DefaultIfMissing, the initializer is the value of a missing + // field. + const auto with_default = + rfl::json::read(R"({})") + .value(); + + EXPECT_EQ(with_default.favorite_foods.value(), + std::vector({"none"})); +} + +TEST(json, test_default_construct_error_not_used_for_later_failures) { + const auto wrong_type = + rfl::json::read(R"({"favorite_foods":"donuts"})"); + + ASSERT_FALSE(wrong_type && true); + EXPECT_TRUE(contains(wrong_type.error().what(), + "Failed to parse field 'favorite_foods': ")) + << wrong_type.error().what(); + EXPECT_FALSE(contains(wrong_type.error().what(), "default-construct")) + << wrong_type.error().what(); + + const auto invalid = + rfl::json::read(R"({"favorite_foods":[]})"); + + ASSERT_FALSE(invalid && true); + EXPECT_EQ(invalid.error().what(), + "Failed to parse field 'favorite_foods': " + validation_error); +} +} // namespace test_default_construct_error diff --git a/tests/json/test_default_val_throwing_initializer.cpp b/tests/json/test_default_val_throwing_initializer.cpp new file mode 100644 index 00000000..3e09f81c --- /dev/null +++ b/tests/json/test_default_val_throwing_initializer.cpp @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include + +namespace test_default_val_throwing_initializer { + +using Positive = rfl::Validator>; + +static_assert(!std::is_nothrow_constructible_v, int>, + "Converting to a Validator can throw."); +static_assert(std::is_nothrow_constructible_v, short>); + +struct InvalidInitializer { + rfl::DefaultVal retries = 0; +}; + +struct ValidInitializer { + rfl::DefaultVal retries = 5; +}; + +TEST(json, test_default_val_throwing_initializer_throws) { + EXPECT_THROW((void)InvalidInitializer{}, std::exception); +} + +TEST(json, test_default_val_throwing_initializer_is_an_error_when_reading) { + const auto res = rfl::json::read(R"({"retries":3})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Could not default-construct " + "test_default_val_throwing_initializer::InvalidInitializer, which " + "is required because it contains rfl::DefaultVal. Give every field " + "a valid default value. Original error: Value expected to be " + "greater than or equal to 1, but got 0."); +} + +TEST(json, test_default_val_throwing_initializer_valid_default_is_used) { + const auto res = rfl::json::read(R"({})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->retries.value().value(), 5); +} + +} // namespace test_default_val_throwing_initializer diff --git a/tests/json/test_default_val_with_validator.cpp b/tests/json/test_default_val_with_validator.cpp new file mode 100644 index 00000000..8ac1d1ff --- /dev/null +++ b/tests/json/test_default_val_with_validator.cpp @@ -0,0 +1,216 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_default_val_with_validator { + +using Path = std::vector; + +using Foods = + rfl::Validator, rfl::Size>>; +using Positive = rfl::Validator>; + +const std::string foods_error = + "Size validation failed: Value expected to be greater than or equal to 1, " + "but got 0."; +const std::string positive_error = + "Value expected to be greater than or equal to 1, but got 0."; + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; +}; + +TEST(json, test_default_val_with_validator) { + const auto homer = + rfl::json::read(R"({"favorite_foods": ["donuts", "pizza"]})"); + ASSERT_TRUE(homer) << homer.error().what(); + EXPECT_EQ(homer->name.value(), "Homer Simpson"); + EXPECT_EQ(homer->favorite_foods.value(), + std::vector({"donuts", "pizza"})); + + const auto empty = rfl::json::read(R"({})"); + ASSERT_FALSE(empty); + EXPECT_EQ(empty.error().what(), "Field named 'favorite_foods' not found."); + + const auto invalid = rfl::json::read(R"({"favorite_foods": []})"); + ASSERT_FALSE(invalid); + EXPECT_EQ(invalid.error().what(), + "Failed to parse field 'favorite_foods': " + foods_error); + + // Reading does not make the default value valid elsewhere. + EXPECT_THROW((void)Person{}, std::exception); + EXPECT_THROW((void)Foods{}, std::exception); +} + +struct Reversed { + Foods favorite_foods; + rfl::DefaultVal name = "Homer Simpson"; +}; + +TEST(json, test_default_val_with_validator_reversed) { + const auto res = rfl::json::read( + R"({"name": "Bart", "favorite_foods": ["pizza"]})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(rfl::json::write(*res), + R"({"favorite_foods":["pizza"],"name":"Bart"})"); +} + +// A missing field keeps its default value, so with DefaultIfMissing or +// DefaultVal that value must be valid. +struct WithDefaultVal { + rfl::DefaultVal id; + rfl::DefaultVal retries; +}; + +TEST(json, test_default_val_with_validator_missing) { + const auto with_dim = + rfl::json::read(R"({})"); + ASSERT_FALSE(with_dim); + EXPECT_EQ(with_dim.error().what(), + "Field named 'favorite_foods' not found, and its default value is " + "invalid: " + + foods_error); + ASSERT_EQ(with_dim.error().details().size(), 1); + EXPECT_EQ(with_dim.error().details()[0].path, Path({"favorite_foods"})); + + const auto present = rfl::json::read( + R"({"favorite_foods": ["donuts"]})"); + ASSERT_TRUE(present) << present.error().what(); + EXPECT_EQ(present->name.value(), "Homer Simpson"); + + const auto missing = rfl::json::read(R"({"id": 1})"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().what(), + "Field named 'retries' not found, and its default value is " + "invalid: " + + positive_error); + + const auto res = rfl::json::read(R"({"retries": 3})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->retries.value().value(), 3); +} + +struct Family { + rfl::DefaultVal town = "Springfield"; + Person head; + std::vector children; +}; + +TEST(json, test_default_val_with_validator_nested) { + const auto res = rfl::json::read( + R"({"head": {"favorite_foods": ["donuts"]}, + "children": [{"name": "Bart", "favorite_foods": ["pizza"]}]})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->head.name.value(), "Homer Simpson"); + EXPECT_EQ(res->children.at(0).favorite_foods->at(0), "pizza"); + + const auto missing = + rfl::json::read(R"({"children": []})"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().what(), + "Field named 'head' not found, and its default value is invalid: " + + foods_error); + + // The nested struct is read as a whole, so its own missing field is + // reported, with the full path. + const auto inner_missing = + rfl::json::read(R"({"head": {}})"); + ASSERT_FALSE(inner_missing); + ASSERT_EQ(inner_missing.error().details().size(), 1); + EXPECT_EQ(inner_missing.error().details()[0].path, + Path({"head", "favorite_foods"})); +} + +struct Flat { + Positive f2; +}; + +struct Containers { + rfl::DefaultVal id; + std::array array; + std::optional optional; + std::variant variant; + rfl::TaggedUnion<"type", Person, Flat> tagged; + rfl::Flatten flat; + rfl::Rename<"renamed", Positive> rename; + rfl::ExtraFields extra; +}; + +TEST(json, test_default_val_with_validator_containers) { + const auto res = rfl::json::read( + R"({"array": [1, 2], "variant": 3, + "tagged": {"type": "Person", "favorite_foods": ["donuts"]}, + "f2": 4, "renamed": 5, "other": 6})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(rfl::json::write(*res), + R"({"id":0,"array":[1,2],"variant":3,)" + R"("tagged":{"type":"Person","name":"Homer Simpson",)" + R"("favorite_foods":["donuts"]},"f2":4,"renamed":5,"other":6})"); + + const auto missing = rfl::json::read( + R"({"array": [1, 2], "variant": 3, "f2": 4, "renamed": 5})"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().what(), + "Field named 'tagged' not found, and its default value is " + "invalid: " + + foods_error); + + const auto invalid_extra = rfl::json::read( + R"({"array": [1, 2], "variant": 3, + "tagged": {"type": "Flat", "f2": 1}, + "f2": 4, "renamed": 5, "other": 0})"); + ASSERT_FALSE(invalid_extra); + EXPECT_EQ(invalid_extra.error().what(), + "Failed to parse field 'other': " + positive_error); +} + +TEST(json, test_default_val_with_validator_no_field_names) { + const auto res = rfl::json::read( + R"(["Bart", ["pizza"]])"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->name.value(), "Bart"); + + const auto missing = + rfl::json::read( + R"(["Bart"])"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().what(), + "Field named 'favorite_foods' not found, and its default value is " + "invalid: " + + foods_error); +} + +struct Snake { + rfl::DefaultVal user_id; + Positive retry_count; +}; + +TEST(json, test_default_val_with_validator_processors) { + using Processors = + rfl::Processors>; + const auto res = rfl::json::read( + R"({"type": "Snake", "retryCount": 2})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->retry_count.value(), 2); + + const auto missing = + rfl::json::read( + R"({"type": "Snake"})"); + ASSERT_FALSE(missing); + EXPECT_EQ(missing.error().details().at(0).path, Path({"retryCount"})); + + const auto fail_fast = + rfl::json::read(R"({})"); + ASSERT_FALSE(fail_fast); + EXPECT_EQ(fail_fast.error().details().size(), 1); +} + +} // namespace test_default_val_with_validator diff --git a/tests/json/test_default_val_with_validator_limits.cpp b/tests/json/test_default_val_with_validator_limits.cpp new file mode 100644 index 00000000..97a09bb6 --- /dev/null +++ b/tests/json/test_default_val_with_validator_limits.cpp @@ -0,0 +1,339 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// What does not change, and the limits of reading structs that contain a +// Validator without a valid default value. +namespace test_default_val_with_validator_limits { + +using Path = std::vector; + +using Foods = + rfl::Validator, rfl::Size>>; +using Positive = rfl::Validator>; + +const std::string positive_error = + "Value expected to be greater than or equal to 1, but got 0."; + +std::string construction_error(const std::string& _type) { + return "Could not default-construct " + "test_default_val_with_validator_limits::" + + _type + + ", which is required because it contains rfl::DefaultVal. Give every " + "field a valid default value. Original error: " + + positive_error; +} + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; +}; + +// A default member initializer that catches the exception of a Validator +// still sees it, as long as the struct can be default-constructed. If it +// cannot and its validation is deferred, that depends on where the compiler +// constructs the Validator that port_or_fallback() returns: outside of the +// struct, where it throws as usual (Positive is returned in registers under +// the Itanium ABI), or directly inside the struct, where its validation is +// deferred like that of any other field (MSVC). +inline Positive port_or_fallback() { + try { + return Positive(); + } catch (std::exception&) { + return Positive(8080); + } +} + +struct Fallback { + rfl::DefaultVal host = "localhost"; + Positive port = port_or_fallback(); +}; + +struct FallbackAndInvalid { + Positive port = port_or_fallback(); + Positive retries; +}; + +TEST(json, test_default_val_with_validator_limits_fallback) { + const auto res = rfl::json::read(R"({})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(rfl::json::write(*res), R"({"host":"localhost","port":8080})"); + + const auto res2 = rfl::json::read( + R"({"retries": 2})"); + if (res2) { + EXPECT_EQ(rfl::json::write(*res2), R"({"port":8080,"retries":2})"); + } else { + EXPECT_EQ(std::string(res2.error().what()), + "Field named 'port' not found, and its default value is " + "invalid: " + + positive_error); + } + + // If the input contains the field, it does not matter. + const auto res3 = rfl::json::read( + R"({"port": 443, "retries": 2})"); + ASSERT_TRUE(res3) << res3.error().what(); + EXPECT_EQ(rfl::json::write(*res3), R"({"port":443,"retries":2})"); +} + +// A read inside a default member initializer has its own invalid default +// values deferred, not those of the struct around it. +struct WithNestedRead { + Person person = + rfl::json::read(R"({"favorite_foods": ["beer"]})").value(); + rfl::DefaultVal retries; +}; + +TEST(json, test_default_val_with_validator_limits_nested_read) { + const auto res = rfl::json::read( + R"({"retries": 2})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->person.favorite_foods->at(0), "beer"); + EXPECT_EQ(res->retries.value().value(), 2); +} + +// Validators that are default-constructed on other threads still throw. +TEST(json, test_default_val_with_validator_limits_threads) { + std::atomic done = false; + std::atomic constructed = 0; + auto other = std::thread([&] { + while (!done) { + try { + Foods foods; + ++constructed; + } catch (std::exception&) { + } + } + }); + auto readers = std::vector(); + std::atomic failed_reads = 0; + for (int i = 0; i < 2; ++i) { + readers.emplace_back([&] { + for (int j = 0; j < 2000; ++j) { + if (!rfl::json::read(R"({"favorite_foods": ["donuts"]})")) { + ++failed_reads; + } + } + }); + } + for (auto& reader : readers) { + reader.join(); + } + done = true; + other.join(); + EXPECT_EQ(failed_reads, 0); + EXPECT_EQ(constructed, 0); +} + +// The regressions found in the review of an earlier attempt to fix upstream +// issue #700. +struct WithMap { + rfl::DefaultVal x; + std::map m; +}; + +struct NoDefaultConstructor { + NoDefaultConstructor(int _v) : v_(_v) {} + using ReflectionType = int; + int reflection() const { return v_; } + int v_; +}; + +struct NotDefaultConstructible { + NoDefaultConstructor n; + Positive p; +}; + +static_assert(!std::is_default_constructible_v); + +struct ConstValidator { + const rfl::Validator> age; + rfl::DefaultVal name = "Maggie"; +}; + +TEST(json, test_default_val_with_validator_limits_earlier_regressions) { + EXPECT_FALSE(rfl::json::read(R"({"m": {"0": "zero"}})")); + const auto map = rfl::json::read(R"({"m": {"1": "one"}})"); + ASSERT_TRUE(map) << map.error().what(); + EXPECT_EQ(map->m.begin()->first.value(), 1); + + const auto ndc = + rfl::json::read(R"({"n": 3, "p": 4})"); + ASSERT_TRUE(ndc) << ndc.error().what(); + EXPECT_EQ(ndc->p.value(), 4); + + const auto con = rfl::json::read(R"({"age": 8})"); + ASSERT_TRUE(con) << con.error().what(); + EXPECT_EQ(rfl::json::write(*con), R"({"age":8,"name":"Maggie"})"); +} + +// A class that is not reflectable, kept out of reading by rfl::Skip. +class Cache { + public: + int hits() const { return hits_; } + + private: + int hits_ = 0; + std::string last_; +}; + +struct WithCache { + rfl::DefaultVal port = 8080; + rfl::Skip cache; + Positive retries; +}; + +struct Schema { + rfl::DefaultVal nullable = false; + std::optional> all_of; + std::optional> properties; + std::unique_ptr items; +}; + +TEST(json, test_default_val_with_validator_limits_other_types) { + const auto cache = rfl::json::read(R"({"retries": 1})"); + ASSERT_TRUE(cache) << cache.error().what(); + EXPECT_EQ(cache->port.value(), 8080); + + const auto schema = rfl::json::read( + R"({"properties": {"a": {"nullable": true}}, "items": {}})"); + ASSERT_TRUE(schema) << schema.error().what(); + EXPECT_TRUE(schema->properties->at("a").nullable.value()); + EXPECT_FALSE(schema->items->nullable.value()); +} + +// The JSON schema leaves out the default values of a struct that cannot be +// default-constructed. +struct WithPerson { + int id; + Person person; +}; + +TEST(json, test_default_val_with_validator_limits_to_schema) { + const auto schema = rfl::json::to_schema(); + EXPECT_NE(schema.find(R"("favorite_foods":{"type":"array")"), + std::string::npos) + << schema; + EXPECT_EQ(schema.find("Homer Simpson"), std::string::npos) << schema; + + const auto nested = rfl::json::to_schema(); + EXPECT_NE(nested.find(R"("person":{"$ref":)"), std::string::npos) << nested; +} + +// [[no_unique_address]] can place a field in the tail padding of another. +struct InTailPadding { + rfl::Validator> a; + int8_t c = 0; +}; + +struct SharesPadding { + [[no_unique_address]] InTailPadding inner; + rfl::DefaultVal p; +}; + +TEST(json, test_default_val_with_validator_limits_no_unique_address) { + const auto res = + rfl::json::read(R"({"inner": {"a": 1, "c": 2}})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().details().at(0).path, Path({"p"})); + + const auto res2 = + rfl::json::read(R"({"inner": {"a": 1, "c": 2}, "p": 3})"); + ASSERT_TRUE(res2) << res2.error().what(); + EXPECT_EQ(res2->p.value().value(), 3); +} + +// A field in the input replaces the default value as a whole. Should a field +// ever be read into its default value instead, the invalid default of p would +// survive here. +struct Inner { + Positive p; + rfl::DefaultVal q; +}; + +struct Outer { + rfl::DefaultVal x; + Inner inner; +}; + +TEST(json, test_default_val_with_validator_limits_found_fields_are_replaced) { + const auto res = + rfl::json::read(R"({"inner": {"q": 1}})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().details().at(0).path, Path({"inner", "p"})); +} + +// Only a Validator that is default-constructed inside the struct can be +// deferred. Anywhere else, it throws as before, and so does T{}. +struct OnTheHeap { + rfl::DefaultVal x; + std::vector v = std::vector(1); +}; + +struct FromTemporary { + rfl::DefaultVal x; + rfl::DefaultVal p = Positive(); +}; + +struct InRflTuple { + rfl::DefaultVal x; + rfl::Tuple t; +}; + +TEST(json, test_default_val_with_validator_limits_not_in_place) { + const auto heap = rfl::json::read(R"({"v": [2]})"); + ASSERT_FALSE(heap); + EXPECT_EQ(heap.error().what(), construction_error("OnTheHeap")); + + const auto temp = rfl::json::read(R"({"p": 2})"); + ASSERT_FALSE(temp); + EXPECT_EQ(temp.error().what(), construction_error("FromTemporary")); + + const auto tuple = rfl::json::read(R"({"t": [2, 3]})"); + ASSERT_FALSE(tuple); + EXPECT_EQ(tuple.error().what(), construction_error("InRflTuple")); +} + +// A field that is never read keeps its default value. +struct SkipsValidator { + rfl::DefaultVal x; + rfl::Skip s; +}; + +TEST(json, test_default_val_with_validator_limits_skip) { + const auto res = rfl::json::read(R"({"x": 1})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 's' not found, and its default value is invalid: " + + positive_error); +} + +// Known gap: a default member initializer that copies a Validator whose +// default value is invalid. Outside of read, Copies{} throws, but the copy +// in b is not checked, so the read succeeds with an invalid value. +struct Copies { + Positive a; + rfl::DefaultVal b = a; +}; + +TEST(json, test_default_val_with_validator_limits_copy_is_not_checked) { + EXPECT_THROW((void)Copies{}, std::exception); + const auto res = rfl::json::read(R"({"a": 5})"); + ASSERT_TRUE(res) << res.error().what(); + EXPECT_EQ(res->a.value(), 5); + EXPECT_EQ(res->b.value().value(), 0); +} + +} // namespace test_default_val_with_validator_limits diff --git a/tests/json/test_skip_throwing_default.cpp b/tests/json/test_skip_throwing_default.cpp new file mode 100644 index 00000000..652eff91 --- /dev/null +++ b/tests/json/test_skip_throwing_default.cpp @@ -0,0 +1,107 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace test_skip_throwing_default { + +using Positive = rfl::Validator>; + +using Path = std::vector; + +struct WithSkip { + rfl::Skip s; + int y; +}; + +struct SkipLast { + int y; + rfl::Skip s; +}; + +/// Not required, because its ReflectionType is a std::optional, and counts +/// the instances that are alive. +struct ThrowingDefault { + using ReflectionType = std::optional; + + static inline int alive = 0; + + ThrowingDefault() { throw std::runtime_error("no default"); } + ThrowingDefault(const ReflectionType&) { ++alive; } + ThrowingDefault(const ThrowingDefault&) { ++alive; } + ~ThrowingDefault() { --alive; } + + ReflectionType reflection() const { return std::nullopt; } +}; + +struct WithThrowingDefault { + int y; + ThrowingDefault t; +}; + +TEST(json, test_skip_throwing_default) { + const auto res = rfl::json::read(R"({"y": 1, "s": 5})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Failed to parse field 's': Value expected to be greater than or " + "equal to 1, but got 0."); +} + +TEST(json, test_skip_throwing_default_field_absent) { + const auto res = rfl::json::read(R"({"y": 1})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 's' not found, and its default value could not be " + "constructed: Value expected to be greater than or equal to 1, " + "but got 0."); + const auto details = res.error().details(); + ASSERT_EQ(details.size(), 1); + EXPECT_EQ(details[0].path, (Path{"s"})); + EXPECT_EQ(details[0].message, res.error().what()); +} + +TEST(json, test_skip_throwing_default_field_absent_no_field_names) { + const auto res = rfl::json::read(R"([1])"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 's' not found, and its default value could not be " + "constructed: Value expected to be greater than or equal to 1, " + "but got 0."); +} + +TEST(json, test_skip_throwing_default_tuple_element_absent) { + const auto res = + rfl::json::read>>(R"([1])"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field 1 was missing, and its default value could not be " + "constructed: Value expected to be greater than or equal to 1, " + "but got 0."); + const auto details = res.error().details(); + ASSERT_EQ(details.size(), 1); + EXPECT_EQ(details[0].path, (Path{size_t{1}})); +} + +// No destructor may run for the field that could not be constructed. +TEST(json, test_skip_throwing_default_does_not_destroy_the_field) { + const auto res = rfl::json::read(R"({"y": 1})"); + ASSERT_FALSE(res); + EXPECT_EQ(res.error().what(), + "Field named 't' not found, and its default value could not be " + "constructed: no default"); + EXPECT_EQ(ThrowingDefault::alive, 0); + + const auto tup = + rfl::json::read>(R"([1])"); + ASSERT_FALSE(tup); + EXPECT_EQ(ThrowingDefault::alive, 0); + + ASSERT_TRUE(rfl::json::read(R"({"y": 1, "t": 2})")); + EXPECT_EQ(ThrowingDefault::alive, 0); +} + +} // namespace test_skip_throwing_default