diff --git a/src/core/io/src/4C_io_input_spec_builders.hpp b/src/core/io/src/4C_io_input_spec_builders.hpp index 4381fc14ec..c3a2c17f5e 100644 --- a/src/core/io/src/4C_io_input_spec_builders.hpp +++ b/src/core/io/src/4C_io_input_spec_builders.hpp @@ -833,6 +833,19 @@ namespace Core::IO [&](const auto& val) { return this->operator()(val.second, size_info + 1); }); } }; + + + //! Helper for selection(). + struct BasedOn + { + std::string name; + }; + + //! Helper for selection(). + struct FromChoices + { + std::map choices; + }; } // namespace Internal /** @@ -1033,6 +1046,48 @@ namespace Core::IO [[nodiscard]] InputSpec group( std::string name, std::vector specs, GroupData data = {}); + /** + * This function is used to select one of multiple InputSpecs based on the value of a parameter + * with name given in @p selector. The selector parameter is always put inside a group with the + * given @p name. For every possible value of the selector, a different InputSpec is expected + * inside the group @p name. For example: + * + * @code + * auto spec = selection("Time integration", based_on("scheme"), + * from_choices({ + * {"OST", parameter("theta")}, + * {"GenAlpha", all_of({ + * parameter("alpha_f"), + * parameter("alpha_m"), + * }), + * })); + * @endcode + * + * will match the following input: + * + * @code + * Time integration: + * scheme: OST + * theta: 0.5 + * @endcode + * + * or the following input: + * + * @code + * Time integration: + * scheme: GenAlpha + * alpha_f: 1 + * alpha_m: 0.5 + * @endcode + * + * @note This function uses the fluent interface idiom and wraps the name of the @p selector + * with based_on() and the choices with from_choices(). + */ + template + requires(std::is_same_v) + [[nodiscard]] InputSpec selection(std::string name, Internal::BasedOn selector, + Internal::FromChoices choices, GroupData data = {}); + /** * All of the given InputSpecs are expected, e.g., * @@ -1190,6 +1245,17 @@ namespace Core::IO * InputSpec, you should rarely need the list() function. */ [[nodiscard]] InputSpec list(std::string name, InputSpec spec, ListData data = {}); + + /** + * A fluent interface helper. Used for selection(). + */ + [[nodiscard]] inline Internal::BasedOn based_on(std::string name); + + /** + * A fluent interface helper. Used for selection(). + */ + [[nodiscard]] inline Internal::FromChoices from_choices( + std::map choices); } // namespace InputSpecBuilders } // namespace Core::IO @@ -1655,6 +1721,24 @@ Core::IO::InputSpec Core::IO::InputSpecBuilders::parameter( }); } +template + requires(std::is_same_v) +Core::IO::InputSpec Core::IO::InputSpecBuilders::selection( + std::string name, Internal::BasedOn selector, Internal::FromChoices choices, GroupData data) +{ + std::vector specs; + specs.reserve(choices.choices.size()); + for (auto&& [choice_name, choice_spec] : choices.choices) + { + specs.emplace_back(all_of({ + selection(selector.name, {choice_name}, + {.description = "Selects which other parameters are allowed in this group."}), + std::move(choice_spec), + })); + } + return group(name, {one_of(specs)}, data); +} + template Core::IO::InputSpec Core::IO::InputSpecBuilders::Internal::selection_internal( @@ -1766,6 +1850,20 @@ auto Core::IO::InputSpecBuilders::store_index_as(std::string name, std::vector choices) +{ + return Internal::FromChoices{std::move(choices)}; +} + + FOUR_C_NAMESPACE_CLOSE #endif diff --git a/src/core/io/tests/4C_io_input_spec_test.cpp b/src/core/io/tests/4C_io_input_spec_test.cpp index 2196d7c280..eff1b5acd8 100644 --- a/src/core/io/tests/4C_io_input_spec_test.cpp +++ b/src/core/io/tests/4C_io_input_spec_test.cpp @@ -952,6 +952,75 @@ required: true } + TEST(InputSpecTest, MatchYamlGroupWithSelection) + { + auto spec = selection("model", based_on("type"), + from_choices({ + {"linear", parameter("coefficient")}, + {"quadratic", one_of({ + all_of({ + parameter("a"), + parameter("b"), + }), + parameter("c"), + })}, + })); + + { + SCOPED_TRACE("First selection"); + ryml::Tree tree = init_yaml_tree_with_exceptions(); + ryml::NodeRef root = tree.rootref(); + ryml::parse_in_arena(R"(model: + type: linear + coefficient: 1.0 +)", + root); + + ConstYamlNodeRef node(root, ""); + InputParameterContainer container; + spec.match(node, container); + EXPECT_EQ(container.group("model").get("type"), "linear"); + EXPECT_EQ(container.group("model").get("coefficient"), 1.0); + } + + { + SCOPED_TRACE("Second selection"); + ryml::Tree tree = init_yaml_tree_with_exceptions(); + ryml::NodeRef root = tree.rootref(); + ryml::parse_in_arena(R"(model: + type: quadratic + a: 1 + b: 2.0 +)", + root); + + ConstYamlNodeRef node(root, ""); + InputParameterContainer container; + spec.match(node, container); + EXPECT_EQ(container.group("model").get("type"), "quadratic"); + EXPECT_EQ(container.group("model").get("a"), 1); + EXPECT_EQ(container.group("model").get("b"), 2.0); + } + + { + SCOPED_TRACE("Second selection, other one_of"); + ryml::Tree tree = init_yaml_tree_with_exceptions(); + ryml::NodeRef root = tree.rootref(); + ryml::parse_in_arena(R"(model: + type: quadratic + c: 3.0 +)", + root); + + ConstYamlNodeRef node(root, ""); + InputParameterContainer container; + spec.match(node, container); + EXPECT_EQ(container.group("model").get("type"), "quadratic"); + EXPECT_EQ(container.group("model").get("c"), 3.0); + } + } + + TEST(InputSpecTest, MatchYamlAllOf) { auto spec = all_of({