diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index dfb14783c..e297fab79 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -25,9 +25,12 @@ target_sources( TimeUtils.cpp TxUtils.cpp LedgerUtils.cpp + newconfig/Array.cpp + newconfig/ArrayView.cpp + newconfig/ConfigConstraints.cpp newconfig/ConfigDefinition.cpp + newconfig/ConfigFileJson.cpp newconfig/ObjectView.cpp - newconfig/ArrayView.cpp newconfig/ValueView.cpp ) diff --git a/src/util/newconfig/Array.cpp b/src/util/newconfig/Array.cpp new file mode 100644 index 000000000..13d9b8361 --- /dev/null +++ b/src/util/newconfig/Array.cpp @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/newconfig/Array.hpp" + +#include "util/Assert.hpp" +#include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include +#include +#include +#include +#include + +namespace util::config { + +Array::Array(ConfigValue arg) : itemPattern_{std::move(arg)} +{ +} + +std::optional +Array::addValue(Value value, std::optional key) +{ + auto const& configValPattern = itemPattern_; + auto const constraint = configValPattern.getConstraint(); + + auto newElem = constraint.has_value() ? ConfigValue{configValPattern.type()}.withConstraint(constraint->get()) + : ConfigValue{configValPattern.type()}; + if (auto const maybeError = newElem.setValue(value, key); maybeError.has_value()) + return maybeError; + elements_.emplace_back(std::move(newElem)); + return std::nullopt; +} + +size_t +Array::size() const +{ + return elements_.size(); +} + +ConfigValue const& +Array::at(std::size_t idx) const +{ + ASSERT(idx < elements_.size(), "Index is out of scope"); + return elements_[idx]; +} + +ConfigValue const& +Array::getArrayPattern() const +{ + return itemPattern_; +} + +std::vector::const_iterator +Array::begin() const +{ + return elements_.begin(); +} + +std::vector::const_iterator +Array::end() const +{ + return elements_.end(); +} + +} // namespace util::config diff --git a/src/util/newconfig/Array.hpp b/src/util/newconfig/Array.hpp index f83bc454c..a6ccbe958 100644 --- a/src/util/newconfig/Array.hpp +++ b/src/util/newconfig/Array.hpp @@ -19,47 +19,42 @@ #pragma once -#include "util/Assert.hpp" #include "util/newconfig/ConfigValue.hpp" -#include "util/newconfig/ObjectView.hpp" -#include "util/newconfig/ValueView.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" #include -#include -#include -#include +#include +#include #include namespace util::config { /** - * @brief Array definition for Json/Yaml config + * @brief Array definition to store multiple values provided by the user from Json/Yaml * * Used in ClioConfigDefinition to represent multiple potential values (like whitelist) + * Is constructed with only 1 element which states which type/constraint must every element + * In the array satisfy */ class Array { public: /** - * @brief Constructs an Array with the provided arguments + * @brief Constructs an Array with provided Arg * - * @tparam Args Types of the arguments - * @param args Arguments to initialize the elements of the Array + * @param arg Argument to set the type and constraint of ConfigValues in Array */ - template - constexpr Array(Args&&... args) : elements_{std::forward(args)...} - { - } + Array(ConfigValue arg); /** * @brief Add ConfigValues to Array class * * @param value The ConfigValue to add + * @param key optional string key to include that will show in error message + * @return optional error if adding config value to array fails. nullopt otherwise */ - void - emplaceBack(ConfigValue value) - { - elements_.push_back(std::move(value)); - } + std::optional + addValue(Value value, std::optional key = std::nullopt); /** * @brief Returns the number of values stored in the Array @@ -67,10 +62,7 @@ class Array { * @return Number of values stored in the Array */ [[nodiscard]] size_t - size() const - { - return elements_.size(); - } + size() const; /** * @brief Returns the ConfigValue at the specified index @@ -79,13 +71,35 @@ class Array { * @return ConfigValue at the specified index */ [[nodiscard]] ConfigValue const& - at(std::size_t idx) const - { - ASSERT(idx < elements_.size(), "index is out of scope"); - return elements_[idx]; - } + at(std::size_t idx) const; + + /** + * @brief Returns the ConfigValue that defines the type/constraint every + * ConfigValue must follow in Array + * + * @return The item_pattern + */ + [[nodiscard]] ConfigValue const& + getArrayPattern() const; + + /** + * @brief Returns an iterator to the beginning of the ConfigValue vector. + * + * @return A constant iterator to the beginning of the vector. + */ + [[nodiscard]] std::vector::const_iterator + begin() const; + + /** + * @brief Returns an iterator to the end of the ConfigValue vector. + * + * @return A constant iterator to the end of the vector. + */ + [[nodiscard]] std::vector::const_iterator + end() const; private: + ConfigValue itemPattern_; std::vector elements_; }; diff --git a/src/util/newconfig/ConfigConstraints.cpp b/src/util/newconfig/ConfigConstraints.cpp new file mode 100644 index 000000000..893cc2755 --- /dev/null +++ b/src/util/newconfig/ConfigConstraints.cpp @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/newconfig/ConfigConstraints.hpp" + +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace util::config { + +std::optional +PortConstraint::checkTypeImpl(Value const& port) const +{ + if (!(std::holds_alternative(port) || std::holds_alternative(port))) + return Error{"Port must be a string or integer"}; + return std::nullopt; +} + +std::optional +PortConstraint::checkValueImpl(Value const& port) const +{ + uint32_t p = 0; + if (std::holds_alternative(port)) { + try { + p = static_cast(std::stoi(std::get(port))); + } catch (std::invalid_argument const& e) { + return Error{"Port string must be an integer."}; + } + } else { + p = static_cast(std::get(port)); + } + if (p >= portMin && p <= portMax) + return std::nullopt; + return Error{"Port does not satisfy the constraint bounds"}; +} + +std::optional +ValidIPConstraint::checkTypeImpl(Value const& ip) const +{ + if (!std::holds_alternative(ip)) + return Error{"Ip value must be a string"}; + return std::nullopt; +} + +std::optional +ValidIPConstraint::checkValueImpl(Value const& ip) const +{ + if (std::get(ip) == "localhost") + return std::nullopt; + + static std::regex const ipv4( + R"(^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$)" + ); + + static std::regex const ip_url( + R"(^((http|https):\/\/)?((([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6})|(((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])))(:\d{1,5})?(\/[^\s]*)?$)" + ); + if (std::regex_match(std::get(ip), ipv4) || std::regex_match(std::get(ip), ip_url)) + return std::nullopt; + + return Error{"Ip is not a valid ip address"}; +} + +std::optional +PositiveDouble::checkTypeImpl(Value const& num) const +{ + if (!(std::holds_alternative(num) || std::holds_alternative(num))) + return Error{"Double number must be of type int or double"}; + return std::nullopt; +} + +std::optional +PositiveDouble::checkValueImpl(Value const& num) const +{ + if (std::get(num) >= 0) + return std::nullopt; + return Error{"Double number must be greater than 0"}; +} + +} // namespace util::config diff --git a/src/util/newconfig/ConfigConstraints.hpp b/src/util/newconfig/ConfigConstraints.hpp new file mode 100644 index 000000000..7fd6b60be --- /dev/null +++ b/src/util/newconfig/ConfigConstraints.hpp @@ -0,0 +1,376 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "rpc/common/APIVersion.hpp" +#include "util/log/Logger.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace util::config { +class ValueView; +class ConfigValue; + +/** + * @brief specific values that are accepted for logger levels in config. + */ +static constexpr std::array LOG_LEVELS = { + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "count", +}; + +/** + * @brief specific values that are accepted for logger tag style in config. + */ +static constexpr std::array LOG_TAGS = { + "int", + "uint", + "null", + "none", + "uuid", +}; + +/** + * @brief specific values that are accepted for cache loading in config. + */ +static constexpr std::array LOAD_CACHE_MODE = { + "sync", + "async", + "none", +}; + +/** + * @brief specific values that are accepted for database type in config. + */ +static constexpr std::array DATABASE_TYPE = {"cassandra"}; + +/** + * @brief An interface to enforce constraints on certain values within ClioConfigDefinition. + */ +class Constraint { +public: + // using "{}" instead of = default because of gcc bug. Bug is fixed in gcc13 + // see here for more info: + // https://stackoverflow.com/questions/72835571/constexpr-c-error-destructor-used-before-its-definition + // https://godbolt.org/z/eMdWThaMY + constexpr virtual ~Constraint() noexcept {}; + + /** + * @brief Check if the value meets the specific constraint. + * + * @param val The value to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] + std::optional + checkConstraint(Value const& val) const + { + if (auto const maybeError = checkTypeImpl(val); maybeError.has_value()) + return maybeError; + return checkValueImpl(val); + } + +protected: + /** + * @brief Creates an error message for all constraints that must satisfy certain hard-coded values. + * + * @tparam arrSize, the size of the array of hardcoded values + * @param key The key to the value + * @param value The value the user provided + * @param arr The array with hard-coded values to add to error message + * @return The error message specifying what the value of key must be + */ + template + constexpr std::string + makeErrorMsg(std::string_view key, Value const& value, std::array arr) const + { + // Extract the value from the variant + auto const valueStr = std::visit([](auto const& v) { return fmt::format("{}", v); }, value); + + // Create the error message + return fmt::format( + R"(You provided value "{}". Key "{}"'s value must be one of the following: {})", + valueStr, + key, + fmt::join(arr, ", ") + ); + } + + /** + * @brief Check if the value is of a correct type for the constraint. + * + * @param val The value type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + virtual std::optional + checkTypeImpl(Value const& val) const = 0; + + /** + * @brief Check if the value is within the constraint. + * + * @param val The value type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + virtual std::optional + checkValueImpl(Value const& val) const = 0; +}; + +/** + * @brief A constraint to ensure the port number is within a valid range. + */ +class PortConstraint final : public Constraint { +public: + constexpr ~PortConstraint() + { + } + +private: + /** + * @brief Check if the type of the value is correct for this specific constraint. + * + * @param port The type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkTypeImpl(Value const& port) const override; + + /** + * @brief Check if the value is within the constraint. + * + * @param port The value to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkValueImpl(Value const& port) const override; + + static constexpr uint32_t portMin = 1; + static constexpr uint32_t portMax = 65535; +}; + +/** + * @brief A constraint to ensure the IP address is valid. + */ +class ValidIPConstraint final : public Constraint { +public: + constexpr ~ValidIPConstraint() + { + } + +private: + /** + * @brief Check if the type of the value is correct for this specific constraint. + * + * @param ip The type to be checked. + * @return An optional Error if the constraint is not met, std::nullopt otherwise + */ + [[nodiscard]] std::optional + checkTypeImpl(Value const& ip) const override; + + /** + * @brief Check if the value is within the constraint. + * + * @param ip The value to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkValueImpl(Value const& ip) const override; +}; + +/** + * @brief A constraint class to ensure the provided value is one of the specified values in an array. + * + * @tparam arrSize The size of the array containing the valid values for the constraint + */ +template +class OneOf final : public Constraint { +public: + /** + * @brief Constructs a constraint where the value must be one of the values in the provided array. + * + * @param key The key of the ConfigValue that has this constraint + * @param arr The value that has this constraint must be of the values in arr + */ + constexpr OneOf(std::string_view key, std::array arr) : key_{key}, arr_{arr} + { + } + + constexpr ~OneOf() + { + } + +private: + /** + * @brief Check if the type of the value is correct for this specific constraint. + * + * @param val The type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkTypeImpl(Value const& val) const override + { + if (!std::holds_alternative(val)) + return Error{fmt::format(R"(Key "{}"'s value must be a string)", key_)}; + return std::nullopt; + } + + /** + * @brief Check if the value matches one of the value in the provided array + * + * @param val The value to check + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkValueImpl(Value const& val) const override + { + namespace rg = std::ranges; + auto const check = [&val](std::string_view name) { return std::get(val) == name; }; + if (rg::any_of(arr_, check)) + return std::nullopt; + + return Error{makeErrorMsg(key_, val, arr_)}; + } + + std::string_view key_; + std::array arr_; +}; + +/** + * @brief A constraint class to ensure an integer value is between two numbers (inclusive) + */ +template +class NumberValueConstraint final : public Constraint { +public: + /** + * @brief Constructs a constraint where the number must be between min_ and max_. + * + * @param min the minimum number it can be to satisfy this constraint + * @param max the maximum number it can be to satisfy this constraint + */ + constexpr NumberValueConstraint(numType min, numType max) : min_{min}, max_{max} + { + } + + constexpr ~NumberValueConstraint() + { + } + +private: + /** + * @brief Check if the type of the value is correct for this specific constraint. + * + * @param num The type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkTypeImpl(Value const& num) const override + { + if (!std::holds_alternative(num)) + return Error{"Number must be of type integer"}; + return std::nullopt; + } + + /** + * @brief Check if the number is positive. + * + * @param num The number to check + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkValueImpl(Value const& num) const override + { + auto const numValue = std::get(num); + if (numValue >= static_cast(min_) && numValue <= static_cast(max_)) + return std::nullopt; + return Error{fmt::format("Number must be between {} and {}", min_, max_)}; + } + + numType min_; + numType max_; +}; + +/** + * @brief A constraint to ensure a double number is positive + */ +class PositiveDouble final : public Constraint { +public: + constexpr ~PositiveDouble() + { + } + +private: + /** + * @brief Check if the type of the value is correct for this specific constraint. + * + * @param num The type to be checked + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkTypeImpl(Value const& num) const override; + + /** + * @brief Check if the number is positive. + * + * @param num The number to check + * @return An Error object if the constraint is not met, nullopt otherwise + */ + [[nodiscard]] std::optional + checkValueImpl(Value const& num) const override; +}; + +static constexpr PortConstraint validatePort{}; +static constexpr ValidIPConstraint validateIP{}; + +static constexpr OneOf validateChannelName{"channel", Logger::CHANNELS}; +static constexpr OneOf validateLogLevelName{"log_level", LOG_LEVELS}; +static constexpr OneOf validateCassandraName{"database.type", DATABASE_TYPE}; +static constexpr OneOf validateLoadMode{"cache.load", LOAD_CACHE_MODE}; +static constexpr OneOf validateLogTag{"log_tag_style", LOG_TAGS}; + +static constexpr PositiveDouble validatePositiveDouble{}; + +static constexpr NumberValueConstraint validateUint16{ + std::numeric_limits::min(), + std::numeric_limits::max() +}; +static constexpr NumberValueConstraint validateUint32{ + std::numeric_limits::min(), + std::numeric_limits::max() +}; +static constexpr NumberValueConstraint validateApiVersion{rpc::API_VERSION_MIN, rpc::API_VERSION_MAX}; + +} // namespace util::config diff --git a/src/util/newconfig/ConfigDefinition.cpp b/src/util/newconfig/ConfigDefinition.cpp index 3d22b9ddf..330c0f68d 100644 --- a/src/util/newconfig/ConfigDefinition.cpp +++ b/src/util/newconfig/ConfigDefinition.cpp @@ -20,10 +20,15 @@ #include "util/newconfig/ConfigDefinition.hpp" #include "util/Assert.hpp" +#include "util/OverloadSet.hpp" #include "util/newconfig/Array.hpp" #include "util/newconfig/ArrayView.hpp" +#include "util/newconfig/ConfigConstraints.hpp" +#include "util/newconfig/ConfigFileInterface.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Error.hpp" #include "util/newconfig/ObjectView.hpp" +#include "util/newconfig/Types.hpp" #include "util/newconfig/ValueView.hpp" #include @@ -38,6 +43,7 @@ #include #include #include +#include namespace util::config { /** @@ -47,62 +53,76 @@ namespace util::config { * without default values must be present in the user's config file. */ static ClioConfigDefinition ClioConfig = ClioConfigDefinition{ - {{"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra")}, + {{"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra").withConstraint(validateCassandraName)}, {"database.cassandra.contact_points", ConfigValue{ConfigType::String}.defaultValue("localhost")}, - {"database.cassandra.port", ConfigValue{ConfigType::Integer}}, + {"database.cassandra.port", ConfigValue{ConfigType::Integer}.withConstraint(validatePort)}, {"database.cassandra.keyspace", ConfigValue{ConfigType::String}.defaultValue("clio")}, {"database.cassandra.replication_factor", ConfigValue{ConfigType::Integer}.defaultValue(3u)}, {"database.cassandra.table_prefix", ConfigValue{ConfigType::String}.defaultValue("table_prefix")}, - {"database.cassandra.max_write_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(10'000)}, - {"database.cassandra.max_read_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(100'000)}, + {"database.cassandra.max_write_requests_outstanding", + ConfigValue{ConfigType::Integer}.defaultValue(10'000).withConstraint(validateUint32)}, + {"database.cassandra.max_read_requests_outstanding", + ConfigValue{ConfigType::Integer}.defaultValue(100'000).withConstraint(validateUint32)}, {"database.cassandra.threads", - ConfigValue{ConfigType::Integer}.defaultValue(static_cast(std::thread::hardware_concurrency()))}, - {"database.cassandra.core_connections_per_host", ConfigValue{ConfigType::Integer}.defaultValue(1)}, - {"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional()}, - {"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)}, - {"etl_source.[].ip", Array{ConfigValue{ConfigType::String}.optional()}}, - {"etl_source.[].ws_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}}, - {"etl_source.[].grpc_port", Array{ConfigValue{ConfigType::String}.optional().min(1).max(65535)}}, - {"forwarding.cache_timeout", ConfigValue{ConfigType::Double}.defaultValue(0.0)}, - {"forwarding.request_timeout", ConfigValue{ConfigType::Double}.defaultValue(10.0)}, + ConfigValue{ConfigType::Integer} + .defaultValue(static_cast(std::thread::hardware_concurrency())) + .withConstraint(validateUint32)}, + {"database.cassandra.core_connections_per_host", + ConfigValue{ConfigType::Integer}.defaultValue(1).withConstraint(validateUint16)}, + {"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint16)}, + {"database.cassandra.write_batch_size", + ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint16)}, + {"etl_source.[].ip", Array{ConfigValue{ConfigType::String}.withConstraint(validateIP)}}, + {"etl_source.[].ws_port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}}, + {"etl_source.[].grpc_port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}}, + {"forwarding.cache_timeout", + ConfigValue{ConfigType::Double}.defaultValue(0.0).withConstraint(validatePositiveDouble)}, + {"forwarding.request_timeout", + ConfigValue{ConfigType::Double}.defaultValue(10.0).withConstraint(validatePositiveDouble)}, {"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}}}, - {"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000)}, - {"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20)}, - {"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20)}, - {"dos_guard.sweep_interval", ConfigValue{ConfigType::Double}.defaultValue(1.0)}, - {"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}}}, - {"cache.peers.[].port", Array{ConfigValue{ConfigType::String}}}, - {"server.ip", ConfigValue{ConfigType::String}}, - {"server.port", ConfigValue{ConfigType::Integer}}, - {"server.max_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(0)}, + {"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}.defaultValue(1000'000).withConstraint(validateUint32)}, + {"dos_guard.max_connections", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)}, + {"dos_guard.max_requests", ConfigValue{ConfigType::Integer}.defaultValue(20).withConstraint(validateUint32)}, + {"dos_guard.sweep_interval", + ConfigValue{ConfigType::Double}.defaultValue(1.0).withConstraint(validatePositiveDouble)}, + {"cache.peers.[].ip", Array{ConfigValue{ConfigType::String}.withConstraint(validateIP)}}, + {"cache.peers.[].port", Array{ConfigValue{ConfigType::String}.withConstraint(validatePort)}}, + {"server.ip", ConfigValue{ConfigType::String}.withConstraint(validateIP)}, + {"server.port", ConfigValue{ConfigType::Integer}.withConstraint(validatePort)}, + {"server.workers", ConfigValue{ConfigType::Integer}.withConstraint(validateUint32)}, + {"server.max_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint32)}, {"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()}, + {"server.admin_password", ConfigValue{ConfigType::String}.optional()}, {"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, {"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, - {"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2)}, - {"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32)}, - {"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48)}, - {"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0)}, - {"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0)}, - {"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512)}, - {"cache.load", ConfigValue{ConfigType::String}.defaultValue("async")}, - {"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional()}}, - {"log_channels.[].log_level", Array{ConfigValue{ConfigType::String}.optional()}}, - {"log_level", ConfigValue{ConfigType::String}.defaultValue("info")}, + {"io_threads", ConfigValue{ConfigType::Integer}.defaultValue(2).withConstraint(validateUint16)}, + {"cache.num_diffs", ConfigValue{ConfigType::Integer}.defaultValue(32).withConstraint(validateUint16)}, + {"cache.num_markers", ConfigValue{ConfigType::Integer}.defaultValue(48).withConstraint(validateUint16)}, + {"cache.num_cursors_from_diff", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)}, + {"cache.num_cursors_from_account", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16) + }, + {"cache.page_fetch_size", ConfigValue{ConfigType::Integer}.defaultValue(512).withConstraint(validateUint16)}, + {"cache.load", ConfigValue{ConfigType::String}.defaultValue("async").withConstraint(validateLoadMode)}, + {"log_channels.[].channel", Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateChannelName)}}, + {"log_channels.[].log_level", + Array{ConfigValue{ConfigType::String}.optional().withConstraint(validateLogLevelName)}}, + {"log_level", ConfigValue{ConfigType::String}.defaultValue("info").withConstraint(validateLogLevelName)}, {"log_format", ConfigValue{ConfigType::String}.defaultValue( R"(%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%)" )}, {"log_to_console", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, {"log_directory", ConfigValue{ConfigType::String}.optional()}, - {"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048)}, - {"log_directory_max_size", ConfigValue{ConfigType::Integer}.defaultValue(50 * 1024)}, - {"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12)}, - {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}, - {"extractor_threads", ConfigValue{ConfigType::Integer}.defaultValue(2u)}, + {"log_rotation_size", ConfigValue{ConfigType::Integer}.defaultValue(2048u).withConstraint(validateUint32)}, + {"log_directory_max_size", + ConfigValue{ConfigType::Integer}.defaultValue(50u * 1024u).withConstraint(validateUint32)}, + {"log_rotation_hour_interval", ConfigValue{ConfigType::Integer}.defaultValue(12).withConstraint(validateUint32)}, + {"log_tag_style", ConfigValue{ConfigType::String}.defaultValue("uint").withConstraint(validateLogTag)}, + {"extractor_threads", ConfigValue{ConfigType::Integer}.defaultValue(2u).withConstraint(validateUint32)}, {"read_only", ConfigValue{ConfigType::Boolean}.defaultValue(false)}, - {"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0)}, - {"start_sequence", ConfigValue{ConfigType::String}.optional()}, - {"finish_sequence", ConfigValue{ConfigType::String}.optional()}, + {"txn_threshold", ConfigValue{ConfigType::Integer}.defaultValue(0).withConstraint(validateUint16)}, + {"start_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)}, + {"finish_sequence", ConfigValue{ConfigType::Integer}.optional().withConstraint(validateUint32)}, {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()}, {"ssl_key_file", ConfigValue{ConfigType::String}.optional()}, {"api_version.min", ConfigValue{ConfigType::Integer}}, @@ -113,7 +133,7 @@ ClioConfigDefinition::ClioConfigDefinition(std::initializer_list p { for (auto const& [key, value] : pair) { if (key.contains("[]")) - ASSERT(std::holds_alternative(value), "Value must be array if key has \"[]\""); + ASSERT(std::holds_alternative(value), R"(Value must be array if key has "[]")"); map_.insert({key, value}); } } @@ -206,4 +226,51 @@ ClioConfigDefinition::arraySize(std::string_view prefix) const std::unreachable(); } +std::optional> +ClioConfigDefinition::parse(ConfigFileInterface const& config) +{ + std::vector listOfErrors; + for (auto& [key, value] : map_) { + // if key doesn't exist in user config, makes sure it is marked as ".optional()" or has ".defaultValue()"" in + // ClioConfigDefitinion above + if (!config.containsKey(key)) { + if (std::holds_alternative(value)) { + if (!(std::get(value).isOptional() || std::get(value).hasValue())) + listOfErrors.emplace_back(key, "key is required in user Config"); + } else if (std::holds_alternative(value)) { + if (!(std::get(value).getArrayPattern().isOptional())) + listOfErrors.emplace_back(key, "key is required in user Config"); + } + continue; + } + ASSERT( + std::holds_alternative(value) || std::holds_alternative(value), + "Value must be of type ConfigValue or Array" + ); + std::visit( + util::OverloadSet{// handle the case where the config value is a single element. + // attempt to set the value from the configuration for the specified key. + [&key, &config, &listOfErrors](ConfigValue& val) { + if (auto const maybeError = val.setValue(config.getValue(key), key); + maybeError.has_value()) + listOfErrors.emplace_back(maybeError.value()); + }, + // handle the case where the config value is an array. + // iterate over each provided value in the array and attempt to set it for the key. + [&key, &config, &listOfErrors](Array& arr) { + for (auto const& val : config.getArray(key)) { + if (auto const maybeError = arr.addValue(val, key); maybeError.has_value()) + listOfErrors.emplace_back(maybeError.value()); + } + } + }, + value + ); + } + if (!listOfErrors.empty()) + return listOfErrors; + + return std::nullopt; +} + } // namespace util::config diff --git a/src/util/newconfig/ConfigDefinition.hpp b/src/util/newconfig/ConfigDefinition.hpp index 8a1a58b6c..01019339e 100644 --- a/src/util/newconfig/ConfigDefinition.hpp +++ b/src/util/newconfig/ConfigDefinition.hpp @@ -24,7 +24,7 @@ #include "util/newconfig/ConfigDescription.hpp" #include "util/newconfig/ConfigFileInterface.hpp" #include "util/newconfig/ConfigValue.hpp" -#include "util/newconfig/Errors.hpp" +#include "util/newconfig/Error.hpp" #include "util/newconfig/ObjectView.hpp" #include "util/newconfig/ValueView.hpp" @@ -41,6 +41,7 @@ #include #include #include +#include namespace util::config { @@ -66,12 +67,13 @@ class ClioConfigDefinition { /** * @brief Parses the configuration file * - * Should also check that no extra configuration key/value pairs are present + * Also checks that no extra configuration key/value pairs are present. Adds to list of Errors + * if it does * * @param config The configuration file interface - * @return An optional Error object if parsing fails + * @return An optional vector of Error objects stating all the failures if parsing fails */ - [[nodiscard]] std::optional + [[nodiscard]] std::optional> parse(ConfigFileInterface const& config); /** @@ -80,9 +82,9 @@ class ClioConfigDefinition { * Should only check for valid values, without populating * * @param config The configuration file interface - * @return An optional Error object if validation fails + * @return An optional vector of Error objects stating all the failures if validation fails */ - [[nodiscard]] std::optional + [[nodiscard]] std::optional> validate(ConfigFileInterface const& config) const; /** diff --git a/src/util/newconfig/ConfigDescription.hpp b/src/util/newconfig/ConfigDescription.hpp index 0a95729b5..1fbb5406c 100644 --- a/src/util/newconfig/ConfigDescription.hpp +++ b/src/util/newconfig/ConfigDescription.hpp @@ -90,7 +90,9 @@ struct ClioConfigDescription { KV{"server.ip", "IP address of the Clio HTTP server."}, KV{"server.port", "Port number of the Clio HTTP server."}, KV{"server.max_queue_size", "Maximum size of the server's request queue."}, + KV{"server.workers", "Maximum number of threads for server to run with."}, KV{"server.local_admin", "Indicates if the server should run with admin privileges."}, + KV{"server.admin_password", "Password for Clio admin-only APIs."}, KV{"prometheus.enabled", "Enable or disable Prometheus metrics."}, KV{"prometheus.compress_reply", "Enable or disable compression of Prometheus responses."}, KV{"io_threads", "Number of I/O threads."}, diff --git a/src/util/newconfig/ConfigFileInterface.hpp b/src/util/newconfig/ConfigFileInterface.hpp index c6db62c4f..a26dda043 100644 --- a/src/util/newconfig/ConfigFileInterface.hpp +++ b/src/util/newconfig/ConfigFileInterface.hpp @@ -19,9 +19,8 @@ #pragma once -#include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" -#include #include #include @@ -36,31 +35,33 @@ namespace util::config { class ConfigFileInterface { public: virtual ~ConfigFileInterface() = default; - /** - * @brief Parses the provided path of user clio configuration data - * - * @param filePath The path to the Clio Config data - */ - virtual void - parse(std::string_view filePath) = 0; /** - * @brief Retrieves a configuration value. + * @brief Retrieves the value of configValue. * - * @param key The key of the configuration value. - * @return An optional containing the configuration value if found, otherwise std::nullopt. + * @param key The key of configuration. + * @return the value assosiated with key. */ - virtual std::optional + virtual Value getValue(std::string_view key) const = 0; /** * @brief Retrieves an array of configuration values. * * @param key The key of the configuration array. - * @return An optional containing a vector of configuration values if found, otherwise std::nullopt. + * @return A vector of configuration values if found, otherwise std::nullopt. */ - virtual std::optional> + virtual std::vector getArray(std::string_view key) const = 0; + + /** + * @brief Checks if key exist in configuration file. + * + * @param key The key to search for. + * @return true if key exists in configuration file, false otherwise. + */ + virtual bool + containsKey(std::string_view key) const = 0; }; } // namespace util::config diff --git a/src/util/newconfig/ConfigFileJson.cpp b/src/util/newconfig/ConfigFileJson.cpp new file mode 100644 index 000000000..4451eccbd --- /dev/null +++ b/src/util/newconfig/ConfigFileJson.cpp @@ -0,0 +1,166 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/newconfig/ConfigFileJson.hpp" + +#include "util/Assert.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace util::config { + +namespace { +/** + * @brief Extracts the value from a JSON object and converts it into the corresponding type. + * + * @param jsonValue The JSON value to extract. + * @return A variant containing the same type corresponding to the extracted value. + */ +[[nodiscard]] Value +extractJsonValue(boost::json::value const& jsonValue) +{ + if (jsonValue.is_int64()) { + return jsonValue.as_int64(); + } + if (jsonValue.is_string()) { + return jsonValue.as_string().c_str(); + } + if (jsonValue.is_bool()) { + return jsonValue.as_bool(); + } + if (jsonValue.is_double()) { + return jsonValue.as_double(); + } + ASSERT(false, "Json is not of type int, string, bool or double"); + std::unreachable(); +} +} // namespace + +ConfigFileJson::ConfigFileJson(boost::json::object jsonObj) +{ + flattenJson(jsonObj, ""); +} + +std::expected +ConfigFileJson::make_ConfigFileJson(boost::filesystem::path configFilePath) +{ + try { + if (auto const in = std::ifstream(configFilePath.string(), std::ios::in | std::ios::binary); in) { + std::stringstream contents; + contents << in.rdbuf(); + auto opts = boost::json::parse_options{}; + opts.allow_comments = true; + auto const tempObj = boost::json::parse(contents.str(), {}, opts).as_object(); + return ConfigFileJson{tempObj}; + } + return std::unexpected( + Error{fmt::format("Could not open configuration file '{}'", configFilePath.string())} + ); + + } catch (std::exception const& e) { + return std::unexpected(Error{fmt::format( + "An error occurred while processing configuration file '{}': {}", configFilePath.string(), e.what() + )}); + } +} + +Value +ConfigFileJson::getValue(std::string_view key) const +{ + auto const jsonValue = jsonObject_.at(key); + auto const value = extractJsonValue(jsonValue); + return value; +} + +std::vector +ConfigFileJson::getArray(std::string_view key) const +{ + ASSERT(jsonObject_.at(key).is_array(), "Key {} has value that is not an array", key); + + std::vector configValues; + auto const arr = jsonObject_.at(key).as_array(); + + for (auto const& item : arr) { + auto const value = extractJsonValue(item); + configValues.emplace_back(value); + } + return configValues; +} + +bool +ConfigFileJson::containsKey(std::string_view key) const +{ + return jsonObject_.contains(key); +} + +void +ConfigFileJson::flattenJson(boost::json::object const& obj, std::string const& prefix) +{ + for (auto const& [key, value] : obj) { + std::string fullKey = prefix.empty() ? std::string(key) : fmt::format("{}.{}", prefix, std::string(key)); + + // In ClioConfigDefinition, value must be a primitive or array + if (value.is_object()) { + flattenJson(value.as_object(), fullKey); + } else if (value.is_array()) { + auto const& arr = value.as_array(); + for (std::size_t i = 0; i < arr.size(); ++i) { + std::string arrayPrefix = fullKey + ".[]"; + if (arr[i].is_object()) { + flattenJson(arr[i].as_object(), arrayPrefix); + } else { + jsonObject_[arrayPrefix] = arr; + } + } + } else { + // if "[]" is present in key, then value must be an array instead of primitive + if (fullKey.contains(".[]") && !jsonObject_.contains(fullKey)) { + boost::json::array newArray; + newArray.emplace_back(value); + jsonObject_[fullKey] = newArray; + } else if (fullKey.contains(".[]") && jsonObject_.contains(fullKey)) { + jsonObject_[fullKey].as_array().emplace_back(value); + } else { + jsonObject_[fullKey] = value; + } + } + } +} + +} // namespace util::config diff --git a/src/util/newconfig/ConfigFileJson.hpp b/src/util/newconfig/ConfigFileJson.hpp new file mode 100644 index 000000000..a27037545 --- /dev/null +++ b/src/util/newconfig/ConfigFileJson.hpp @@ -0,0 +1,100 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/newconfig/ConfigFileInterface.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace util::config { + +/** @brief Json representation of config */ +class ConfigFileJson final : public ConfigFileInterface { +public: + /** + * @brief Construct a new ConfigJson object and stores the values from + * user's config into a json object. + * + * @param jsonObj the Json object to parse; represents user's config + */ + ConfigFileJson(boost::json::object jsonObj); + + /** + * @brief Retrieves a configuration value by its key. + * + * @param key The key of the configuration value to retrieve. + * @return A variant containing the same type corresponding to the extracted value. + */ + [[nodiscard]] Value + getValue(std::string_view key) const override; + + /** + * @brief Retrieves an array of configuration values by its key. + * + * @param key The key of the configuration array to retrieve. + * @return A vector of variants holding the config values specified by user. + */ + [[nodiscard]] std::vector + getArray(std::string_view key) const override; + + /** + * @brief Checks if the configuration contains a specific key. + * + * @param key The key to check for. + * @return True if the key exists, false otherwise. + */ + [[nodiscard]] bool + containsKey(std::string_view key) const override; + + /** + * @brief Creates a new ConfigFileJson by parsing the provided JSON file and + * stores the values in the object. + * + * @param configFilePath The path to the JSON file to be parsed. + * @return A ConfigFileJson object if parsing user file is successful. Error otherwise + */ + [[nodiscard]] static std::expected + make_ConfigFileJson(boost::filesystem::path configFilePath); + +private: + /** + * @brief Recursive function to flatten a JSON object into the same structure as the Clio Config. + * + * The keys will end up having the same naming convensions in Clio Config. + * Other than the keys specified in user Config file, no new keys are created. + * + * @param obj The JSON object to flatten. + * @param prefix The prefix to use for the keys in the flattened object. + */ + void + flattenJson(boost::json::object const& obj, std::string const& prefix); + + boost::json::object jsonObject_; +}; + +} // namespace util::config diff --git a/src/util/newconfig/Errors.hpp b/src/util/newconfig/ConfigFileYaml.hpp similarity index 68% rename from src/util/newconfig/Errors.hpp rename to src/util/newconfig/ConfigFileYaml.hpp index 75ab93680..ac943ef53 100644 --- a/src/util/newconfig/Errors.hpp +++ b/src/util/newconfig/ConfigFileYaml.hpp @@ -19,15 +19,31 @@ #pragma once -#include +#include "util/newconfig/ConfigFileInterface.hpp" +#include "util/newconfig/Types.hpp" + +#include + #include +#include + +// TODO: implement when we support yaml namespace util::config { -/** @brief todo: Will display the different errors when parsing config */ -struct Error { - std::string_view key; - std::string_view error; +/** @brief Yaml representation of config */ +class ConfigFileYaml final : public ConfigFileInterface { +public: + ConfigFileYaml() = default; + + Value + getValue(std::string_view key) const override; + + std::vector + getArray(std::string_view key) const override; + + bool + containsKey(std::string_view key) const override; }; } // namespace util::config diff --git a/src/util/newconfig/ConfigValue.hpp b/src/util/newconfig/ConfigValue.hpp index df6e87a80..eb26b69f8 100644 --- a/src/util/newconfig/ConfigValue.hpp +++ b/src/util/newconfig/ConfigValue.hpp @@ -20,43 +20,23 @@ #pragma once #include "util/Assert.hpp" -#include "util/UnsupportedType.hpp" +#include "util/OverloadSet.hpp" +#include "util/newconfig/ConfigConstraints.hpp" +#include "util/newconfig/Error.hpp" +#include "util/newconfig/Types.hpp" + +#include #include #include +#include #include #include -#include +#include #include namespace util::config { -/** @brief Custom clio config types */ -enum class ConfigType { Integer, String, Double, Boolean }; - -/** - * @brief Get the corresponding clio config type - * - * @tparam Type The type to get the corresponding ConfigType for - * @return The corresponding ConfigType - */ -template -constexpr ConfigType -getType() -{ - if constexpr (std::is_same_v) { - return ConfigType::Integer; - } else if constexpr (std::is_same_v) { - return ConfigType::String; - } else if constexpr (std::is_same_v) { - return ConfigType::Double; - } else if constexpr (std::is_same_v) { - return ConfigType::Boolean; - } else { - static_assert(util::Unsupported, "Wrong config type"); - } -} - /** * @brief Represents the config values for Json/Yaml config * @@ -65,8 +45,6 @@ getType() */ class ConfigValue { public: - using Type = std::variant; - /** * @brief Constructor initializing with the config type * @@ -83,47 +61,101 @@ class ConfigValue { * @return Reference to this ConfigValue */ [[nodiscard]] ConfigValue& - defaultValue(Type value) + defaultValue(Value value) { - setValue(value); + auto const err = checkTypeConsistency(type_, value); + ASSERT(!err.has_value(), "{}", err->error); + value_ = value; return *this; } /** - * @brief Gets the config type + * @brief Sets the value current ConfigValue given by the User's defined value * - * @return The config type + * @param value The value to set + * @param key The Config key associated with the value. Optional to include; Used for debugging message to user. + * @return optional Error if user tries to set a value of wrong type or not within a constraint */ - [[nodiscard]] constexpr ConfigType - type() const + [[nodiscard]] std::optional + setValue(Value value, std::optional key = std::nullopt) { - return type_; + auto err = checkTypeConsistency(type_, value); + if (err.has_value()) { + if (key.has_value()) + err->error = fmt::format("{} {}", key.value(), err->error); + return err; + } + + if (cons_.has_value()) { + auto constraintCheck = cons_->get().checkConstraint(value); + if (constraintCheck.has_value()) { + if (key.has_value()) + constraintCheck->error = fmt::format("{} {}", key.value(), constraintCheck->error); + return constraintCheck; + } + } + value_ = value; + return std::nullopt; } /** - * @brief Sets the minimum value for the config + * @brief Assigns a constraint to the ConfigValue. * - * @param min The minimum value - * @return Reference to this ConfigValue + * This method associates a specific constraint with the ConfigValue. + * If the ConfigValue already holds a value, the method will check whether + * the value satisfies the given constraint. If the constraint is not satisfied, + * an assertion failure will occur with a detailed error message. + * + * @param cons The constraint to be applied to the ConfigValue. + * @return A reference to the modified ConfigValue object. */ [[nodiscard]] constexpr ConfigValue& - min(std::uint32_t min) + withConstraint(Constraint const& cons) { - min_ = min; + cons_ = std::reference_wrapper(cons); + ASSERT(cons_.has_value(), "Constraint must be defined"); + + if (value_.has_value()) { + auto const& temp = cons_.value().get(); + auto const& result = temp.checkConstraint(value_.value()); + if (result.has_value()) { + // useful for specifying clear Error message + std::string type; + std::visit( + util::OverloadSet{ + [&type](bool tmp) { type = fmt::format("bool {}", tmp); }, + [&type](std::string const& tmp) { type = fmt::format("string {}", tmp); }, + [&type](double tmp) { type = fmt::format("double {}", tmp); }, + [&type](int64_t tmp) { type = fmt::format("int {}", tmp); } + }, + value_.value() + ); + ASSERT(false, "Value {} ConfigValue does not satisfy the set Constraint", type); + } + } return *this; } /** - * @brief Sets the maximum value for the config + * @brief Retrieves the constraint associated with this ConfigValue, if any. * - * @param max The maximum value - * @return Reference to this ConfigValue + * @return An optional reference to the associated Constraint. */ - [[nodiscard]] constexpr ConfigValue& - max(std::uint32_t max) + [[nodiscard]] std::optional> + getConstraint() const { - max_ = max; - return *this; + return cons_; + } + + /** + * @brief Gets the config type + * + * @return The config type + */ + [[nodiscard]] constexpr ConfigType + type() const + { + return type_; } /** @@ -165,7 +197,7 @@ class ConfigValue { * * @return Config Value */ - [[nodiscard]] Type const& + [[nodiscard]] Value const& getValue() const { return value_.value(); @@ -178,39 +210,28 @@ class ConfigValue { * @param type The config type * @param value The config value */ - static void - checkTypeConsistency(ConfigType type, Type value) + static std::optional + checkTypeConsistency(ConfigType type, Value value) { - if (std::holds_alternative(value)) { - ASSERT(type == ConfigType::String, "Value does not match type string"); - } else if (std::holds_alternative(value)) { - ASSERT(type == ConfigType::Boolean, "Value does not match type boolean"); - } else if (std::holds_alternative(value)) { - ASSERT(type == ConfigType::Double, "Value does not match type double"); - } else if (std::holds_alternative(value)) { - ASSERT(type == ConfigType::Integer, "Value does not match type integer"); + if (type == ConfigType::String && !std::holds_alternative(value)) { + return Error{"value does not match type string"}; } - } - - /** - * @brief Sets the value for the config - * - * @param value The value to set - * @return The value that was set - */ - Type - setValue(Type value) - { - checkTypeConsistency(type_, value); - value_ = value; - return value; + if (type == ConfigType::Boolean && !std::holds_alternative(value)) { + return Error{"value does not match type boolean"}; + } + if (type == ConfigType::Double && !std::holds_alternative(value)) { + return Error{"value does not match type double"}; + } + if (type == ConfigType::Integer && !std::holds_alternative(value)) { + return Error{"value does not match type integer"}; + } + return std::nullopt; } ConfigType type_{}; bool optional_{false}; - std::optional value_; - std::optional min_; - std::optional max_; + std::optional value_; + std::optional> cons_; }; } // namespace util::config diff --git a/src/util/newconfig/Error.hpp b/src/util/newconfig/Error.hpp new file mode 100644 index 000000000..8261c2a3f --- /dev/null +++ b/src/util/newconfig/Error.hpp @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include + +#include +#include +#include + +namespace util::config { + +/** @brief Displays the different errors when parsing user config */ +struct Error { + /** + * @brief Constructs an Error with a custom error message. + * + * @param err the error message to display to users. + */ + Error(std::string err) : error{std::move(err)} + { + } + + /** + * @brief Constructs an Error with a custom error message. + * + * @param key the key associated with the error. + * @param err the error message to display to users. + */ + Error(std::string_view key, std::string_view err) + : error{ + fmt::format("{} {}", key, err), + } + { + } + + std::string error; +}; + +} // namespace util::config diff --git a/src/util/newconfig/Types.hpp b/src/util/newconfig/Types.hpp new file mode 100644 index 000000000..42c9ddffa --- /dev/null +++ b/src/util/newconfig/Types.hpp @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#pragma once + +#include "util/UnsupportedType.hpp" + +#include +#include +#include +#include + +namespace util::config { + +/** @brief Custom clio config types */ +enum class ConfigType { Integer, String, Double, Boolean }; + +/** @brief Represents the supported Config Values */ +using Value = std::variant; + +/** + * @brief Get the corresponding clio config type + * + * @tparam Type The type to get the corresponding ConfigType for + * @return The corresponding ConfigType + */ +template +constexpr ConfigType +getType() +{ + if constexpr (std::is_same_v) { + return ConfigType::Integer; + } else if constexpr (std::is_same_v) { + return ConfigType::String; + } else if constexpr (std::is_same_v) { + return ConfigType::Double; + } else if constexpr (std::is_same_v) { + return ConfigType::Boolean; + } else { + static_assert(util::Unsupported, "Wrong config type"); + } +} + +} // namespace util::config diff --git a/src/util/newconfig/ValueView.cpp b/src/util/newconfig/ValueView.cpp index 3019c8442..a8b16a8b3 100644 --- a/src/util/newconfig/ValueView.cpp +++ b/src/util/newconfig/ValueView.cpp @@ -21,6 +21,7 @@ #include "util/Assert.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" #include #include @@ -55,9 +56,9 @@ double ValueView::asDouble() const { if (configVal_.get().hasValue()) { - if (type() == ConfigType::Double) { + if (type() == ConfigType::Double) return std::get(configVal_.get().getValue()); - } + if (type() == ConfigType::Integer) return static_cast(std::get(configVal_.get().getValue())); } diff --git a/src/util/newconfig/ValueView.hpp b/src/util/newconfig/ValueView.hpp index 1622cbdf3..5fd0d40a5 100644 --- a/src/util/newconfig/ValueView.hpp +++ b/src/util/newconfig/ValueView.hpp @@ -21,6 +21,7 @@ #include "util/Assert.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" #include @@ -84,7 +85,7 @@ class ValueView { return static_cast(val); } } - ASSERT(false, "Value view is not of any Int type"); + ASSERT(false, "Value view is not of Int type"); return 0; } diff --git a/tests/common/util/newconfig/FakeConfigData.hpp b/tests/common/util/newconfig/FakeConfigData.hpp index 683bae0e4..4064a4f97 100644 --- a/tests/common/util/newconfig/FakeConfigData.hpp +++ b/tests/common/util/newconfig/FakeConfigData.hpp @@ -20,13 +20,25 @@ #pragma once #include "util/newconfig/Array.hpp" +#include "util/newconfig/ConfigConstraints.hpp" #include "util/newconfig/ConfigDefinition.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" #include using namespace util::config; +/** + * @brief A mock ClioConfigDefinition for testing purposes. + * + * In the actual Clio configuration, arrays typically hold optional values, meaning users are not required to + * provide values for them. + * + * For primitive types (i.e., single specific values), some are mandatory and must be explicitly defined in the + * user's configuration file, including both the key and the corresponding value, while some are optional + */ + inline ClioConfigDefinition generateConfig() { @@ -36,60 +48,167 @@ generateConfig() {"header.admin", ConfigValue{ConfigType::Boolean}.defaultValue(true)}, {"header.sub.sub2Value", ConfigValue{ConfigType::String}.defaultValue("TSM")}, {"ip", ConfigValue{ConfigType::Double}.defaultValue(444.22)}, - {"array.[].sub", - Array{ - ConfigValue{ConfigType::Double}.defaultValue(111.11), ConfigValue{ConfigType::Double}.defaultValue(4321.55) - }}, - {"array.[].sub2", - Array{ - ConfigValue{ConfigType::String}.defaultValue("subCategory"), - ConfigValue{ConfigType::String}.defaultValue("temporary") - }}, - {"higher.[].low.section", Array{ConfigValue{ConfigType::String}.defaultValue("true")}}, - {"higher.[].low.admin", Array{ConfigValue{ConfigType::Boolean}.defaultValue(false)}}, - {"dosguard.whitelist.[]", - Array{ - ConfigValue{ConfigType::String}.defaultValue("125.5.5.2"), - ConfigValue{ConfigType::String}.defaultValue("204.2.2.2") - }}, - {"dosguard.port", ConfigValue{ConfigType::Integer}.defaultValue(55555)} + {"array.[].sub", Array{ConfigValue{ConfigType::Double}}}, + {"array.[].sub2", Array{ConfigValue{ConfigType::String}.optional()}}, + {"higher.[].low.section", Array{ConfigValue{ConfigType::String}.withConstraint(validateChannelName)}}, + {"higher.[].low.admin", Array{ConfigValue{ConfigType::Boolean}}}, + {"dosguard.whitelist.[]", Array{ConfigValue{ConfigType::String}.optional()}}, + {"dosguard.port", ConfigValue{ConfigType::Integer}.defaultValue(55555).withConstraint(validatePort)}, + {"optional.withDefault", ConfigValue{ConfigType::Double}.defaultValue(0.0).optional()}, + {"optional.withNoDefault", ConfigValue{ConfigType::Double}.optional()}, + {"requireValue", ConfigValue{ConfigType::String}} }; } -/* The config definition above would look like this structure in config.json: -"header": { - "text1": "value", - "port": 123, - "admin": true, - "sub": { - "sub2Value": "TSM" - } - }, - "ip": 444.22, - "array": [ - { - "sub": 111.11, - "sub2": "subCategory" - }, - { - "sub": 4321.55, - "sub2": "temporary" - } - ], - "higher": [ - { - "low": { - "section": "true", - "admin": false +/* The config definition above would look like this structure in config.json +{ + "header": { + "text1": "value", + "port": 321, + "admin": true, + "sub": { + "sub2Value": "TSM" } - } - ], - "dosguard": { - "whitelist": [ - "125.5.5.2", "204.2.2.2" - ], - "port" : 55555 - }, + }, + "ip": 444.22, + "array": [ + { + "sub": //optional for user to include + "sub2": //optional for user to include + }, + ], + "higher": [ + { + "low": { + "section": //optional for user to include + "admin": //optional for user to include + } + } + ], + "dosguard": { + "whitelist": [ + // mandatory for user to include + ], + "port" : 55555 + }, + }, + "optional" : { + "withDefault" : 0.0, + "withNoDefault" : //optional for user to include + }, + "requireValue" : // value must be provided by user + } +*/ +/* Used to test overwriting default values in ClioConfigDefinition Above */ +constexpr static auto JSONData = R"JSON( + { + "header": { + "text1": "value", + "port": 321, + "admin": false, + "sub": { + "sub2Value": "TSM" + } + }, + "array": [ + { + "sub": 111.11, + "sub2": "subCategory" + }, + { + "sub": 4321.55, + "sub2": "temporary" + }, + { + "sub": 5555.44, + "sub2": "london" + } + ], + "higher": [ + { + "low": { + "section": "WebServer", + "admin": false + } + } + ], + "dosguard": { + "whitelist": [ + "125.5.5.1", "204.2.2.1" + ], + "port" : 44444 + }, + "optional" : { + "withDefault" : 0.0 + }, + "requireValue" : "required" + } +)JSON"; +/* After parsing jsonValue and populating it into ClioConfig, It will look like this below in json format; +{ + "header": { + "text1": "value", + "port": 321, + "admin": false, + "sub": { + "sub2Value": "TSM" + } + }, + "ip": 444.22, + "array": [ + { + "sub": 111.11, + "sub2": "subCategory" + }, + { + "sub": 4321.55, + "sub2": "temporary" + }, + { + "sub": 5555.44, + "sub2": "london" + } + ], + "higher": [ + { + "low": { + "section": "WebServer", + "admin": false + } + } + ], + "dosguard": { + "whitelist": [ + "125.5.5.1", "204.2.2.1" + ], + "port" : 44444 + } + }, + "optional" : { + "withDefault" : 0.0 + }, + "requireValue" : "required" + } */ + +// Invalid Json key/values +constexpr static auto invalidJSONData = R"JSON( +{ + "header": { + "port": "999", + "admin": "true" + }, + "dosguard": { + "whitelist": [ + false + ] + }, + "idk": true, + "requireValue" : "required", + "optional" : { + "withDefault" : "0.0" + } +} +)JSON"; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 74cd91b8e..2af8c6bcf 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -133,12 +133,13 @@ target_sources( web/IntervalSweepHandlerTests.cpp web/WhitelistHandlerTests.cpp # New Config + util/newconfig/ArrayTests.cpp util/newconfig/ArrayViewTests.cpp + util/newconfig/ClioConfigDefinitionTests.cpp + util/newconfig/ConfigValueTests.cpp util/newconfig/ObjectViewTests.cpp + util/newconfig/JsonConfigFileTests.cpp util/newconfig/ValueViewTests.cpp - util/newconfig/ArrayTests.cpp - util/newconfig/ConfigValueTests.cpp - util/newconfig/ClioConfigDefinitionTests.cpp ) configure_file(test_data/cert.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/cert.pem COPYONLY) diff --git a/tests/unit/util/newconfig/ArrayTests.cpp b/tests/unit/util/newconfig/ArrayTests.cpp index a7b493098..98f2ac20a 100644 --- a/tests/unit/util/newconfig/ArrayTests.cpp +++ b/tests/unit/util/newconfig/ArrayTests.cpp @@ -18,33 +18,69 @@ //============================================================================== #include "util/newconfig/Array.hpp" +#include "util/newconfig/ConfigConstraints.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" #include "util/newconfig/ValueView.hpp" #include +#include +#include +#include + using namespace util::config; -TEST(ArrayTest, testConfigArray) +TEST(ArrayTest, addSingleValue) +{ + auto arr = Array{ConfigValue{ConfigType::Double}}; + arr.addValue(111.11); + EXPECT_EQ(arr.size(), 1); +} + +TEST(ArrayTest, addAndCheckMultipleValues) { - auto arr = Array{ - ConfigValue{ConfigType::Boolean}.defaultValue(false), - ConfigValue{ConfigType::Integer}.defaultValue(1234), - ConfigValue{ConfigType::Double}.defaultValue(22.22), - }; - auto cv = arr.at(0); - ValueView const vv{cv}; - EXPECT_EQ(vv.asBool(), false); - - auto cv2 = arr.at(1); - ValueView const vv2{cv2}; - EXPECT_EQ(vv2.asIntType(), 1234); + auto arr = Array{ConfigValue{ConfigType::Double}}; + arr.addValue(111.11); + arr.addValue(222.22); + arr.addValue(333.33); + EXPECT_EQ(arr.size(), 3); + + auto const cv = arr.at(0); + ValueView vv{cv}; + EXPECT_EQ(vv.asDouble(), 111.11); + + auto const cv2 = arr.at(1); + ValueView vv2{cv2}; + EXPECT_EQ(vv2.asDouble(), 222.22); EXPECT_EQ(arr.size(), 3); - arr.emplaceBack(ConfigValue{ConfigType::String}.defaultValue("false")); + arr.addValue(444.44); EXPECT_EQ(arr.size(), 4); - auto cv4 = arr.at(3); - ValueView const vv4{cv4}; - EXPECT_EQ(vv4.asString(), "false"); + auto const cv4 = arr.at(3); + ValueView vv4{cv4}; + EXPECT_EQ(vv4.asDouble(), 444.44); +} + +TEST(ArrayTest, testArrayPattern) +{ + auto const arr = Array{ConfigValue{ConfigType::String}}; + auto const arrPattern = arr.getArrayPattern(); + EXPECT_EQ(arrPattern.type(), ConfigType::String); +} + +TEST(ArrayTest, iterateValueArray) +{ + auto arr = Array{ConfigValue{ConfigType::Integer}.withConstraint(validateUint16)}; + std::vector const expected{543, 123, 909}; + + for (auto const num : expected) + arr.addValue(num); + + std::vector actual; + for (auto it = arr.begin(); it != arr.end(); ++it) + actual.emplace_back(std::get(it->getValue())); + + EXPECT_TRUE(std::ranges::equal(expected, actual)); } diff --git a/tests/unit/util/newconfig/ArrayViewTests.cpp b/tests/unit/util/newconfig/ArrayViewTests.cpp index fb048a7c6..0761e77eb 100644 --- a/tests/unit/util/newconfig/ArrayViewTests.cpp +++ b/tests/unit/util/newconfig/ArrayViewTests.cpp @@ -19,11 +19,13 @@ #include "util/newconfig/ArrayView.hpp" #include "util/newconfig/ConfigDefinition.hpp" -#include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/ConfigFileJson.hpp" #include "util/newconfig/FakeConfigData.hpp" #include "util/newconfig/ObjectView.hpp" +#include "util/newconfig/Types.hpp" #include "util/newconfig/ValueView.hpp" +#include #include #include @@ -31,33 +33,65 @@ using namespace util::config; struct ArrayViewTest : testing::Test { - ClioConfigDefinition const configData = generateConfig(); + ArrayViewTest() + { + ConfigFileJson const jsonFileObj{boost::json::parse(JSONData).as_object()}; + auto const errors = configData.parse(jsonFileObj); + EXPECT_TRUE(!errors.has_value()); + } + ClioConfigDefinition configData = generateConfig(); }; -TEST_F(ArrayViewTest, ArrayValueTest) +// Array View tests can only be tested after the values are populated from user Config +// into ConfigClioDefinition +TEST_F(ArrayViewTest, ArrayGetValueDouble) { + auto const precision = 1e-9; ArrayView const arrVals = configData.getArray("array.[].sub"); - auto valIt = arrVals.begin(); + + auto const firstVal = arrVals.valueAt(0); + EXPECT_EQ(firstVal.type(), ConfigType::Double); + EXPECT_TRUE(firstVal.hasValue()); + EXPECT_FALSE(firstVal.isOptional()); + + EXPECT_NEAR(111.11, firstVal.asDouble(), precision); + EXPECT_NEAR(4321.55, arrVals.valueAt(1).asDouble(), precision); +} + +TEST_F(ArrayViewTest, ArrayGetValueString) +{ + ArrayView const arrVals = configData.getArray("array.[].sub2"); + ValueView const firstVal = arrVals.valueAt(0); + + EXPECT_EQ(firstVal.type(), ConfigType::String); + EXPECT_EQ("subCategory", firstVal.asString()); + EXPECT_EQ("london", arrVals.valueAt(2).asString()); +} + +TEST_F(ArrayViewTest, IterateValuesDouble) +{ auto const precision = 1e-9; + ArrayView const arrVals = configData.getArray("array.[].sub"); + + auto valIt = arrVals.begin(); EXPECT_NEAR((*valIt++).asDouble(), 111.11, precision); EXPECT_NEAR((*valIt++).asDouble(), 4321.55, precision); + EXPECT_NEAR((*valIt++).asDouble(), 5555.44, precision); EXPECT_EQ(valIt, arrVals.end()); +} - EXPECT_NEAR(111.11, arrVals.valueAt(0).asDouble(), precision); - EXPECT_NEAR(4321.55, arrVals.valueAt(1).asDouble(), precision); +TEST_F(ArrayViewTest, IterateValuesString) +{ + ArrayView const arrVals = configData.getArray("array.[].sub2"); - ArrayView const arrVals2 = configData.getArray("array.[].sub2"); - auto val2It = arrVals2.begin(); + auto val2It = arrVals.begin(); EXPECT_EQ((*val2It++).asString(), "subCategory"); EXPECT_EQ((*val2It++).asString(), "temporary"); - EXPECT_EQ(val2It, arrVals2.end()); - - ValueView const tempVal = arrVals2.valueAt(0); - EXPECT_EQ(tempVal.type(), ConfigType::String); - EXPECT_EQ("subCategory", tempVal.asString()); + EXPECT_EQ((*val2It++).asString(), "london"); + EXPECT_EQ(val2It, arrVals.end()); } -TEST_F(ArrayViewTest, ArrayWithObjTest) +TEST_F(ArrayViewTest, ArrayWithObj) { ArrayView const arrVals = configData.getArray("array.[]"); ArrayView const arrValAlt = configData.getArray("array"); @@ -73,20 +107,19 @@ TEST_F(ArrayViewTest, IterateArray) { auto arr = configData.getArray("dosguard.whitelist"); EXPECT_EQ(2, arr.size()); - EXPECT_EQ(arr.valueAt(0).asString(), "125.5.5.2"); - EXPECT_EQ(arr.valueAt(1).asString(), "204.2.2.2"); + EXPECT_EQ(arr.valueAt(0).asString(), "125.5.5.1"); + EXPECT_EQ(arr.valueAt(1).asString(), "204.2.2.1"); auto it = arr.begin(); - EXPECT_EQ((*it++).asString(), "125.5.5.2"); - EXPECT_EQ((*it++).asString(), "204.2.2.2"); + EXPECT_EQ((*it++).asString(), "125.5.5.1"); + EXPECT_EQ((*it++).asString(), "204.2.2.1"); EXPECT_EQ((it), arr.end()); } -TEST_F(ArrayViewTest, DifferentArrayIterators) +TEST_F(ArrayViewTest, CompareDifferentArrayIterators) { auto const subArray = configData.getArray("array.[].sub"); auto const dosguardArray = configData.getArray("dosguard.whitelist.[]"); - ASSERT_EQ(subArray.size(), dosguardArray.size()); auto itArray = subArray.begin(); auto itDosguard = dosguardArray.begin(); @@ -98,7 +131,7 @@ TEST_F(ArrayViewTest, DifferentArrayIterators) TEST_F(ArrayViewTest, IterateObject) { auto arr = configData.getArray("array"); - EXPECT_EQ(2, arr.size()); + EXPECT_EQ(3, arr.size()); auto it = arr.begin(); EXPECT_EQ(111.11, (*it).getValue("sub").asDouble()); @@ -107,33 +140,37 @@ TEST_F(ArrayViewTest, IterateObject) EXPECT_EQ(4321.55, (*it).getValue("sub").asDouble()); EXPECT_EQ("temporary", (*it++).getValue("sub2").asString()); + EXPECT_EQ(5555.44, (*it).getValue("sub").asDouble()); + EXPECT_EQ("london", (*it++).getValue("sub2").asString()); + EXPECT_EQ(it, arr.end()); } struct ArrayViewDeathTest : ArrayViewTest {}; -TEST_F(ArrayViewDeathTest, IncorrectAccess) +TEST_F(ArrayViewDeathTest, AccessArrayOutOfBounce) { - ArrayView const arr = configData.getArray("higher"); - - // dies because higher only has 1 object - EXPECT_DEATH({ [[maybe_unused]] auto _ = arr.objectAt(1); }, ".*"); - - ArrayView const arrVals2 = configData.getArray("array.[].sub2"); - ValueView const tempVal = arrVals2.valueAt(0); + // dies because higher only has 1 object (trying to access 2nd element) + EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getArray("higher").objectAt(1); }, ".*"); +} - // dies because array.[].sub2 only has 2 config values - EXPECT_DEATH([[maybe_unused]] auto _ = arrVals2.valueAt(2), ".*"); +TEST_F(ArrayViewDeathTest, AccessIndexOfWrongType) +{ + auto const& arrVals2 = configData.getArray("array.[].sub2"); + auto const& tempVal = arrVals2.valueAt(0); // dies as value is not of type int EXPECT_DEATH({ [[maybe_unused]] auto _ = tempVal.asIntType(); }, ".*"); } -TEST_F(ArrayViewDeathTest, IncorrectIterateAccess) +TEST_F(ArrayViewDeathTest, GetValueWhenItIsObject) { ArrayView const arr = configData.getArray("higher"); EXPECT_DEATH({ [[maybe_unused]] auto _ = arr.begin(); }, ".*"); +} +TEST_F(ArrayViewDeathTest, GetObjectWhenItIsValue) +{ ArrayView const dosguardWhitelist = configData.getArray("dosguard.whitelist"); EXPECT_DEATH({ [[maybe_unused]] auto _ = dosguardWhitelist.begin(); }, ".*"); } diff --git a/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp b/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp index d50ea2630..e92a7b38f 100644 --- a/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp +++ b/tests/unit/util/newconfig/ClioConfigDefinitionTests.cpp @@ -20,17 +20,27 @@ #include "util/newconfig/ArrayView.hpp" #include "util/newconfig/ConfigDefinition.hpp" #include "util/newconfig/ConfigDescription.hpp" -#include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/ConfigFileJson.hpp" #include "util/newconfig/FakeConfigData.hpp" +#include "util/newconfig/Types.hpp" +#include "util/newconfig/ValueView.hpp" +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include +#include using namespace util::config; -// TODO: parsing config file and populating into config will be here once implemented struct NewConfigTest : testing::Test { ClioConfigDefinition const configData = generateConfig(); }; @@ -45,12 +55,9 @@ TEST_F(NewConfigTest, fetchValues) EXPECT_EQ(true, configData.getValue("header.admin").asBool()); EXPECT_EQ("TSM", configData.getValue("header.sub.sub2Value").asString()); EXPECT_EQ(444.22, configData.getValue("ip").asDouble()); - - auto const v2 = configData.getValueInArray("dosguard.whitelist", 0); - EXPECT_EQ(v2.asString(), "125.5.5.2"); } -TEST_F(NewConfigTest, fetchObject) +TEST_F(NewConfigTest, fetchObjectDirectly) { auto const obj = configData.getObject("header"); EXPECT_TRUE(obj.containsKey("sub.sub2Value")); @@ -58,27 +65,6 @@ TEST_F(NewConfigTest, fetchObject) auto const obj2 = obj.getObject("sub"); EXPECT_TRUE(obj2.containsKey("sub2Value")); EXPECT_EQ(obj2.getValue("sub2Value").asString(), "TSM"); - - auto const objInArr = configData.getObject("array", 0); - auto const obj2InArr = configData.getObject("array", 1); - EXPECT_EQ(objInArr.getValue("sub").asDouble(), 111.11); - EXPECT_EQ(objInArr.getValue("sub2").asString(), "subCategory"); - EXPECT_EQ(obj2InArr.getValue("sub").asDouble(), 4321.55); - EXPECT_EQ(obj2InArr.getValue("sub2").asString(), "temporary"); -} - -TEST_F(NewConfigTest, fetchArray) -{ - auto const obj = configData.getObject("dosguard"); - EXPECT_TRUE(obj.containsKey("whitelist.[]")); - - auto const arr = obj.getArray("whitelist"); - EXPECT_EQ(2, arr.size()); - - auto const sameArr = configData.getArray("dosguard.whitelist"); - EXPECT_EQ(2, sameArr.size()); - EXPECT_EQ(sameArr.valueAt(0).asString(), arr.valueAt(0).asString()); - EXPECT_EQ(sameArr.valueAt(1).asString(), arr.valueAt(1).asString()); } TEST_F(NewConfigTest, CheckKeys) @@ -91,9 +77,11 @@ TEST_F(NewConfigTest, CheckKeys) EXPECT_TRUE(configData.hasItemsWithPrefix("dosguard")); EXPECT_TRUE(configData.hasItemsWithPrefix("ip")); - EXPECT_EQ(configData.arraySize("array"), 2); - EXPECT_EQ(configData.arraySize("higher"), 1); - EXPECT_EQ(configData.arraySize("dosguard.whitelist"), 2); + // all arrays currently not populated, only has "itemPattern_" that defines + // the type/constraint each configValue will have later on + EXPECT_EQ(configData.arraySize("array"), 0); + EXPECT_EQ(configData.arraySize("higher"), 0); + EXPECT_EQ(configData.arraySize("dosguard.whitelist"), 0); } TEST_F(NewConfigTest, CheckAllKeys) @@ -110,7 +98,10 @@ TEST_F(NewConfigTest, CheckAllKeys) "higher.[].low.section", "higher.[].low.admin", "dosguard.whitelist.[]", - "dosguard.port" + "dosguard.port", + "optional.withDefault", + "optional.withNoDefault", + "requireValue" }; for (auto i = configData.begin(); i != configData.end(); ++i) { @@ -121,31 +112,42 @@ TEST_F(NewConfigTest, CheckAllKeys) struct NewConfigDeathTest : NewConfigTest {}; -TEST_F(NewConfigDeathTest, IncorrectGetValues) +TEST_F(NewConfigDeathTest, GetNonExistentKeys) { - EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getValue("head"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getValue("head."); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getValue("asdf"); }, ".*"); +} + +TEST_F(NewConfigDeathTest, GetValueButIsArray) +{ EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getValue("dosguard.whitelist"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getValue("dosguard.whitelist.[]"); }, ".*"); } -TEST_F(NewConfigDeathTest, IncorrectGetObject) +TEST_F(NewConfigDeathTest, GetNonExistentObjectKey) { ASSERT_FALSE(configData.contains("head")); EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("head"); }, ".*"); + EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("doesNotExist"); }, ".*"); +} + +TEST_F(NewConfigDeathTest, GetObjectButIsArray) +{ EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("array"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("array", 2); }, ".*"); - EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("doesNotExist"); }, ".*"); } -TEST_F(NewConfigDeathTest, IncorrectGetArray) +TEST_F(NewConfigDeathTest, GetArrayButIsValue) { EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getArray("header.text1"); }, ".*"); +} + +TEST_F(NewConfigDeathTest, GetNonExistentArrayKey) +{ EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getArray("asdf"); }, ".*"); } -TEST(ConfigDescription, getValues) +TEST(ConfigDescription, GetValues) { ClioConfigDescription const definition{}; @@ -154,10 +156,150 @@ TEST(ConfigDescription, getValues) EXPECT_EQ(definition.get("prometheus.enabled"), "Enable or disable Prometheus metrics."); } -TEST(ConfigDescriptionAssertDeathTest, nonExistingKeyTest) +TEST(ConfigDescriptionAssertDeathTest, NonExistingKeyTest) { ClioConfigDescription const definition{}; EXPECT_DEATH({ [[maybe_unused]] auto a = definition.get("data"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto a = definition.get("etl_source.[]"); }, ".*"); } + +/** @brief Testing override the default values with the ones in Json */ +struct OverrideConfigVals : testing::Test { + OverrideConfigVals() + { + ConfigFileJson const jsonFileObj{boost::json::parse(JSONData).as_object()}; + auto const errors = configData.parse(jsonFileObj); + EXPECT_TRUE(!errors.has_value()); + } + ClioConfigDefinition configData = generateConfig(); +}; + +TEST_F(OverrideConfigVals, ValidateValuesStrings) +{ + // make sure the values in configData are overriden + EXPECT_TRUE(configData.contains("header.text1")); + EXPECT_EQ(configData.getValue("header.text1").asString(), "value"); + + EXPECT_FALSE(configData.contains("header.sub")); + EXPECT_TRUE(configData.contains("header.sub.sub2Value")); + EXPECT_EQ(configData.getValue("header.sub.sub2Value").asString(), "TSM"); + + EXPECT_TRUE(configData.contains("requireValue")); + EXPECT_EQ(configData.getValue("requireValue").asString(), "required"); +} + +TEST_F(OverrideConfigVals, ValidateValuesDouble) +{ + EXPECT_TRUE(configData.contains("optional.withDefault")); + EXPECT_EQ(configData.getValue("optional.withDefault").asDouble(), 0.0); + + // make sure the values not overwritten, (default values) are there too + EXPECT_TRUE(configData.contains("ip")); + EXPECT_EQ(configData.getValue("ip").asDouble(), 444.22); +} + +TEST_F(OverrideConfigVals, ValidateValuesInteger) +{ + EXPECT_TRUE(configData.contains("dosguard.port")); + EXPECT_EQ(configData.getValue("dosguard.port").asIntType(), 44444); + + EXPECT_TRUE(configData.contains("header.port")); + EXPECT_EQ(configData.getValue("header.port").asIntType(), 321); +} + +TEST_F(OverrideConfigVals, ValidateValuesBool) +{ + EXPECT_TRUE(configData.contains("header.admin")); + EXPECT_EQ(configData.getValue("header.admin").asBool(), false); +} + +TEST_F(OverrideConfigVals, ValidateIntegerValuesInArrays) +{ + // Check array values (sub) + EXPECT_TRUE(configData.contains("array.[].sub")); + auto const arrSub = configData.getArray("array.[].sub"); + + std::vector expectedArrSubVal{111.11, 4321.55, 5555.44}; + std::vector actualArrSubVal{}; + for (auto it = arrSub.begin(); it != arrSub.end(); ++it) { + actualArrSubVal.emplace_back((*it).asDouble()); + } + EXPECT_TRUE(std::ranges::equal(expectedArrSubVal, actualArrSubVal)); +} + +TEST_F(OverrideConfigVals, ValidateStringValuesInArrays) +{ + // Check array values (sub2) + EXPECT_TRUE(configData.contains("array.[].sub2")); + auto const arrSub2 = configData.getArray("array.[].sub2"); + + std::vector expectedArrSub2Val{"subCategory", "temporary", "london"}; + std::vector actualArrSub2Val{}; + for (auto it = arrSub2.begin(); it != arrSub2.end(); ++it) { + actualArrSub2Val.emplace_back((*it).asString()); + } + EXPECT_TRUE(std::ranges::equal(expectedArrSub2Val, actualArrSub2Val)); + + // Check dosguard values + EXPECT_TRUE(configData.contains("dosguard.whitelist.[]")); + auto const dosguard = configData.getArray("dosguard.whitelist.[]"); + EXPECT_EQ("125.5.5.1", dosguard.valueAt(0).asString()); + EXPECT_EQ("204.2.2.1", dosguard.valueAt(1).asString()); +} + +TEST_F(OverrideConfigVals, FetchArray) +{ + auto const obj = configData.getObject("dosguard"); + EXPECT_TRUE(obj.containsKey("whitelist.[]")); + + auto const arr = obj.getArray("whitelist"); + EXPECT_EQ(2, arr.size()); + + auto const sameArr = configData.getArray("dosguard.whitelist"); + EXPECT_EQ(2, sameArr.size()); + EXPECT_EQ(sameArr.valueAt(0).asString(), arr.valueAt(0).asString()); + EXPECT_EQ(sameArr.valueAt(1).asString(), arr.valueAt(1).asString()); +} + +TEST_F(OverrideConfigVals, FetchObjectByArray) +{ + auto const objInArr = configData.getObject("array", 0); + auto const obj2InArr = configData.getObject("array", 1); + auto const obj3InArr = configData.getObject("array", 2); + + EXPECT_EQ(objInArr.getValue("sub").asDouble(), 111.11); + EXPECT_EQ(objInArr.getValue("sub2").asString(), "subCategory"); + EXPECT_EQ(obj2InArr.getValue("sub").asDouble(), 4321.55); + EXPECT_EQ(obj2InArr.getValue("sub2").asString(), "temporary"); + EXPECT_EQ(obj3InArr.getValue("sub").asDouble(), 5555.44); + EXPECT_EQ(obj3InArr.getValue("sub2").asString(), "london"); +} + +struct IncorrectOverrideValues : testing::Test { + ClioConfigDefinition configData = generateConfig(); +}; + +TEST_F(IncorrectOverrideValues, InvalidJsonErrors) +{ + ConfigFileJson const jsonFileObj{boost::json::parse(invalidJSONData).as_object()}; + auto const errors = configData.parse(jsonFileObj); + EXPECT_TRUE(errors.has_value()); + + // Expected error messages + std::unordered_set expectedErrors{ + "dosguard.whitelist.[] value does not match type string", + "higher.[].low.section key is required in user Config", + "higher.[].low.admin key is required in user Config", + "array.[].sub key is required in user Config", + "header.port value does not match type integer", + "header.admin value does not match type boolean", + "optional.withDefault value does not match type double" + }; + + std::unordered_set actualErrors; + for (auto const& error : errors.value()) { + actualErrors.insert(error.error); + } + EXPECT_EQ(expectedErrors, actualErrors); +} diff --git a/tests/unit/util/newconfig/ConfigValueTests.cpp b/tests/unit/util/newconfig/ConfigValueTests.cpp index e60572b3f..6ed1a6f1d 100644 --- a/tests/unit/util/newconfig/ConfigValueTests.cpp +++ b/tests/unit/util/newconfig/ConfigValueTests.cpp @@ -17,24 +17,207 @@ */ //============================================================================== +#include "util/newconfig/ConfigConstraints.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Types.hpp" +#include #include +#include +#include + using namespace util::config; -TEST(ConfigValue, testConfigValue) +TEST(ConfigValue, GetSetString) { - auto cvStr = ConfigValue{ConfigType::String}.defaultValue("12345"); + auto const cvStr = ConfigValue{ConfigType::String}.defaultValue("12345"); EXPECT_EQ(cvStr.type(), ConfigType::String); EXPECT_TRUE(cvStr.hasValue()); EXPECT_FALSE(cvStr.isOptional()); +} - auto cvInt = ConfigValue{ConfigType::Integer}.defaultValue(543); +TEST(ConfigValue, GetSetInteger) +{ + auto const cvInt = ConfigValue{ConfigType::Integer}.defaultValue(543); EXPECT_EQ(cvInt.type(), ConfigType::Integer); - EXPECT_TRUE(cvStr.hasValue()); - EXPECT_FALSE(cvStr.isOptional()); + EXPECT_TRUE(cvInt.hasValue()); + EXPECT_FALSE(cvInt.isOptional()); - auto cvOpt = ConfigValue{ConfigType::Integer}.optional(); + auto const cvOpt = ConfigValue{ConfigType::Integer}.optional(); EXPECT_TRUE(cvOpt.isOptional()); } + +// A test for each constraint so it's easy to change in the future +TEST(ConfigValue, PortConstraint) +{ + auto const portConstraint{PortConstraint{}}; + EXPECT_FALSE(portConstraint.checkConstraint(4444).has_value()); + EXPECT_TRUE(portConstraint.checkConstraint(99999).has_value()); +} + +TEST(ConfigValue, SetValuesOnPortConstraint) +{ + auto cvPort = ConfigValue{ConfigType::Integer}.defaultValue(4444).withConstraint(validatePort); + auto const err = cvPort.setValue(99999); + EXPECT_TRUE(err.has_value()); + EXPECT_EQ(err->error, "Port does not satisfy the constraint bounds"); + EXPECT_TRUE(cvPort.setValue(33.33).has_value()); + EXPECT_TRUE(cvPort.setValue(33.33).value().error == "value does not match type integer"); + EXPECT_FALSE(cvPort.setValue(1).has_value()); + + auto cvPort2 = ConfigValue{ConfigType::String}.defaultValue("4444").withConstraint(validatePort); + auto const strPortError = cvPort2.setValue("100000"); + EXPECT_TRUE(strPortError.has_value()); + EXPECT_EQ(strPortError->error, "Port does not satisfy the constraint bounds"); +} + +TEST(ConfigValue, OneOfConstraintOneValue) +{ + std::array const arr = {"tracer"}; + auto const databaseConstraint{OneOf{"database.type", arr}}; + EXPECT_FALSE(databaseConstraint.checkConstraint("tracer").has_value()); + + EXPECT_TRUE(databaseConstraint.checkConstraint(345).has_value()); + EXPECT_EQ(databaseConstraint.checkConstraint(345)->error, R"(Key "database.type"'s value must be a string)"); + + EXPECT_TRUE(databaseConstraint.checkConstraint("123.44").has_value()); + EXPECT_EQ( + databaseConstraint.checkConstraint("123.44")->error, + R"(You provided value "123.44". Key "database.type"'s value must be one of the following: tracer)" + ); +} + +TEST(ConfigValue, OneOfConstraint) +{ + std::array const arr = {"123", "trace", "haha"}; + auto const oneOfCons{OneOf{"log_level", arr}}; + + EXPECT_FALSE(oneOfCons.checkConstraint("trace").has_value()); + + EXPECT_TRUE(oneOfCons.checkConstraint(345).has_value()); + EXPECT_EQ(oneOfCons.checkConstraint(345)->error, R"(Key "log_level"'s value must be a string)"); + + EXPECT_TRUE(oneOfCons.checkConstraint("PETER_WAS_HERE").has_value()); + EXPECT_EQ( + oneOfCons.checkConstraint("PETER_WAS_HERE")->error, + R"(You provided value "PETER_WAS_HERE". Key "log_level"'s value must be one of the following: 123, trace, haha)" + ); +} + +TEST(ConfigValue, IpConstraint) +{ + auto ip = ConfigValue{ConfigType::String}.defaultValue("127.0.0.1").withConstraint(validateIP); + EXPECT_FALSE(ip.setValue("http://127.0.0.1").has_value()); + EXPECT_FALSE(ip.setValue("http://127.0.0.1.com").has_value()); + auto const err = ip.setValue("123.44"); + EXPECT_TRUE(err.has_value()); + EXPECT_EQ(err->error, "Ip is not a valid ip address"); + EXPECT_FALSE(ip.setValue("126.0.0.2")); + + EXPECT_TRUE(ip.setValue("644.3.3.0")); + EXPECT_TRUE(ip.setValue("127.0.0.1.0")); + EXPECT_TRUE(ip.setValue("")); + EXPECT_TRUE(ip.setValue("http://example..com")); + EXPECT_FALSE(ip.setValue("localhost")); + EXPECT_FALSE(ip.setValue("http://example.com:8080/path")); +} + +TEST(ConfigValue, positiveNumConstraint) +{ + auto const numCons{NumberValueConstraint{0, 5}}; + EXPECT_FALSE(numCons.checkConstraint(0)); + EXPECT_FALSE(numCons.checkConstraint(5)); + + EXPECT_TRUE(numCons.checkConstraint(true)); + EXPECT_EQ(numCons.checkConstraint(true)->error, fmt::format("Number must be of type integer")); + + EXPECT_TRUE(numCons.checkConstraint(8)); + EXPECT_EQ(numCons.checkConstraint(8)->error, fmt::format("Number must be between {} and {}", 0, 5)); +} + +TEST(ConfigValue, SetValuesOnNumberConstraint) +{ + auto positiveNum = ConfigValue{ConfigType::Integer}.defaultValue(20u).withConstraint(validateUint16); + auto const err = positiveNum.setValue(-22, "key"); + EXPECT_TRUE(err.has_value()); + EXPECT_EQ(err->error, fmt::format("key Number must be between {} and {}", 0, 65535)); + EXPECT_FALSE(positiveNum.setValue(99, "key")); +} + +TEST(ConfigValue, PositiveDoubleConstraint) +{ + auto const doubleCons{PositiveDouble{}}; + EXPECT_FALSE(doubleCons.checkConstraint(0.2)); + EXPECT_FALSE(doubleCons.checkConstraint(5.54)); + EXPECT_TRUE(doubleCons.checkConstraint("-5")); + EXPECT_EQ(doubleCons.checkConstraint("-5")->error, "Double number must be of type int or double"); + EXPECT_EQ(doubleCons.checkConstraint(-5.6)->error, "Double number must be greater than 0"); + EXPECT_FALSE(doubleCons.checkConstraint(12.1)); +} + +struct ConstraintTestBundle { + std::string name; + Constraint const& cons_; +}; + +struct ConstraintDeathTest : public testing::Test, public testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P( + EachConstraints, + ConstraintDeathTest, + testing::Values( + ConstraintTestBundle{"logTagConstraint", validateLogTag}, + ConstraintTestBundle{"portConstraint", validatePort}, + ConstraintTestBundle{"ipConstraint", validateIP}, + ConstraintTestBundle{"channelConstraint", validateChannelName}, + ConstraintTestBundle{"logLevelConstraint", validateLogLevelName}, + ConstraintTestBundle{"cannsandraNameCnstraint", validateCassandraName}, + ConstraintTestBundle{"loadModeConstraint", validateLoadMode}, + ConstraintTestBundle{"ChannelNameConstraint", validateChannelName}, + ConstraintTestBundle{"ApiVersionConstraint", validateApiVersion}, + ConstraintTestBundle{"Uint16Constraint", validateUint16}, + ConstraintTestBundle{"Uint32Constraint", validateUint32}, + ConstraintTestBundle{"PositiveDoubleConstraint", validatePositiveDouble} + ), + [](testing::TestParamInfo const& info) { return info.param.name; } +); + +TEST_P(ConstraintDeathTest, TestEachConstraint) +{ + EXPECT_DEATH( + { + [[maybe_unused]] auto const a = + ConfigValue{ConfigType::Boolean}.defaultValue(true).withConstraint(GetParam().cons_); + }, + ".*" + ); +} + +TEST(ConfigValueDeathTest, SetInvalidValueTypeStringAndBool) +{ + EXPECT_DEATH( + { + [[maybe_unused]] auto a = ConfigValue{ConfigType::String}.defaultValue(33).withConstraint(validateLoadMode); + }, + ".*" + ); + EXPECT_DEATH({ [[maybe_unused]] auto a = ConfigValue{ConfigType::Boolean}.defaultValue(-66); }, ".*"); +} + +TEST(ConfigValueDeathTest, OutOfBounceIntegerConstraint) +{ + EXPECT_DEATH( + { + [[maybe_unused]] auto a = + ConfigValue{ConfigType::Integer}.defaultValue(999999).withConstraint(validateUint16); + }, + ".*" + ); + EXPECT_DEATH( + { + [[maybe_unused]] auto a = ConfigValue{ConfigType::Integer}.defaultValue(-66).withConstraint(validateUint32); + }, + ".*" + ); +} diff --git a/tests/unit/util/newconfig/JsonConfigFileTests.cpp b/tests/unit/util/newconfig/JsonConfigFileTests.cpp new file mode 100644 index 000000000..8c64a5f0e --- /dev/null +++ b/tests/unit/util/newconfig/JsonConfigFileTests.cpp @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/TmpFile.hpp" +#include "util/newconfig/ConfigFileJson.hpp" +#include "util/newconfig/FakeConfigData.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +TEST(CreateConfigFile, filePath) +{ + auto const jsonFileObj = ConfigFileJson::make_ConfigFileJson(TmpFile(JSONData).path); + EXPECT_TRUE(jsonFileObj.has_value()); + + EXPECT_TRUE(jsonFileObj->containsKey("array.[].sub")); + auto const arrSub = jsonFileObj->getArray("array.[].sub"); + EXPECT_EQ(arrSub.size(), 3); +} + +TEST(CreateConfigFile, incorrectFilePath) +{ + auto const jsonFileObj = util::config::ConfigFileJson::make_ConfigFileJson("123/clio"); + EXPECT_FALSE(jsonFileObj.has_value()); +} + +struct ParseJson : testing::Test { + ParseJson() : jsonFileObj{boost::json::parse(JSONData).as_object()} + { + } + + ConfigFileJson const jsonFileObj; +}; + +TEST_F(ParseJson, validateValues) +{ + EXPECT_TRUE(jsonFileObj.containsKey("header.text1")); + EXPECT_EQ(std::get(jsonFileObj.getValue("header.text1")), "value"); + + EXPECT_TRUE(jsonFileObj.containsKey("header.sub.sub2Value")); + EXPECT_EQ(std::get(jsonFileObj.getValue("header.sub.sub2Value")), "TSM"); + + EXPECT_TRUE(jsonFileObj.containsKey("dosguard.port")); + EXPECT_EQ(std::get(jsonFileObj.getValue("dosguard.port")), 44444); + + EXPECT_FALSE(jsonFileObj.containsKey("idk")); + EXPECT_FALSE(jsonFileObj.containsKey("optional.withNoDefault")); +} + +TEST_F(ParseJson, validateArrayValue) +{ + // validate array.[].sub matches expected values + EXPECT_TRUE(jsonFileObj.containsKey("array.[].sub")); + auto const arrSub = jsonFileObj.getArray("array.[].sub"); + EXPECT_EQ(arrSub.size(), 3); + + std::vector expectedArrSubVal{111.11, 4321.55, 5555.44}; + std::vector actualArrSubVal{}; + + for (auto it = arrSub.begin(); it != arrSub.end(); ++it) { + ASSERT_TRUE(std::holds_alternative(*it)); + actualArrSubVal.emplace_back(std::get(*it)); + } + EXPECT_TRUE(std::ranges::equal(expectedArrSubVal, actualArrSubVal)); + + // validate array.[].sub2 matches expected values + EXPECT_TRUE(jsonFileObj.containsKey("array.[].sub2")); + auto const arrSub2 = jsonFileObj.getArray("array.[].sub2"); + EXPECT_EQ(arrSub2.size(), 3); + std::vector expectedArrSub2Val{"subCategory", "temporary", "london"}; + std::vector actualArrSub2Val{}; + + for (auto it = arrSub2.begin(); it != arrSub2.end(); ++it) { + ASSERT_TRUE(std::holds_alternative(*it)); + actualArrSub2Val.emplace_back(std::get(*it)); + } + EXPECT_TRUE(std::ranges::equal(expectedArrSub2Val, actualArrSub2Val)); + + EXPECT_TRUE(jsonFileObj.containsKey("dosguard.whitelist.[]")); + auto const whitelistArr = jsonFileObj.getArray("dosguard.whitelist.[]"); + EXPECT_EQ(whitelistArr.size(), 2); + EXPECT_EQ("125.5.5.1", std::get(whitelistArr.at(0))); + EXPECT_EQ("204.2.2.1", std::get(whitelistArr.at(1))); +} + +struct JsonValueDeathTest : ParseJson {}; + +TEST_F(JsonValueDeathTest, invalidGetArray) +{ + EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("header.text1"), ".*"); +} diff --git a/tests/unit/util/newconfig/ObjectViewTests.cpp b/tests/unit/util/newconfig/ObjectViewTests.cpp index afe28fc43..6e0588947 100644 --- a/tests/unit/util/newconfig/ObjectViewTests.cpp +++ b/tests/unit/util/newconfig/ObjectViewTests.cpp @@ -19,15 +19,23 @@ #include "util/newconfig/ArrayView.hpp" #include "util/newconfig/ConfigDefinition.hpp" +#include "util/newconfig/ConfigFileJson.hpp" #include "util/newconfig/FakeConfigData.hpp" #include "util/newconfig/ObjectView.hpp" +#include #include using namespace util::config; struct ObjectViewTest : testing::Test { - ClioConfigDefinition const configData = generateConfig(); + ObjectViewTest() + { + ConfigFileJson const jsonFileObj{boost::json::parse(JSONData).as_object()}; + auto const errors = configData.parse(jsonFileObj); + EXPECT_TRUE(!errors.has_value()); + } + ClioConfigDefinition configData = generateConfig(); }; TEST_F(ObjectViewTest, ObjectValueTest) @@ -39,14 +47,14 @@ TEST_F(ObjectViewTest, ObjectValueTest) EXPECT_TRUE(headerObj.containsKey("admin")); EXPECT_EQ("value", headerObj.getValue("text1").asString()); - EXPECT_EQ(123, headerObj.getValue("port").asIntType()); - EXPECT_EQ(true, headerObj.getValue("admin").asBool()); + EXPECT_EQ(321, headerObj.getValue("port").asIntType()); + EXPECT_EQ(false, headerObj.getValue("admin").asBool()); } -TEST_F(ObjectViewTest, ObjectInArray) +TEST_F(ObjectViewTest, ObjectValuesInArray) { ArrayView const arr = configData.getArray("array"); - EXPECT_EQ(arr.size(), 2); + EXPECT_EQ(arr.size(), 3); ObjectView const firstObj = arr.objectAt(0); ObjectView const secondObj = arr.objectAt(1); EXPECT_TRUE(firstObj.containsKey("sub")); @@ -62,7 +70,7 @@ TEST_F(ObjectViewTest, ObjectInArray) EXPECT_EQ(secondObj.getValue("sub2").asString(), "temporary"); } -TEST_F(ObjectViewTest, ObjectInArrayMoreComplex) +TEST_F(ObjectViewTest, GetObjectsInDifferentWays) { ArrayView const arr = configData.getArray("higher"); ASSERT_EQ(1, arr.size()); @@ -78,7 +86,7 @@ TEST_F(ObjectViewTest, ObjectInArrayMoreComplex) ObjectView const objLow = firstObj.getObject("low"); EXPECT_TRUE(objLow.containsKey("section")); EXPECT_TRUE(objLow.containsKey("admin")); - EXPECT_EQ(objLow.getValue("section").asString(), "true"); + EXPECT_EQ(objLow.getValue("section").asString(), "WebServer"); EXPECT_EQ(objLow.getValue("admin").asBool(), false); } @@ -90,18 +98,25 @@ TEST_F(ObjectViewTest, getArrayInObject) auto const arr = obj.getArray("whitelist"); EXPECT_EQ(2, arr.size()); - EXPECT_EQ("125.5.5.2", arr.valueAt(0).asString()); - EXPECT_EQ("204.2.2.2", arr.valueAt(1).asString()); + EXPECT_EQ("125.5.5.1", arr.valueAt(0).asString()); + EXPECT_EQ("204.2.2.1", arr.valueAt(1).asString()); } struct ObjectViewDeathTest : ObjectViewTest {}; -TEST_F(ObjectViewDeathTest, incorrectKeys) +TEST_F(ObjectViewDeathTest, KeyDoesNotExist) { - EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("header.text1"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("head"); }, ".*"); +} + +TEST_F(ObjectViewDeathTest, KeyIsValueView) +{ + EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("header.text1"); }, ".*"); EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getArray("header"); }, ".*"); +} +TEST_F(ObjectViewDeathTest, KeyisArrayView) +{ // dies because only 1 object in higher.[].low EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("higher.[].low", 1); }, ".*"); } diff --git a/tests/unit/util/newconfig/ValueViewTests.cpp b/tests/unit/util/newconfig/ValueViewTests.cpp index 7d8f1a0a0..bcde0e16e 100644 --- a/tests/unit/util/newconfig/ValueViewTests.cpp +++ b/tests/unit/util/newconfig/ValueViewTests.cpp @@ -20,6 +20,7 @@ #include "util/newconfig/ConfigDefinition.hpp" #include "util/newconfig/ConfigValue.hpp" #include "util/newconfig/FakeConfigData.hpp" +#include "util/newconfig/Types.hpp" #include "util/newconfig/ValueView.hpp" #include