mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-20 03:35:55 +00:00
refactor: Clio Config (#1593)
Add constraint + parse json into Config Second part of refactoring Clio Config; First PR found [here](https://github.com/XRPLF/clio/pull/1544) Steps that are left to implement: - Replacing all the places where we fetch config values (by using config.valueOr/MaybeValue) to instead get it from Config Definition - Generate markdown file using Clio Config Description
This commit is contained in:
@@ -25,9 +25,12 @@ target_sources(
|
||||
TimeUtils.cpp
|
||||
TxUtils.cpp
|
||||
LedgerUtils.cpp
|
||||
newconfig/ConfigDefinition.cpp
|
||||
newconfig/ObjectView.cpp
|
||||
newconfig/Array.cpp
|
||||
newconfig/ArrayView.cpp
|
||||
newconfig/ConfigConstraints.cpp
|
||||
newconfig/ConfigDefinition.cpp
|
||||
newconfig/ConfigFileJson.cpp
|
||||
newconfig/ObjectView.cpp
|
||||
newconfig/ValueView.cpp
|
||||
)
|
||||
|
||||
|
||||
84
src/util/newconfig/Array.cpp
Normal file
84
src/util/newconfig/Array.cpp
Normal file
@@ -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 <cstddef>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace util::config {
|
||||
|
||||
Array::Array(ConfigValue arg) : itemPattern_{std::move(arg)}
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
Array::addValue(Value value, std::optional<std::string_view> 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<ConfigValue>::const_iterator
|
||||
Array::begin() const
|
||||
{
|
||||
return elements_.begin();
|
||||
}
|
||||
|
||||
std::vector<ConfigValue>::const_iterator
|
||||
Array::end() const
|
||||
{
|
||||
return elements_.end();
|
||||
}
|
||||
|
||||
} // namespace util::config
|
||||
@@ -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 <cstddef>
|
||||
#include <iterator>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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 <typename... Args>
|
||||
constexpr Array(Args&&... args) : elements_{std::forward<Args>(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<Error>
|
||||
addValue(Value value, std::optional<std::string_view> key = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief Returns the number of values stored in the Array
|
||||
@@ -67,10 +62,7 @@ public:
|
||||
* @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 @@ public:
|
||||
* @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<ConfigValue>::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<ConfigValue>::const_iterator
|
||||
end() const;
|
||||
|
||||
private:
|
||||
ConfigValue itemPattern_;
|
||||
std::vector<ConfigValue> elements_;
|
||||
};
|
||||
|
||||
|
||||
105
src/util/newconfig/ConfigConstraints.cpp
Normal file
105
src/util/newconfig/ConfigConstraints.cpp
Normal file
@@ -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 <fmt/core.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace util::config {
|
||||
|
||||
std::optional<Error>
|
||||
PortConstraint::checkTypeImpl(Value const& port) const
|
||||
{
|
||||
if (!(std::holds_alternative<int64_t>(port) || std::holds_alternative<std::string>(port)))
|
||||
return Error{"Port must be a string or integer"};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
PortConstraint::checkValueImpl(Value const& port) const
|
||||
{
|
||||
uint32_t p = 0;
|
||||
if (std::holds_alternative<std::string>(port)) {
|
||||
try {
|
||||
p = static_cast<uint32_t>(std::stoi(std::get<std::string>(port)));
|
||||
} catch (std::invalid_argument const& e) {
|
||||
return Error{"Port string must be an integer."};
|
||||
}
|
||||
} else {
|
||||
p = static_cast<uint32_t>(std::get<int64_t>(port));
|
||||
}
|
||||
if (p >= portMin && p <= portMax)
|
||||
return std::nullopt;
|
||||
return Error{"Port does not satisfy the constraint bounds"};
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
ValidIPConstraint::checkTypeImpl(Value const& ip) const
|
||||
{
|
||||
if (!std::holds_alternative<std::string>(ip))
|
||||
return Error{"Ip value must be a string"};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
ValidIPConstraint::checkValueImpl(Value const& ip) const
|
||||
{
|
||||
if (std::get<std::string>(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<std::string>(ip), ipv4) || std::regex_match(std::get<std::string>(ip), ip_url))
|
||||
return std::nullopt;
|
||||
|
||||
return Error{"Ip is not a valid ip address"};
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
PositiveDouble::checkTypeImpl(Value const& num) const
|
||||
{
|
||||
if (!(std::holds_alternative<double>(num) || std::holds_alternative<int64_t>(num)))
|
||||
return Error{"Double number must be of type int or double"};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
PositiveDouble::checkValueImpl(Value const& num) const
|
||||
{
|
||||
if (std::get<double>(num) >= 0)
|
||||
return std::nullopt;
|
||||
return Error{"Double number must be greater than 0"};
|
||||
}
|
||||
|
||||
} // namespace util::config
|
||||
376
src/util/newconfig/ConfigConstraints.hpp
Normal file
376
src/util/newconfig/ConfigConstraints.hpp
Normal file
@@ -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 <fmt/core.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace util::config {
|
||||
class ValueView;
|
||||
class ConfigValue;
|
||||
|
||||
/**
|
||||
* @brief specific values that are accepted for logger levels in config.
|
||||
*/
|
||||
static constexpr std::array<char const*, 7> 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<char const*, 5> LOG_TAGS = {
|
||||
"int",
|
||||
"uint",
|
||||
"null",
|
||||
"none",
|
||||
"uuid",
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief specific values that are accepted for cache loading in config.
|
||||
*/
|
||||
static constexpr std::array<char const*, 3> LOAD_CACHE_MODE = {
|
||||
"sync",
|
||||
"async",
|
||||
"none",
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief specific values that are accepted for database type in config.
|
||||
*/
|
||||
static constexpr std::array<char const*, 1> 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<Error>
|
||||
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 <std::size_t arrSize>
|
||||
constexpr std::string
|
||||
makeErrorMsg(std::string_view key, Value const& value, std::array<char const*, arrSize> 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<Error>
|
||||
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<Error>
|
||||
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<Error>
|
||||
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<Error>
|
||||
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<Error>
|
||||
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<Error>
|
||||
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 <std::size_t arrSize>
|
||||
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<char const*, arrSize> 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<Error>
|
||||
checkTypeImpl(Value const& val) const override
|
||||
{
|
||||
if (!std::holds_alternative<std::string>(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<Error>
|
||||
checkValueImpl(Value const& val) const override
|
||||
{
|
||||
namespace rg = std::ranges;
|
||||
auto const check = [&val](std::string_view name) { return std::get<std::string>(val) == name; };
|
||||
if (rg::any_of(arr_, check))
|
||||
return std::nullopt;
|
||||
|
||||
return Error{makeErrorMsg(key_, val, arr_)};
|
||||
}
|
||||
|
||||
std::string_view key_;
|
||||
std::array<char const*, arrSize> arr_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A constraint class to ensure an integer value is between two numbers (inclusive)
|
||||
*/
|
||||
template <typename numType>
|
||||
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<Error>
|
||||
checkTypeImpl(Value const& num) const override
|
||||
{
|
||||
if (!std::holds_alternative<int64_t>(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<Error>
|
||||
checkValueImpl(Value const& num) const override
|
||||
{
|
||||
auto const numValue = std::get<int64_t>(num);
|
||||
if (numValue >= static_cast<int64_t>(min_) && numValue <= static_cast<int64_t>(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<Error>
|
||||
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<Error>
|
||||
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<uint16_t> validateUint16{
|
||||
std::numeric_limits<uint16_t>::min(),
|
||||
std::numeric_limits<uint16_t>::max()
|
||||
};
|
||||
static constexpr NumberValueConstraint<uint32_t> validateUint32{
|
||||
std::numeric_limits<uint32_t>::min(),
|
||||
std::numeric_limits<uint32_t>::max()
|
||||
};
|
||||
static constexpr NumberValueConstraint<uint32_t> validateApiVersion{rpc::API_VERSION_MIN, rpc::API_VERSION_MAX};
|
||||
|
||||
} // namespace util::config
|
||||
@@ -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 <fmt/core.h>
|
||||
@@ -38,6 +43,7 @@
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
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<uint32_t>(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<uint32_t>(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<KeyValuePair> p
|
||||
{
|
||||
for (auto const& [key, value] : pair) {
|
||||
if (key.contains("[]"))
|
||||
ASSERT(std::holds_alternative<Array>(value), "Value must be array if key has \"[]\"");
|
||||
ASSERT(std::holds_alternative<Array>(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<std::vector<Error>>
|
||||
ClioConfigDefinition::parse(ConfigFileInterface const& config)
|
||||
{
|
||||
std::vector<Error> 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<ConfigValue>(value)) {
|
||||
if (!(std::get<ConfigValue>(value).isOptional() || std::get<ConfigValue>(value).hasValue()))
|
||||
listOfErrors.emplace_back(key, "key is required in user Config");
|
||||
} else if (std::holds_alternative<Array>(value)) {
|
||||
if (!(std::get<Array>(value).getArrayPattern().isOptional()))
|
||||
listOfErrors.emplace_back(key, "key is required in user Config");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ASSERT(
|
||||
std::holds_alternative<ConfigValue>(value) || std::holds_alternative<Array>(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
|
||||
|
||||
@@ -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 <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace util::config {
|
||||
|
||||
@@ -66,12 +67,13 @@ public:
|
||||
/**
|
||||
* @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<Error>
|
||||
[[nodiscard]] std::optional<std::vector<Error>>
|
||||
parse(ConfigFileInterface const& config);
|
||||
|
||||
/**
|
||||
@@ -80,9 +82,9 @@ public:
|
||||
* 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<Error>
|
||||
[[nodiscard]] std::optional<std::vector<Error>>
|
||||
validate(ConfigFileInterface const& config) const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,7 +90,9 @@ private:
|
||||
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."},
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/newconfig/ConfigValue.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<ConfigValue>
|
||||
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<std::vector<ConfigValue>>
|
||||
virtual std::vector<Value>
|
||||
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
|
||||
|
||||
166
src/util/newconfig/ConfigFileJson.cpp
Normal file
166
src/util/newconfig/ConfigFileJson.cpp
Normal file
@@ -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 <boost/filesystem/path.hpp>
|
||||
#include <boost/json/array.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/parse_options.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iostream>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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, Error>
|
||||
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>(
|
||||
Error{fmt::format("Could not open configuration file '{}'", configFilePath.string())}
|
||||
);
|
||||
|
||||
} catch (std::exception const& e) {
|
||||
return std::unexpected<Error>(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<Value>
|
||||
ConfigFileJson::getArray(std::string_view key) const
|
||||
{
|
||||
ASSERT(jsonObject_.at(key).is_array(), "Key {} has value that is not an array", key);
|
||||
|
||||
std::vector<Value> 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
|
||||
100
src/util/newconfig/ConfigFileJson.hpp
Normal file
100
src/util/newconfig/ConfigFileJson.hpp
Normal file
@@ -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 <boost/filesystem/path.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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<Value>
|
||||
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<ConfigFileJson, Error>
|
||||
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
|
||||
@@ -19,15 +19,31 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "util/newconfig/ConfigFileInterface.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// 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<Value>
|
||||
getArray(std::string_view key) const override;
|
||||
|
||||
bool
|
||||
containsKey(std::string_view key) const override;
|
||||
};
|
||||
|
||||
} // namespace util::config
|
||||
@@ -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 <fmt/core.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
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 <typename Type>
|
||||
constexpr ConfigType
|
||||
getType()
|
||||
{
|
||||
if constexpr (std::is_same_v<Type, int64_t>) {
|
||||
return ConfigType::Integer;
|
||||
} else if constexpr (std::is_same_v<Type, std::string>) {
|
||||
return ConfigType::String;
|
||||
} else if constexpr (std::is_same_v<Type, double>) {
|
||||
return ConfigType::Double;
|
||||
} else if constexpr (std::is_same_v<Type, bool>) {
|
||||
return ConfigType::Boolean;
|
||||
} else {
|
||||
static_assert(util::Unsupported<Type>, "Wrong config type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Represents the config values for Json/Yaml config
|
||||
*
|
||||
@@ -65,8 +45,6 @@ getType()
|
||||
*/
|
||||
class ConfigValue {
|
||||
public:
|
||||
using Type = std::variant<int64_t, std::string, bool, double>;
|
||||
|
||||
/**
|
||||
* @brief Constructor initializing with the config type
|
||||
*
|
||||
@@ -83,12 +61,92 @@ public:
|
||||
* @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 Sets the value current ConfigValue given by the User's defined value
|
||||
*
|
||||
* @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]] std::optional<Error>
|
||||
setValue(Value value, std::optional<std::string_view> key = std::nullopt)
|
||||
{
|
||||
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 Assigns a constraint to the 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&
|
||||
withConstraint(Constraint const& cons)
|
||||
{
|
||||
cons_ = std::reference_wrapper<Constraint const>(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 Retrieves the constraint associated with this ConfigValue, if any.
|
||||
*
|
||||
* @return An optional reference to the associated Constraint.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::reference_wrapper<Constraint const>>
|
||||
getConstraint() const
|
||||
{
|
||||
return cons_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Gets the config type
|
||||
*
|
||||
@@ -100,32 +158,6 @@ public:
|
||||
return type_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the minimum value for the config
|
||||
*
|
||||
* @param min The minimum value
|
||||
* @return Reference to this ConfigValue
|
||||
*/
|
||||
[[nodiscard]] constexpr ConfigValue&
|
||||
min(std::uint32_t min)
|
||||
{
|
||||
min_ = min;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum value for the config
|
||||
*
|
||||
* @param max The maximum value
|
||||
* @return Reference to this ConfigValue
|
||||
*/
|
||||
[[nodiscard]] constexpr ConfigValue&
|
||||
max(std::uint32_t max)
|
||||
{
|
||||
max_ = max;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the config value as optional, meaning the user doesn't have to provide the value in their config
|
||||
*
|
||||
@@ -165,7 +197,7 @@ public:
|
||||
*
|
||||
* @return Config Value
|
||||
*/
|
||||
[[nodiscard]] Type const&
|
||||
[[nodiscard]] Value const&
|
||||
getValue() const
|
||||
{
|
||||
return value_.value();
|
||||
@@ -178,39 +210,28 @@ private:
|
||||
* @param type The config type
|
||||
* @param value The config value
|
||||
*/
|
||||
static void
|
||||
checkTypeConsistency(ConfigType type, Type value)
|
||||
static std::optional<Error>
|
||||
checkTypeConsistency(ConfigType type, Value value)
|
||||
{
|
||||
if (std::holds_alternative<std::string>(value)) {
|
||||
ASSERT(type == ConfigType::String, "Value does not match type string");
|
||||
} else if (std::holds_alternative<bool>(value)) {
|
||||
ASSERT(type == ConfigType::Boolean, "Value does not match type boolean");
|
||||
} else if (std::holds_alternative<double>(value)) {
|
||||
ASSERT(type == ConfigType::Double, "Value does not match type double");
|
||||
} else if (std::holds_alternative<int64_t>(value)) {
|
||||
ASSERT(type == ConfigType::Integer, "Value does not match type integer");
|
||||
if (type == ConfigType::String && !std::holds_alternative<std::string>(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<bool>(value)) {
|
||||
return Error{"value does not match type boolean"};
|
||||
}
|
||||
if (type == ConfigType::Double && !std::holds_alternative<double>(value)) {
|
||||
return Error{"value does not match type double"};
|
||||
}
|
||||
if (type == ConfigType::Integer && !std::holds_alternative<int64_t>(value)) {
|
||||
return Error{"value does not match type integer"};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ConfigType type_{};
|
||||
bool optional_{false};
|
||||
std::optional<Type> value_;
|
||||
std::optional<std::uint32_t> min_;
|
||||
std::optional<std::uint32_t> max_;
|
||||
std::optional<Value> value_;
|
||||
std::optional<std::reference_wrapper<Constraint const>> cons_;
|
||||
};
|
||||
|
||||
} // namespace util::config
|
||||
|
||||
57
src/util/newconfig/Error.hpp
Normal file
57
src/util/newconfig/Error.hpp
Normal file
@@ -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 <fmt/core.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
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
|
||||
60
src/util/newconfig/Types.hpp
Normal file
60
src/util/newconfig/Types.hpp
Normal file
@@ -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 <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
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<int64_t, std::string, bool, double>;
|
||||
|
||||
/**
|
||||
* @brief Get the corresponding clio config type
|
||||
*
|
||||
* @tparam Type The type to get the corresponding ConfigType for
|
||||
* @return The corresponding ConfigType
|
||||
*/
|
||||
template <typename Type>
|
||||
constexpr ConfigType
|
||||
getType()
|
||||
{
|
||||
if constexpr (std::is_same_v<Type, int64_t>) {
|
||||
return ConfigType::Integer;
|
||||
} else if constexpr (std::is_same_v<Type, std::string>) {
|
||||
return ConfigType::String;
|
||||
} else if constexpr (std::is_same_v<Type, double>) {
|
||||
return ConfigType::Double;
|
||||
} else if constexpr (std::is_same_v<Type, bool>) {
|
||||
return ConfigType::Boolean;
|
||||
} else {
|
||||
static_assert(util::Unsupported<Type>, "Wrong config type");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace util::config
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/newconfig/ConfigValue.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -55,9 +56,9 @@ double
|
||||
ValueView::asDouble() const
|
||||
{
|
||||
if (configVal_.get().hasValue()) {
|
||||
if (type() == ConfigType::Double) {
|
||||
if (type() == ConfigType::Double)
|
||||
return std::get<double>(configVal_.get().getValue());
|
||||
}
|
||||
|
||||
if (type() == ConfigType::Integer)
|
||||
return static_cast<double>(std::get<int64_t>(configVal_.get().getValue()));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/newconfig/ConfigValue.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
@@ -84,7 +85,7 @@ public:
|
||||
return static_cast<T>(val);
|
||||
}
|
||||
}
|
||||
ASSERT(false, "Value view is not of any Int type");
|
||||
ASSERT(false, "Value view is not of Int type");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <gtest/gtest.h>
|
||||
|
||||
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";
|
||||
|
||||
@@ -133,12 +133,13 @@ target_sources(
|
||||
web/RPCServerHandlerTests.cpp
|
||||
web/ServerTests.cpp
|
||||
# New Config
|
||||
util/newconfig/ArrayViewTests.cpp
|
||||
util/newconfig/ObjectViewTests.cpp
|
||||
util/newconfig/ValueViewTests.cpp
|
||||
util/newconfig/ArrayTests.cpp
|
||||
util/newconfig/ConfigValueTests.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
|
||||
)
|
||||
|
||||
configure_file(test_data/cert.pem ${CMAKE_BINARY_DIR}/tests/unit/test_data/cert.pem COPYONLY)
|
||||
|
||||
@@ -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 <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
using namespace util::config;
|
||||
|
||||
TEST(ArrayTest, testConfigArray)
|
||||
TEST(ArrayTest, addSingleValue)
|
||||
{
|
||||
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 arr = Array{ConfigValue{ConfigType::Double}};
|
||||
arr.addValue(111.11);
|
||||
EXPECT_EQ(arr.size(), 1);
|
||||
}
|
||||
|
||||
auto cv2 = arr.at(1);
|
||||
ValueView const vv2{cv2};
|
||||
EXPECT_EQ(vv2.asIntType<int>(), 1234);
|
||||
TEST(ArrayTest, addAndCheckMultipleValues)
|
||||
{
|
||||
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<int64_t> const expected{543, 123, 909};
|
||||
|
||||
for (auto const num : expected)
|
||||
arr.addValue(num);
|
||||
|
||||
std::vector<int64_t> actual;
|
||||
for (auto it = arr.begin(); it != arr.end(); ++it)
|
||||
actual.emplace_back(std::get<int64_t>(it->getValue()));
|
||||
|
||||
EXPECT_TRUE(std::ranges::equal(expected, actual));
|
||||
}
|
||||
|
||||
@@ -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 <boost/json/parse.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -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)
|
||||
{
|
||||
ArrayView const arrVals = configData.getArray("array.[].sub");
|
||||
auto valIt = arrVals.begin<ValueView>();
|
||||
auto const precision = 1e-9;
|
||||
EXPECT_NEAR((*valIt++).asDouble(), 111.11, precision);
|
||||
EXPECT_NEAR((*valIt++).asDouble(), 4321.55, precision);
|
||||
EXPECT_EQ(valIt, arrVals.end<ValueView>());
|
||||
ArrayView const arrVals = configData.getArray("array.[].sub");
|
||||
|
||||
EXPECT_NEAR(111.11, arrVals.valueAt(0).asDouble(), precision);
|
||||
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);
|
||||
|
||||
ArrayView const arrVals2 = configData.getArray("array.[].sub2");
|
||||
auto val2It = arrVals2.begin<ValueView>();
|
||||
EXPECT_EQ((*val2It++).asString(), "subCategory");
|
||||
EXPECT_EQ((*val2It++).asString(), "temporary");
|
||||
EXPECT_EQ(val2It, arrVals2.end<ValueView>());
|
||||
|
||||
ValueView const tempVal = arrVals2.valueAt(0);
|
||||
EXPECT_EQ(tempVal.type(), ConfigType::String);
|
||||
EXPECT_EQ("subCategory", tempVal.asString());
|
||||
}
|
||||
|
||||
TEST_F(ArrayViewTest, ArrayWithObjTest)
|
||||
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<ValueView>();
|
||||
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<ValueView>());
|
||||
}
|
||||
|
||||
TEST_F(ArrayViewTest, IterateValuesString)
|
||||
{
|
||||
ArrayView const arrVals = configData.getArray("array.[].sub2");
|
||||
|
||||
auto val2It = arrVals.begin<ValueView>();
|
||||
EXPECT_EQ((*val2It++).asString(), "subCategory");
|
||||
EXPECT_EQ((*val2It++).asString(), "temporary");
|
||||
EXPECT_EQ((*val2It++).asString(), "london");
|
||||
EXPECT_EQ(val2It, arrVals.end<ValueView>());
|
||||
}
|
||||
|
||||
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<ValueView>();
|
||||
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<ValueView>());
|
||||
}
|
||||
|
||||
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<ValueView>();
|
||||
auto itDosguard = dosguardArray.begin<ValueView>();
|
||||
@@ -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<ObjectView>();
|
||||
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<ObjectView>());
|
||||
}
|
||||
|
||||
struct ArrayViewDeathTest : ArrayViewTest {};
|
||||
|
||||
TEST_F(ArrayViewDeathTest, IncorrectAccess)
|
||||
TEST_F(ArrayViewDeathTest, AccessArrayOutOfBounce)
|
||||
{
|
||||
ArrayView const arr = configData.getArray("higher");
|
||||
// dies because higher only has 1 object (trying to access 2nd element)
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getArray("higher").objectAt(1); }, ".*");
|
||||
}
|
||||
|
||||
// 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 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<int>(); }, ".*");
|
||||
}
|
||||
|
||||
TEST_F(ArrayViewDeathTest, IncorrectIterateAccess)
|
||||
TEST_F(ArrayViewDeathTest, GetValueWhenItIsObject)
|
||||
{
|
||||
ArrayView const arr = configData.getArray("higher");
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto _ = arr.begin<ValueView>(); }, ".*");
|
||||
}
|
||||
|
||||
TEST_F(ArrayViewDeathTest, GetObjectWhenItIsValue)
|
||||
{
|
||||
ArrayView const dosguardWhitelist = configData.getArray("dosguard.whitelist");
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto _ = dosguardWhitelist.begin<ObjectView>(); }, ".*");
|
||||
}
|
||||
|
||||
@@ -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 <boost/filesystem/operations.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
#include <boost/json/value_to.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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("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, GetObjectButIsArray)
|
||||
{
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("array"); }, ".*");
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto a_ = configData.getObject("array", 2); }, ".*");
|
||||
}
|
||||
|
||||
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<int>(), 44444);
|
||||
|
||||
EXPECT_TRUE(configData.contains("header.port"));
|
||||
EXPECT_EQ(configData.getValue("header.port").asIntType<int64_t>(), 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<double> expectedArrSubVal{111.11, 4321.55, 5555.44};
|
||||
std::vector<double> actualArrSubVal{};
|
||||
for (auto it = arrSub.begin<ValueView>(); it != arrSub.end<ValueView>(); ++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<std::string> expectedArrSub2Val{"subCategory", "temporary", "london"};
|
||||
std::vector<std::string> actualArrSub2Val{};
|
||||
for (auto it = arrSub2.begin<ValueView>(); it != arrSub2.end<ValueView>(); ++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<std::string_view> 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<std::string_view> actualErrors;
|
||||
for (auto const& error : errors.value()) {
|
||||
actualErrors.insert(error.error);
|
||||
}
|
||||
EXPECT_EQ(expectedErrors, actualErrors);
|
||||
}
|
||||
|
||||
@@ -17,24 +17,207 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include "util/newconfig/ConfigConstraints.hpp"
|
||||
#include "util/newconfig/ConfigValue.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
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<char const*, 1> 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<char const*, 3> 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<ConstraintTestBundle> {};
|
||||
|
||||
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<ConstraintTestBundle> 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);
|
||||
},
|
||||
".*"
|
||||
);
|
||||
}
|
||||
|
||||
113
tests/unit/util/newconfig/JsonConfigFileTests.cpp
Normal file
113
tests/unit/util/newconfig/JsonConfigFileTests.cpp
Normal file
@@ -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 <boost/json/parse.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
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<std::string>(jsonFileObj.getValue("header.text1")), "value");
|
||||
|
||||
EXPECT_TRUE(jsonFileObj.containsKey("header.sub.sub2Value"));
|
||||
EXPECT_EQ(std::get<std::string>(jsonFileObj.getValue("header.sub.sub2Value")), "TSM");
|
||||
|
||||
EXPECT_TRUE(jsonFileObj.containsKey("dosguard.port"));
|
||||
EXPECT_EQ(std::get<int64_t>(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<double> expectedArrSubVal{111.11, 4321.55, 5555.44};
|
||||
std::vector<double> actualArrSubVal{};
|
||||
|
||||
for (auto it = arrSub.begin(); it != arrSub.end(); ++it) {
|
||||
ASSERT_TRUE(std::holds_alternative<double>(*it));
|
||||
actualArrSubVal.emplace_back(std::get<double>(*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<std::string> expectedArrSub2Val{"subCategory", "temporary", "london"};
|
||||
std::vector<std::string> actualArrSub2Val{};
|
||||
|
||||
for (auto it = arrSub2.begin(); it != arrSub2.end(); ++it) {
|
||||
ASSERT_TRUE(std::holds_alternative<std::string>(*it));
|
||||
actualArrSub2Val.emplace_back(std::get<std::string>(*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<std::string>(whitelistArr.at(0)));
|
||||
EXPECT_EQ("204.2.2.1", std::get<std::string>(whitelistArr.at(1)));
|
||||
}
|
||||
|
||||
struct JsonValueDeathTest : ParseJson {};
|
||||
|
||||
TEST_F(JsonValueDeathTest, invalidGetArray)
|
||||
{
|
||||
EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("header.text1"), ".*");
|
||||
}
|
||||
@@ -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 <boost/json/parse.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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<int>());
|
||||
EXPECT_EQ(true, headerObj.getValue("admin").asBool());
|
||||
EXPECT_EQ(321, headerObj.getValue("port").asIntType<int>());
|
||||
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("head"); }, ".*");
|
||||
}
|
||||
|
||||
TEST_F(ObjectViewDeathTest, KeyIsValueView)
|
||||
{
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("header.text1"); }, ".*");
|
||||
EXPECT_DEATH({ [[maybe_unused]] auto _ = configData.getObject("head"); }, ".*");
|
||||
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); }, ".*");
|
||||
}
|
||||
|
||||
@@ -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 <gtest/gtest.h>
|
||||
|
||||
Reference in New Issue
Block a user