From d75b9c3f27e0afe1690d7e8a161abb70a58b4fab Mon Sep 17 00:00:00 2001 From: "matthias@arch" Date: Mon, 26 Sep 2022 20:32:45 +0200 Subject: [PATCH] added SettingsManager --- src/settings_manager.hpp | 702 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 src/settings_manager.hpp diff --git a/src/settings_manager.hpp b/src/settings_manager.hpp new file mode 100644 index 0000000..63e55bb --- /dev/null +++ b/src/settings_manager.hpp @@ -0,0 +1,702 @@ +#pragma once + +#include "file_io.hpp" +#include "exceptions.hpp" +#include "util/string_conversion.hpp" +#include "util/string.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gz { + + enum SettingsManagerAllowedValueTypes { + SM_RANGE, SM_LIST, + }; + + /** + * @brief Information about the allowed values + * @details + * If type is + * - SM_RANGE -> allowedValues must be an integer [ low, high, step ], the allowed value can then be in [ low, high ) but must be low + n*step + * - SM_LIST -> allowedValues is a list strings, which are the allowed values + */ + struct SettingsManagerAllowedValues { + SettingsManagerAllowedValueTypes type; + std::vector> allowedValues; + }; + + /** + * @brief Creation info for SettingsManager + */ + struct SettingsManagerCreateInfo { + /** + * @brief (Optional) Path to the file containing the settings + */ + std::string filepath; + /** + * @brief (Optional) Map containing default values. + * @details + * Values loaded from a file will overwrite these. + */ + util::unordered_string_map initialValues; + /** + * @brief Wether to insert a fallback, when no value is present + * @details + * If true, fallback value for key will be added if key does not have a value. + * This applies when calling SettingsManager::getOr or SettingsManager::getCopyOr + */ + bool insertFallbacks = false; + /** + * @brief Wether to load values from filepath when creating the SettingsManager + */ + bool readFileOnCreation = false; + /** + * @brief Wether to write the values to filepath when destroying the SettingsManager + */ + bool writeFileOnExit = false; + /** + * @brief A map containing SettingsMangerAllowedValues to restrict values + * @details + * If allowedValues contains key, its value must be allowed by SettingsMangerAllowedValues struct. + * If it is not allowed, any operation that tries to set an invalid value will throw InvalidArgument or be ignored: + * - in constructor: always ignored + * - in SettingsManager::readFileOnCreation: always ignored + * - in SettingsManager::setAllowedValues: always ignored + * - in SettingsManager::set: depends on throwExceptionWhenNewValueNotAllowed + * @see sm_validity + */ + util::unordered_string_map allowedValues; + /** + * @brief Wether to throw an exception when trying to set an invalid value + */ + bool throwExceptionWhenNewValueNotAllowed = true; + }; + + using SettingsCallbackFunction = std::function; + + /** + * @brief + * @details + * @section sm_about About + * The SettingsManager is basically a map with extra features. + * It stores key-value pairs in a map. Both key and value are strings, but other types can also be stored. + * + * @section sm_validity Restricting values + * SettingsManager can restrict the values for a given keys to a range of numbers, or a list of possible strings. + * This can be done by providing @ref SettingsMangerAllowedValues::allowedValues "a map containing information on the allowed values" in the create info. + * The settings manager will then never return an invalid value, but it might still throw errors if there is no value set. + * + * @section sm_cache Storing other types + * While all values are internally strings, SettingsManager can also store different types. + * The types must satisfy the concept StringConvertible = ConstructibleFromString and ConvertibleToString. + * + * If a value is requested using get(), the value will be converted to T and returned. + * Additionaly, the converted type is cached so that the conversion can be skipped if it is requested again. + * The user must instantiate the SettingsManager with all the cache-types beforehand, only those can be retrieved using get. + * + * The strings are converted using from_string(T), which is either user defined for custom types or @ref from_string "this library". + * There is of course no guarantee that the conversion works, the @ref sm_getters "getters" will throw an exception if it doesnt. + * + * @section sm_callback Callback functions + * You can @ref SettingsManager::addCallbackFunction "add a callback function" for each key that gets called when a setting gets changed. + */ + template + class SettingsManager { + public: + /** + * @brief Create a SettingsManager + * @details + * The maps from createInfo are no longer valid after this function. + */ + SettingsManager(SettingsManagerCreateInfo& createInfo); + ~SettingsManager(); + SettingsManager(const SettingsManager&) = delete; + SettingsManager& operator=(const SettingsManager&) = delete; + + /** + * @name Retrieve values + * @{ + */ + /** + * @brief Get a reference to a value + * @param key The key to the value + * @returns The value belonging to key + * @throws InvalidArgument if key is not present + */ + const std::string& get(const std::string& key) const; + /** + * @brief Get a reference to the value coverted to T + * @param key The key to the setting + * @returns The setting belonging to key, constructed by getTypeFromString() + * @throws InvalidArgument if key is not present + * @throws InvalidType if T is not a registered type + * @throws InvalidType if type T can not be constructed from value (string) + */ + template + const T& get(const std::string& key); + + /** + * @brief Get a reference to the value, or return the fallback if the setting is not present + * @param key The key to the value + * @param fallback A fallback to return if the key is not present + * @returns The value belonging to key, constructed by getTypeFromString() + * @throws InvalidArgument if key is not present + */ + const std::string& getOr(const std::string& key, const std::string& fallback); + + /** + * @brief Get a reference to the value coverted to T, or return the fallback if the setting is not present + * @param key The key to the value + * @param fallback A fallback to return if the key is not present + * @returns The value belonging to key, constructed by getTypeFromString() + * @throws InvalidArgument if key is not present + * @throws InvalidType if T is not a registered type + * @throws InvalidType if type T can not be constructed from value (string) + * @throws InvalidType if the fallback can not be converted to string + */ + template + const T& getOr(const std::string& key, const T& fallback); + + /** + * @brief Same as get, but returns a copy of value and not a const reference + */ + const std::string getCopy(const std::string& key) const; + + /** + * @brief Same as get, but returns a copy of value and not a const reference + */ + template + const T getCopy(const std::string& key); + + /** + * @brief Same as getOr, but returns a copy of value and not a const reference + */ + const std::string getCopyOr(const std::string& key, const std::string& fallback); + + /** + * @brief Same as getOr, but returns a copy of value and not a const reference + */ + template + const T getCopyOr(const std::string& key, const T& fallback); + + /** + * @} + */ + + + /** + * @name Set values + */ + /** + * @brief Set the value of key to value + * @throws InvalidArgument if value is @ref sm_validity "invalid" + * @throws Exception if an exception occurs during a potential @red sm_callback "callback function" + */ + void set(const std::string& key, const std::string& value); + + /** + * @brief Set the value of key to value + * @throws InvalidArgument if value is @ref sm_validity "invalid" + * @throws InvalidType if T is not a registered type + * @throws Exception if an exception occurs during a potential @red sm_callback "callback function" + */ + template + void set(const std::string& key, const T& value); + /** + * @} + */ + + /** + * @name Callback functions + */ + /** + * @brief Add a callback function that gets called when the the value to key is changed with set + */ + void addCallbackFunction(const std::string& key, SettingsCallbackFunction callbackFunction); + /** + * @brief Remove the callback function for key + */ + void removeCallbackFunction(const std::string& key); + /** + * @} + */ + + /** + * @name File operations + * @{ + */ + void writeToFile() const; + /** + * @brief Read the contents from the provided @ref SettingsManagerCreateInfo::filepath "filepath" + * @param checkValidity if true, inserts only files that are @ref sm_validity "valid" + */ + void readFromFile(bool checkValidity=true); + /** + * @} + */ + std::string to_string() const { + return to_string(settings); + }; + + /** + * @name Restricting values + */ + /** + * @ingroup sm_allowed_values + * @brief Check if a value is allowed for key + */ + bool isValueAllowed(const std::string& key, const std::string& value) const noexcept; + + /** + * @ingroup sm_allowed_values + * @brief Set the allowed values for a key + * @details + * If the current value for key is now invalid, it will be removed. + * values struct is no longer valid after this function + * @throws InvalidArgument if the values struct is invalid + */ + void setAllowedValues(const std::string& key, SettingsManagerAllowedValues& values); + + /** + * @ingroup sm_allowed_values + * @brief Remove allowed values for key. All values are allowed afterwards + */ + void removeAllowedValues(const std::string& key) noexcept; + /** + * @} + */ + + private: + /** + * @ingroup sm_cache + * @{ + */ + /** + * @brief Recursive initialitation of the cache + * @details + * The cache is initialized by placing the the typename in the cacheTypes set. + * It will not actually be added as cache type. + * The "Void" type template parameter is needed because the exit for the recursion must also have a template signature. + * When calling this, use initCache(). + */ + template Void, StringConvertible CacheType1, StringConvertible... CacheTypesOther> + void initCache(); + /** + * @brief End for the recursive cache initialization + * + */ + template Void> + void initCache() {}; + std::set cacheTypes; + util::unordered_string_map>> settingsCache; + template + inline bool isRegisteredType(); + /** + * @} + */ + /** + * @name Restricting values + * @see sm_validity + * @{ + */ + // Allowed Values + /** + * @brief Make sure the allowedValues map is valid + * @throws InvalidArgument if any of the initial allowedValues structs is invalid + */ + void initAllowedValues(); + /** + * @brief Check if allowed values struct is valid. + * @details + * If type is SM_RANGE and allowedValues only has 2 values (lowest and highest value), the third (step) value=1 is appended + * @throws InvalidArgument if it is not valid + */ + void isAllowedValueStructValid(SettingsManagerAllowedValues& av) const; + util::unordered_string_map allowedValues; + bool throwExceptionWhenNewValueNotAllowed; + /** + * @} + */ + + util::unordered_string_map settings; + util::unordered_string_map settingsCallbackFunctions; + + // Settings + /** + * @brief Add fallback to values if key is not present when using getOr + */ + bool insertFallbacks; + bool writeFileOnExit; + std::string filepath; + + }; +} + +namespace gz { + + template + SettingsManager::SettingsManager(SettingsManagerCreateInfo& createInfo) { + insertFallbacks = createInfo.insertFallbacks; + writeFileOnExit = createInfo.writeFileOnExit; + throwExceptionWhenNewValueNotAllowed = createInfo.throwExceptionWhenNewValueNotAllowed; + + allowedValues = std::move(createInfo.allowedValues); + settings = std::move(createInfo.initialValues); + + filepath = createInfo.filepath; + if (createInfo.readFileOnCreation) { + readFromFile(false); + } + + // erase invalid initalValues + for (auto it = settings.begin(); it != settings.end(); it++) { + if (!isValueAllowed(it->first, it->second)) { + it = settings.erase(it); + } + } + + initCache(); + + } + + template + SettingsManager::~SettingsManager() { + if (writeFileOnExit) { + writeToFile(); + } + } + + template + template Void, StringConvertible CacheType1, StringConvertible... CacheTypesOther> + void SettingsManager::initCache() { + cacheTypes.insert(typeid(CacheType1).name()); + initCache(); + std::cout << "initCache: type " << typeid(CacheType1).name() << "\n"; + } + + template + template + inline bool SettingsManager::isRegisteredType() { + return cacheTypes.contains(typeid(T).name()); + } + +// +// GET +// + // REFERENCE + template + const std::string& SettingsManager::get(const std::string& key) const { + if (!settings.contains(key)) { + throw InvalidArgument("Invalid key: " + key + "'", "SettingsManager::get"); + } + return settings.at(key); + } + + template + template + const T& SettingsManager::get(const std::string& key) { + if (!isRegisteredType()) { + throw InvalidType("Invalid type: '" + std::string(typeid(T).name()) + "'", "SettingsManager::get"); + } + if (!settings.contains(key)) { + throw InvalidArgument("Invalid key: '" + key + "'", "SettingsManager::get"); + } + // if not cached -> cache + if (!settingsCache[typeid(T).name()].contains(key)) { + try { + settingsCache[typeid(T).name()][key] = from_string(settings[key]); + } + catch (...) { + throw InvalidType("Could not convert value '" + settings[key] + "' to type '" + typeid(T).name() + "'. Key: '" + key + "'", "SettingsManager::get"); + } + } + return std::get(settingsCache[typeid(T).name()][key]); + } + + + // REFERENCE OR FALLBACK + template + const std::string& SettingsManager::getOr(const std::string& key, const std::string& fallback) { + if (settings.contains(key)) { + return get(key); + } + else { + if (insertFallbacks) { + settings[key] = fallback; + } + return fallback; + } + } + + template + template + const T& SettingsManager::getOr(const std::string& key, const T& fallback) { + if (!isRegisteredType()) { + throw InvalidType("Invalid type: '" + std::string(typeid(T).name()) + "'", "SettingsManager::getOr"); + } + if (settings.contains(key)) { + return get(key); + } + else { + if (insertFallbacks) { + try { + settings[key] = to_string(fallback); + } + catch (...) { + throw InvalidType("Can not convert fallback value to string. Key: '" + key + "'", "SettingsManager::getOr"); + } + settingsCache[typeid(T).name()][key] = fallback; + } + return fallback; + } + } + + // COPY + template + const std::string SettingsManager::getCopy(const std::string& key) const { + return get(key); + } + template + template + const T SettingsManager::getCopy(const std::string& key) { + return get(key); + } + // COPY OR FALLBACK + template + const std::string SettingsManager::getCopyOr(const std::string& key, const std::string& fallback) { + return getOr(key, fallback); + } + template + template + const T SettingsManager::getCopyOr(const std::string& key, const T& fallback) { + return getOr(key, fallback); + } + +// +// SET +// + template + void SettingsManager::set(const std::string& key, const std::string& value) { + // check if new value is allowed + if (!isValueAllowed(key, value)) { + if (throwExceptionWhenNewValueNotAllowed) { + throw InvalidArgument("Value '" + value + "' is not allowed. Key: '" + key + "'", "SettingsManager::set"); + } + else { + return; + } + } + // remove old value from cache + for (auto& [type, cache] : settingsCache) { + if (cache.contains(key)) { + cache.erase(key); + } + } + settings[key] = value; + // Call the callback, if any + if (settingsCallbackFunctions.contains(key)) { + try { + settingsCallbackFunctions[key](value); + } + catch (std::exception& e) { + throw Exception("An exception occured in the callback for changing a value: '" + std::string(e.what()) + "'. Key: '" + key + "'", "SettingsManager::set"); + } + catch (...) { + throw Exception("An exception occured in the callback for changing a value. Key: '" + key + "'", "SettingsManager::set"); + } + } + } + + template + template + void SettingsManager::set(const std::string& key, const T& value) { + if (!isRegisteredType()) { + throw InvalidType("Invalid type: '" + std::string(typeid(T).name()) + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + // convert to string + std::string s; + try { + s = to_string(value); + } + catch (std::exception& e) { + throw InvalidArgument("Could not convert value to string, an exception occured: '" + std::string(e.what()) + "'. Key: '" + key + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + catch (...) { + throw InvalidArgument("Could not convert value to string, an exception occured. Key: '" + key + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + // check if new value is allowed + if (!isValueAllowed(key, s)) { + if (throwExceptionWhenNewValueNotAllowed) { + throw InvalidArgument("Value '" + s + "' is not allowed. Key: '" + key + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + else { + return; + } + } + // remove old value from cache + for (auto& [type, cache] : settingsCache) { + if (cache.contains(key)) { + cache.erase(key); + } + } + // add new value + settings[key] = std::move(s); + settingsCache[typeid(T).name()][key] = value; + // Call the callback, if any + if (settingsCallbackFunctions.contains(key)) { + try { + settingsCallbackFunctions[key](value); + } + catch (std::exception& e) { + throw Exception("An exception occured in the callback for changing a value: '" + std::string(e.what()) + "'. Key: '" + key + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + catch (...) { + throw Exception("An exception occured in the callback for changing a value. Key: '" + key + "'", "SettingsManager::set<" + std::string(typeid(T).name()) + ">"); + } + } + } + +// +// ALLOWED VALUES +// + template + bool SettingsManager::isValueAllowed(const std::string& key, const std::string& value) const noexcept { + if (!allowedValues.contains(key)) { + return true; + } + switch (allowedValues.at(key).type) { + case SM_LIST: + for (auto it = allowedValues.at(key).allowedValues.begin(); it != allowedValues.at(key).allowedValues.end(); it++) { + if (std::get(*it) == value) { + return true; + } + } + break; + case SM_RANGE: + int intVal; + try { + intVal = std::stoi(value); + } + catch (std::invalid_argument) { + return false; + } + catch (std::out_of_range) { + return false; + } + bool valid = true; + // intVal >= lowest + valid &= intVal >= std::get(allowedValues.at(key).allowedValues[0]); + // intVal < highest + valid &= intVal < std::get(allowedValues.at(key).allowedValues[1]); + // intVal == lowest + n * step + valid &= (intVal - std::get(allowedValues.at(key).allowedValues[0])) % std::get(allowedValues.at(key).allowedValues[2]) == 0; + return valid; + break; + } // switch + } + + template + void SettingsManager::initAllowedValues() { + for (auto& [key, av] : allowedValues) { + isAllowedValueStructValid(av); + } // for + } + + template + void SettingsManager::isAllowedValueStructValid(SettingsManagerAllowedValues& av) const { + switch (av.type) { + case SM_LIST: + if (av.allowedValues.empty()) { + throw InvalidArgument("Allowed value vector needs to have at least one element, but is empty.", "SettingsManager::isAllowedValueStructValid"); + } + for (size_t i = 0; i < av.allowedValues.size(); i++) { + try { + std::get(av.allowedValues[i]); + } + catch (std::bad_variant_access& e) { + throw InvalidType("AllowedValueType is SM_LIST but allowedValues[" + std::to_string(i) + "] is not of type .", "SettingsManager::isAllowedValueStructValid"); + } + } + break; + case SM_RANGE: + if (av.allowedValues.size() == 2) { + av.allowedValues.push_back(1); + } + else if (av.allowedValues.size() != 3) { + throw InvalidArgument("AllowedValueType is SM_RANGE but allowedValues does not have size 2 or 3.", "SettingsManager::isAllowedValueStructValid"); + } + try { + std::get(av.allowedValues[0]); + std::get(av.allowedValues[1]); + std::get(av.allowedValues[2]); + } + catch (std::bad_variant_access& e) { + throw InvalidType("AllowedValueType is SM_RANGE but at least one value in allowedValues is not of type .", "SettingsManager::isAllowedValueStructValid"); + } + break; + } // switch + } + + + template + void SettingsManager::setAllowedValues(const std::string& key, SettingsManagerAllowedValues& av) { + isAllowedValueStructValid(av); + allowedValues[key] = std::move(av); + } + + template + void SettingsManager::removeAllowedValues(const std::string& key) noexcept { + allowedValues.erase(key); + } +// +// CALLBACKS +// + template + void SettingsManager::addCallbackFunction(const std::string& key, SettingsCallbackFunction callbackFunction) { + settingsCallbackFunctions[key] = callbackFunction; + } + template + void SettingsManager::removeCallbackFunction(const std::string& key) { + settingsCallbackFunctions.erase(key); + } + +// +// FILE IO +// + template + void SettingsManager::writeToFile() const { + if (filepath.empty()) { + throw InvalidArgument("filename is not set", "writeToFile"); + } + writeKeyValueFile(filepath, settings); + } + + template + void SettingsManager::readFromFile(bool checkValidity) { + if (filepath.empty()) { + throw InvalidArgument("filename is not set", "readFromFile"); + } + std::unordered_map map = readKeyValueFile(filepath); + settings.insert(map.begin(), map.end()); + if (checkValidity) { + // insert only valid values + for (auto it = map.begin(); it != map.end(); it++) { + if (isValueAllowed(it->first, it->second)) { + settings.insert(*it); + } + } + } + else { + settings.insert(map.begin(), map.end()); + } + } +} +