diff --git a/llarp/config/definition.cpp b/llarp/config/definition.cpp index 76c61e2b4..a169c39cf 100644 --- a/llarp/config/definition.cpp +++ b/llarp/config/definition.cpp @@ -36,12 +36,51 @@ Configuration::defineOption(ConfigDefinition_ptr def) Configuration& Configuration::addConfigValue(string_view section, string_view name, string_view value) { - ConfigDefinition_ptr& definition = lookupDefinitionOrThrow(section, name); + auto secItr = m_definitions.find(std::string(section)); + if (secItr == m_definitions.end()) + { + // fallback to undeclared handler if available + auto undItr = m_undeclaredHandlers.find(std::string(section)); + if (undItr == m_undeclaredHandlers.end()) + throw std::invalid_argument(stringify("no declared section [", section, "]")); + else + { + auto& handler = undItr->second; + handler(section, name, value); + return *this; + } + } + + // section was valid, get definition by name + auto& sectionDefinitions = secItr->second; + auto defItr = sectionDefinitions.find(std::string(name)); + if (defItr == sectionDefinitions.end()) + throw std::invalid_argument(stringify("no declared option [", section, "]:", name)); + + ConfigDefinition_ptr& definition = defItr->second; definition->parseValue(std::string(value)); return *this; } +void +Configuration::addUndeclaredHandler(const std::string& section, UndeclaredValueHandler handler) +{ + auto itr = m_undeclaredHandlers.find(section); + if (itr != m_undeclaredHandlers.end()) + throw std::logic_error(stringify("section ", section, " already has a handler")); + + m_undeclaredHandlers[section] = std::move(handler); +} + +void +Configuration::removeUndeclaredHandler(const std::string& section) +{ + auto itr = m_undeclaredHandlers.find(section); + if (itr != m_undeclaredHandlers.end()) + m_undeclaredHandlers.erase(itr); +} + void Configuration::validateRequiredFields() { @@ -110,7 +149,7 @@ Configuration::lookupDefinitionOrThrow(string_view section, string_view name) co { const auto sectionItr = m_definitions.find(std::string(section)); if (sectionItr == m_definitions.end()) - throw std::invalid_argument(stringify("No config section ", section)); + throw std::invalid_argument(stringify("No config section [", section, "]")); auto& sectionDefinitions = sectionItr->second; const auto definitionItr = sectionDefinitions.find(std::string(name)); diff --git a/llarp/config/definition.hpp b/llarp/config/definition.hpp index ff2f82e58..c68a9603e 100644 --- a/llarp/config/definition.hpp +++ b/llarp/config/definition.hpp @@ -177,6 +177,9 @@ namespace llarp std::function acceptor; }; + using UndeclaredValueHandler + = std::function; + using ConfigDefinition_ptr = std::unique_ptr; @@ -201,7 +204,6 @@ namespace llarp /// with defaults and optionally fields which have a specified value (values provided through /// calls to addConfigValue()). struct Configuration { - SectionMap m_definitions; /// Spefify the parameters and type of a configuration option. The parameters are members of /// ConfigDefinitionBase; the type is inferred from ConfigDefinition's template parameter T. @@ -264,6 +266,25 @@ namespace llarp return derived->getValue(); } + /// Add an "undeclared" handler for the given section. This is a handler that will be called + /// whenever a k:v pair is found that doesn't match a provided definition. + /// + /// Any exception thrown by the handler will progagate back through the call to + /// addConfigValue(). + /// + /// @param section is the section for which any undeclared values will invoke the provided + /// handler + /// @param handler + /// @throws if there is already a handler for this section + void + addUndeclaredHandler(const std::string& section, UndeclaredValueHandler handler); + + /// Removes an "undeclared" handler for the given section. + /// + /// @param section is the section which we want to remove the handler for + void + removeUndeclaredHandler(const std::string& section); + /// Validate that all required fields are present. /// /// @throws std::invalid_argument if configuration constraints are not met @@ -303,6 +324,10 @@ namespace llarp using DefVisitor = std::function; void visitDefinitions(const std::string& section, DefVisitor visitor) const; + SectionMap m_definitions; + + std::unordered_map m_undeclaredHandlers; + // track insertion order std::vector m_sectionOrdering; std::unordered_map> m_definitionOrdering; diff --git a/test/config/test_llarp_config_definition.cpp b/test/config/test_llarp_config_definition.cpp index b814fe902..34759024d 100644 --- a/test/config/test_llarp_config_definition.cpp +++ b/test/config/test_llarp_config_definition.cpp @@ -1,7 +1,10 @@ #include +#include #include +using llarp::string_view; + TEST_CASE("ConfigDefinition int parse test", "[config]") { llarp::ConfigDefinition def("foo", "bar", false, 42); @@ -214,3 +217,99 @@ TEST_CASE("Configuration defineOptions passthrough test", "[config]") config.defineOption("foo", "bar", false, 1); CHECK(config.getConfigValue("foo", "bar") == 1); } + +TEST_CASE("Configuration undeclared definition basic test", "[config]") +{ + llarp::Configuration config; + + bool invoked = false; + + config.addUndeclaredHandler("foo", [&](string_view section, string_view name, string_view value) { + CHECK(section == "foo"); + CHECK(name == "bar"); + CHECK(value == "val"); + + invoked = true; + }); + + REQUIRE_NOTHROW(config.addConfigValue("foo", "bar", "val")); + + CHECK(invoked); +} + +TEST_CASE("Configuration undeclared add more than once test", "[config]") +{ + llarp::Configuration config; + + std::string calledBy = ""; + + config.addUndeclaredHandler("foo", [&](string_view, string_view, string_view) { + calledBy = "a"; + }); + REQUIRE_THROWS_WITH( + config.addUndeclaredHandler("foo", [&](string_view, string_view, string_view) { + calledBy = "b"; + }), + "section foo already has a handler"); + + REQUIRE_NOTHROW(config.addConfigValue("foo", "bar", "val")); + + CHECK(calledBy == "a"); +} + +TEST_CASE("Configuration undeclared add/remove test", "[config]") +{ + llarp::Configuration config; + + std::string calledBy = ""; + + // add... + REQUIRE_NOTHROW(config.addUndeclaredHandler("foo", [&](string_view, string_view, string_view) { + calledBy = "a"; + })); + + REQUIRE_NOTHROW(config.addConfigValue("foo", "bar", "val")); + + CHECK(calledBy == "a"); + + calledBy = ""; + + // ...then remove... + REQUIRE_NOTHROW(config.removeUndeclaredHandler("foo")); + + CHECK_THROWS_WITH( + config.addConfigValue("foo", "bar", "val"), + "no declared section [foo]"); + + // ...then add again + REQUIRE_NOTHROW(config.addUndeclaredHandler("foo", [&](string_view, string_view, string_view) { + calledBy = "b"; + })); + + REQUIRE_NOTHROW(config.addConfigValue("foo", "bar", "val")); + + CHECK(calledBy == "b"); +} + +TEST_CASE("Configuration undeclared handler exception propagation test", "[config]") +{ + llarp::Configuration config; + + config.addUndeclaredHandler("foo", [](string_view, string_view, string_view) { + throw std::runtime_error("FAIL"); + }); + + REQUIRE_THROWS_WITH(config.addConfigValue("foo", "bar", "val"), "FAIL"); +} + +TEST_CASE("Configuration undeclared handler wrong section", "[config]") +{ + llarp::Configuration config; + + config.addUndeclaredHandler("foo", [](string_view, string_view, string_view) { + throw std::runtime_error("FAIL"); + }); + + REQUIRE_THROWS_WITH(config.addConfigValue("argle", "bar", "val"), "no declared section [argle]"); +} +