diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index d18e8b3a8..003464c14 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -2,6 +2,7 @@ include(Version) set(LIB_UTIL_SRC config/config.cpp + config/definition.cpp config/ini.cpp config/key_manager.cpp constants/limits.cpp diff --git a/llarp/config/definition.cpp b/llarp/config/definition.cpp new file mode 100644 index 000000000..cbf918d49 --- /dev/null +++ b/llarp/config/definition.cpp @@ -0,0 +1,32 @@ +#include + +#include + +namespace llarp +{ + +ConfigDefinitionBase::ConfigDefinitionBase(std::string section_, + std::string name_, + bool required_, + bool multiValued_) + : section(section_) + , name(name_) + , required(required_) + , multiValued(multiValued_) +{ +} + +Configuration& Configuration::addDefinition(ConfigDefinition_ptr def) +{ + auto& sectionDefinitions = m_definitions[def->section]; + if (sectionDefinitions.find(def->name) != sectionDefinitions.end()) + throw std::invalid_argument(stringify("definition for [", + def->section, "]:", def->name, " already exists")); + + sectionDefinitions[def->name] = std::move(def); + + return *this; +} + +} // namespace llarp + diff --git a/llarp/config/definition.hpp b/llarp/config/definition.hpp new file mode 100644 index 000000000..8dd923f79 --- /dev/null +++ b/llarp/config/definition.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace llarp +{ + + /// non-templated base class for all config definition types. + struct ConfigDefinitionBase + { + ConfigDefinitionBase(std::string section_, + std::string name_, + bool required_, + bool multiValued_); + + virtual ~ConfigDefinitionBase() {} + + /// subclasses should provide their default value as a string + virtual std::string defaultValueAsString() = 0; + + /// subclasses should parse and store the provided input + virtual void parseValue(const std::string& input) = 0; + + /// subclasess should write their parsed value (not default value) as a string + virtual std::string writeValue(bool useDefault) = 0; + + std::string section; + std::string name; + bool required = false; + bool multiValued = false; + size_t numFound = 0; + }; + + template + struct ConfigDefinition : public ConfigDefinitionBase + { + ConfigDefinition(std::string section_, + std::string name_, + bool required_, + bool multiValued_, + nonstd::optional defaultValue_) + : ConfigDefinitionBase(section_, name_, required_, multiValued_) + , defaultValue(defaultValue_) + { + } + + nonstd::optional getValue() const + { + if (parsedValue) + return parsedValue.value(); + else if (not required) + return defaultValue.value(); + else + return {}; + } + + std::string defaultValueAsString() + { + if (defaultValue) + return std::to_string(defaultValue.value()); + else + return ""; + } + + void parseValue(const std::string& input) + { + if (not multiValued and parsedValue) + { + throw std::invalid_argument(stringify("duplicate value for ", name, + ", previous value: ", parsedValue.value())); + } + + std::istringstream iss(input); + T t; + iss >> t; + if (iss.fail()) + throw std::invalid_argument(stringify(input, " is not a valid" , typeid(T).name())); + else + parsedValue = t; + } + + std::string writeValue(bool useDefault) + { + if (parsedValue) + return std::to_string(parsedValue.value()); + else if (useDefault and defaultValue.has_value()) + return std::to_string(defaultValue.value()); + else + return {}; + } + + nonstd::optional defaultValue; + nonstd::optional parsedValue; // needs to be set when parseValue() called + }; + + + using ConfigDefinition_ptr = std::unique_ptr; + + /// A configuration holds an ordered set of ConfigDefinitions defining the allowable values and + /// their constraints and an optional set defining overrides of those values (e.g. the results + /// of a parsed config file). + struct Configuration { + // the first std::string template parameter is the section + std::unordered_map> m_definitions; + + Configuration& addDefinition(ConfigDefinition_ptr def); + + Configuration& addConfigValue(string_view section, + string_view name, + string_view value) + { + ConfigDefinition_ptr& definition = lookupDefinitionOrThrow(section, name); + definition->parseValue(std::string(value)); + + return *this; + } + + template + nonstd::optional getConfigValue(string_view section, string_view name) + { + ConfigDefinition_ptr& definition = lookupDefinitionOrThrow(section, name); + + auto derived = dynamic_cast*>(definition.get()); + if (not derived) + throw std::invalid_argument(stringify("", typeid(T).name(), + " is the incorrect type for [", section, "]:", name)); + + return derived->getValue(); + } + + /// Validate the config, presumably called after parsing. This will throw an exception if the + /// parsed values do not meet the provided definition. + /// + /// Note that this will only handle a subset of errors that may occur. Specifically, this will + /// handle errors about missing required fields, whereas errors about incorrect type, + /// duplicates, etc. are handled during parsing. + /// + /// @throws std::invalid_argument if configuration constraints are not met + void validate(); + + std::string + generateDefaultConfig(); + + std::string + generateOverridenConfig(); + + private: + + const ConfigDefinition_ptr& lookupDefinitionOrThrow(string_view section, string_view name) const + { + const auto sectionItr = m_definitions.find(std::string(section)); + if (sectionItr == m_definitions.end()) + throw std::invalid_argument(stringify("No config section ", section)); + + auto& sectionDefinitions = sectionItr->second; + const auto definitionItr = sectionDefinitions.find(std::string(name)); + if (definitionItr == sectionDefinitions.end()) + throw std::invalid_argument(stringify("No config item ", name, " within section ", section)); + + return definitionItr->second; + } + + ConfigDefinition_ptr& lookupDefinitionOrThrow(string_view section, string_view name) + { + return const_cast( + const_cast(this)->lookupDefinitionOrThrow(section, name)); + } + }; + +} // namespace llarp + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0a6db0109..d788fd16d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -81,6 +81,7 @@ add_executable(${CATCH_EXE} util/test_llarp_util_printer.cpp util/test_llarp_util_str.cpp util/test_llarp_util_decaying_hashset.cpp + config/test_llarp_config_definition.cpp check_main.cpp) target_link_libraries(${CATCH_EXE} PUBLIC ${STATIC_LIB} Catch2::Catch2)