diff --git a/include/rfl/Validator.hpp b/include/rfl/Validator.hpp index 7ced2daa..e8442154 100644 --- a/include/rfl/Validator.hpp +++ b/include/rfl/Validator.hpp @@ -11,6 +11,13 @@ #include "Result.hpp" #include "internal/HasValidation.hpp" +namespace rfl::internal::parsing { +// Forward declare this method used to determine if validation is called from +// the Parser +constexpr bool called_from_read_struct_with_default( + const std::source_location& loc); +} // namespace rfl::internal::parsing + namespace rfl { /// A wrapper that validates a value against one or more validation rules. @@ -40,8 +47,12 @@ 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 constructor is being called from + /// `rfl::Parser::read_struct_with_default()`. See the documentation of that + /// method for more details. + Validator(const std::source_location& loc = std::source_location::current()) + : value_(validate_from_default_constructor_(T(), loc)) {} /// Move constructor. /// @param _other The validator to move from @@ -170,6 +181,16 @@ struct Validator { const T& reflection() const { return value_; } private: + T&& validate_from_default_constructor_(T&& value, + const std::source_location& loc) { + // If this was originally called from + // `rfl::parsing::Parser::read_struct_with_default()`, then skip validation + if (internal::parsing::called_from_read_struct_with_default(loc)) { + return std::move(value); + } + return ValidationType::validate(std::move(value)).value(); + } + /// The underlying value. T value_; }; diff --git a/include/rfl/parsing/Parser_default.hpp b/include/rfl/parsing/Parser_default.hpp index b9a7f1a7..3343e970 100644 --- a/include/rfl/parsing/Parser_default.hpp +++ b/include/rfl/parsing/Parser_default.hpp @@ -67,6 +67,43 @@ #include "schemaful/IsSchemafulReader.hpp" #include "schemaful/IsSchemafulWriter.hpp" +namespace rfl::internal::parsing { + +/// This method is meant to be used `rfl::Validator` to check if the caller is +/// `Parser::read_struct_with_default()` to allow an "escape-hatch" with +/// validation. See that method and the constructor for `rfl::Validator` for +/// more details. +/// +/// This method is implemented in this file so that we can do an extra check +/// against the file name of the caller (using `std::source_location`), without +/// hardcoding the file path. However, the Parser class name and the method are +/// hardcoded here. +constexpr bool called_from_read_struct_with_default( + const std::source_location& loc) { + constexpr std::source_location this_loc = std::source_location::current(); + constexpr std::string_view parser_class_name = "rfl::parsing::Parser"; + constexpr std::string_view target_fname = "read_struct_with_default("; + + std::string_view caller_function_name{loc.function_name()}; + + // Check that the expected class name is present in the full function name + if (caller_function_name.find(parser_class_name) == std::string_view::npos) { + return false; + } + // Check that the expected function name is present in the full function name + if (caller_function_name.find(target_fname) == std::string_view::npos) { + return false; + } + // Check that the filename matches this one + if (loc.file_name() != this_loc.file_name()) { + return false; + } + + return true; +} + +} // namespace rfl::internal::parsing + namespace rfl::parsing { /// Default case - anything that cannot be explicitly matched. @@ -694,6 +731,19 @@ struct Parser { static Result read_struct_with_default(const R& _r, const InputVarType& _var) { try { + // `rfl::Validator` has an "escape-hatch" that will skip validation on + // construction in the specific case that the caller has an + // `std::source_location` that matches this specific function, to help + // with this specific case where we want to default initialize the struct + // to get the default values of the fields, but for which some of the + // defaults do not satisfy the validation (e.g. a field with a min size + // constraint that has no default). This should be safe because the final + // parser will overwrite these fields again, and errors will be raised for + // invalid default values. + // We also assert here to be sure `called_from_read_struct_with_default()` + // has been implemented correctly. + static_assert(internal::parsing::called_from_read_struct_with_default( + std::source_location::current())); 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. diff --git a/tests/json/test_default_val.cpp b/tests/json/test_default_val.cpp index 9ef09671..bbe2d3e4 100644 --- a/tests/json/test_default_val.cpp +++ b/tests/json/test_default_val.cpp @@ -36,3 +36,35 @@ TEST(json, test_default_val) { R"({"first_name":"Waylon","last_name":"Smith","town":"Springfield","country":"USA"})"); } } // namespace test_default_val + +namespace test_default_val_alongside_size_validator { + +struct Person { + rfl::DefaultVal name = "Homer Simpson"; + rfl::Validator, rfl::Size>> + favorite_foods; +}; + +TEST(json, test_default_val_alongside_size_validator) { + static_assert(rfl::internal::has_default_val_v, + "Should have default val"); + + const auto should_fail = rfl::json::read(R"({})"); + + EXPECT_EQ(should_fail && true, false) + << "Should have failed due to missing required field"; + + 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().size(), 2); + EXPECT_EQ(homer.favorite_foods.value()[0], "donuts"); + EXPECT_EQ(homer.favorite_foods.value()[1], "pizza"); + + write_and_read( + Person{"Waylon Smith", std::vector{"sushi", "pizza"}}, + R"({"name":"Waylon Smith","favorite_foods":["sushi","pizza"]})"); +} +} // namespace test_default_val_alongside_size_validator