diff --git a/docs/content/default_val.md b/docs/content/default_val.md index ced77a07..6b29b52b 100644 --- a/docs/content/default_val.md +++ b/docs/content/default_val.md @@ -52,6 +52,42 @@ const auto homer = rfl::json::read(R"(["Homer"])").va // homer.town == "" ``` +## All fields need a valid default value + +A struct that contains an `rfl::DefaultVal` is default-constructed before it is read. The fields found in the input are then written into it. This means that every field of the struct needs a valid default value, even a required field that is in the input. + +This matters when the default value of a field's type is not valid, for instance when the field has a validator: + +```cpp +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::Validator, rfl::Size>> favorite_foods; +}; + +// Fails, even though favorite_foods is in the input. +rfl::json::read(R"({"favorite_foods":["donuts","pizza"]})"); +``` + +An empty vector does not satisfy `rfl::Size>`, so `Person{}` throws an exception and you get the following error: + +``` +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. +``` + +To avoid this, give the field a valid default value: + +```cpp +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::Validator, rfl::Size>> favorite_foods = + std::vector({"none"}); +}; +``` + +The field is still required. If `favorite_foods` is missing in the input, you get the error `Field named 'favorite_foods' not found.` The default value is only used for a missing field if you pass the `rfl::DefaultIfMissing` processor. + +The same restriction applies to every struct that is read with the `rfl::DefaultIfMissing` processor, whether it contains an `rfl::DefaultVal` or not. + ## 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..a431bb4e 100644 --- a/docs/content/processors.md +++ b/docs/content/processors.md @@ -310,6 +310,10 @@ 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. Every field +therefore needs a valid default value, even if the field is in the input. +This is explained in the documentation on `rfl::DefaultVal`. + ### `rfl::DurationsAsCount` By default, `std::chrono::duration` types are written as an object containing diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index f17c13e1..91cd540d 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -2,12 +2,14 @@ #define RFL_PARSING_PARSER_DEFAULT_HPP_ #include +#include #include #include "../Result.hpp" #include "../always_false.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" @@ -720,22 +722,39 @@ struct Parser { static Result read_struct_with_default(const R& _r, const InputVarType& _var) { 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)); - using ViewType = decltype(view); - auto err = - Parser::read_view_with_default( - _r, _var, &view); - if (err) [[unlikely]] { - return error(std::move(*err)); + // This might fail, for instance if the default value of a field does not + // satisfy its validator. The outer handler is for this line only. + auto t = T{}; + try { + auto view = ProcessorsType::template process(to_view(t)); + using ViewType = decltype(view); + auto err = + Parser::read_view_with_default( + _r, _var, &view); + if (err) [[unlikely]] { + return error(std::move(*err)); + } + return t; + } catch (std::exception& e) { + return error(e.what()); } - return t; } catch (std::exception& e) { - return error(e.what()); + return error(default_construction_error(e.what())); } } + + /// 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/tests/json/test_default_construct_error.cpp b/tests/json/test_default_construct_error.cpp new file mode 100644 index 00000000..4157eadd --- /dev/null +++ b/tests/json/test_default_construct_error.cpp @@ -0,0 +1,147 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_default_construct_error { + +using Foods = + rfl::Validator, rfl::Size>>; + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods; +}; + +struct PersonWithValidDefault { + rfl::DefaultVal name = "Homer Simpson"; + Foods favorite_foods = std::vector({"none"}); +}; + +struct Pet { + std::string name; + Foods favorite_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