mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
@@ -36,14 +36,22 @@ Array::Array(ConfigValue arg) : itemPattern_{std::move(arg)}
|
||||
{
|
||||
}
|
||||
|
||||
std::string_view
|
||||
Array::prefix(std::string_view key)
|
||||
{
|
||||
static constexpr std::string_view kARRAY_SUFFIX = ".[]";
|
||||
ASSERT(key.contains(kARRAY_SUFFIX), "Provided key is not an array key: {}", key);
|
||||
|
||||
return key.substr(0, key.rfind(kARRAY_SUFFIX) + kARRAY_SUFFIX.size());
|
||||
}
|
||||
|
||||
std::optional<Error>
|
||||
Array::addValue(Value value, std::optional<std::string_view> key)
|
||||
{
|
||||
auto const& configValPattern = itemPattern_;
|
||||
auto const constraint = configValPattern.getConstraint();
|
||||
auto const constraint = itemPattern_.getConstraint();
|
||||
|
||||
auto newElem = constraint.has_value() ? ConfigValue{configValPattern.type()}.withConstraint(constraint->get())
|
||||
: ConfigValue{configValPattern.type()};
|
||||
auto newElem = constraint.has_value() ? ConfigValue{itemPattern_.type()}.withConstraint(constraint->get())
|
||||
: ConfigValue{itemPattern_.type()};
|
||||
if (auto const maybeError = newElem.setValue(value, key); maybeError.has_value())
|
||||
return maybeError;
|
||||
elements_.emplace_back(std::move(newElem));
|
||||
|
||||
@@ -47,6 +47,17 @@ public:
|
||||
*/
|
||||
Array(ConfigValue arg);
|
||||
|
||||
/**
|
||||
* @brief Extract array prefix from a key, For example for a key foo.[].bar the method will return foo.[]
|
||||
* @note Provided key must contain '.[]'
|
||||
* @warning Be careful with string_view! Returned value is valid only while the key is valid
|
||||
*
|
||||
* @param key The key to extract the array prefix from
|
||||
* @return Prefix of array extracted from the key
|
||||
*/
|
||||
static std::string_view
|
||||
prefix(std::string_view key);
|
||||
|
||||
/**
|
||||
* @brief Add ConfigValues to Array class
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "util/newconfig/ConfigFileJson.hpp"
|
||||
|
||||
#include "util/Assert.hpp"
|
||||
#include "util/newconfig/Array.hpp"
|
||||
#include "util/newconfig/Error.hpp"
|
||||
#include "util/newconfig/Types.hpp"
|
||||
|
||||
@@ -30,15 +31,19 @@
|
||||
#include <boost/json/value.hpp>
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -69,14 +74,17 @@ extractJsonValue(boost::json::value const& jsonValue)
|
||||
if (jsonValue.is_double()) {
|
||||
return jsonValue.as_double();
|
||||
}
|
||||
ASSERT(false, "Json is not of type int, uint, string, bool or double");
|
||||
if (jsonValue.is_null()) {
|
||||
return NullType{};
|
||||
}
|
||||
ASSERT(false, "Json is not of type null, int, uint, string, bool or double");
|
||||
std::unreachable();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConfigFileJson::ConfigFileJson(boost::json::object jsonObj)
|
||||
{
|
||||
flattenJson(jsonObj, "");
|
||||
flattenJson(jsonObj);
|
||||
}
|
||||
|
||||
std::expected<ConfigFileJson, Error>
|
||||
@@ -86,8 +94,7 @@ ConfigFileJson::makeConfigFileJson(std::filesystem::path const& configFilePath)
|
||||
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 opts = boost::json::parse_options{.allow_comments = true};
|
||||
auto const tempObj = boost::json::parse(contents.str(), {}, opts).as_object();
|
||||
return ConfigFileJson{tempObj};
|
||||
}
|
||||
@@ -105,7 +112,9 @@ ConfigFileJson::makeConfigFileJson(std::filesystem::path const& configFilePath)
|
||||
Value
|
||||
ConfigFileJson::getValue(std::string_view key) const
|
||||
{
|
||||
ASSERT(containsKey(key), "Key {} not found in ConfigFileJson", key);
|
||||
auto const jsonValue = jsonObject_.at(key);
|
||||
ASSERT(jsonValue.is_primitive(), "Key {} has value that is not a primitive", key);
|
||||
auto const value = extractJsonValue(jsonValue);
|
||||
return value;
|
||||
}
|
||||
@@ -113,14 +122,15 @@ ConfigFileJson::getValue(std::string_view key) const
|
||||
std::vector<Value>
|
||||
ConfigFileJson::getArray(std::string_view key) const
|
||||
{
|
||||
ASSERT(containsKey(key), "Key {} not found in ConfigFileJson", key);
|
||||
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);
|
||||
auto value = extractJsonValue(item);
|
||||
configValues.emplace_back(std::move(value));
|
||||
}
|
||||
return configValues;
|
||||
}
|
||||
@@ -131,38 +141,90 @@ ConfigFileJson::containsKey(std::string_view key) const
|
||||
return jsonObject_.contains(key);
|
||||
}
|
||||
|
||||
void
|
||||
ConfigFileJson::flattenJson(boost::json::object const& obj, std::string const& prefix)
|
||||
boost::json::object const&
|
||||
ConfigFileJson::inner() const
|
||||
{
|
||||
for (auto const& [key, value] : obj) {
|
||||
std::string const fullKey = prefix.empty() ? std::string(key) : fmt::format("{}.{}", prefix, std::string(key));
|
||||
return jsonObject_;
|
||||
}
|
||||
|
||||
// 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 const arrayPrefix = fullKey + ".[]";
|
||||
if (arr[i].is_object()) {
|
||||
flattenJson(arr[i].as_object(), arrayPrefix);
|
||||
void
|
||||
ConfigFileJson::flattenJson(boost::json::object const& jsonRootObject)
|
||||
{
|
||||
struct Task {
|
||||
boost::json::object const& object;
|
||||
std::string prefix;
|
||||
std::optional<size_t> arrayIndex = std::nullopt;
|
||||
};
|
||||
|
||||
std::queue<Task> tasks;
|
||||
tasks.push(Task{.object = jsonRootObject, .prefix = ""});
|
||||
|
||||
std::unordered_map<std::string, size_t> arraysSizes;
|
||||
|
||||
while (not tasks.empty()) {
|
||||
auto const task = std::move(tasks.front());
|
||||
tasks.pop();
|
||||
|
||||
for (auto const& [key, value] : task.object) {
|
||||
auto fullKey =
|
||||
task.prefix.empty() ? std::string(key) : fmt::format("{}.{}", task.prefix, std::string_view{key});
|
||||
|
||||
if (value.is_object()) {
|
||||
tasks.push(
|
||||
Task{.object = value.as_object(), .prefix = std::move(fullKey), .arrayIndex = task.arrayIndex}
|
||||
);
|
||||
} else if (value.is_array()) {
|
||||
fullKey += ".[]";
|
||||
auto const& array = value.as_array();
|
||||
|
||||
if (std::ranges::all_of(array, [](auto const& v) { return v.is_primitive(); })) {
|
||||
jsonObject_[fullKey] = array;
|
||||
} else if (std::ranges::all_of(array, [](auto const& v) { return v.is_object(); })) {
|
||||
for (size_t i = 0; i < array.size(); ++i) {
|
||||
tasks.push(Task{.object = array.at(i).as_object(), .prefix = fullKey, .arrayIndex = i});
|
||||
}
|
||||
} else {
|
||||
jsonObject_[arrayPrefix] = arr;
|
||||
ASSERT(
|
||||
false,
|
||||
"Arrays containing both values and objects are not supported. Please check the array {}",
|
||||
fullKey
|
||||
);
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
if (task.arrayIndex.has_value()) {
|
||||
if (not jsonObject_.contains(fullKey)) {
|
||||
jsonObject_[fullKey] = boost::json::array{};
|
||||
}
|
||||
|
||||
auto& targetArray = jsonObject_.at(fullKey).as_array();
|
||||
while (targetArray.size() < (*task.arrayIndex + 1)) {
|
||||
targetArray.push_back(boost::json::value());
|
||||
}
|
||||
targetArray.at(*task.arrayIndex) = value;
|
||||
auto const prefix = std::string{Array::prefix(fullKey)};
|
||||
arraysSizes[prefix] = std::max(arraysSizes[prefix], targetArray.size());
|
||||
} else {
|
||||
jsonObject_[fullKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjust length of each array containing objects
|
||||
std::ranges::for_each(jsonObject_, [&arraysSizes](auto& item) {
|
||||
auto const key = item.key();
|
||||
if (not key.contains("[]"))
|
||||
return;
|
||||
|
||||
auto& value = item.value();
|
||||
auto const prefix = std::string{Array::prefix(key)};
|
||||
if (auto const it = arraysSizes.find(prefix); it != arraysSizes.end()) {
|
||||
auto const size = it->second;
|
||||
while (value.as_array().size() < size) {
|
||||
value.as_array().push_back(boost::json::value{});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace util::config
|
||||
|
||||
@@ -35,6 +35,8 @@ namespace util::config {
|
||||
|
||||
/** @brief Json representation of config */
|
||||
class ConfigFileJson final : public ConfigFileInterface {
|
||||
boost::json::object jsonObject_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new ConfigJson object and stores the values from
|
||||
@@ -81,20 +83,26 @@ public:
|
||||
[[nodiscard]] static std::expected<ConfigFileJson, Error>
|
||||
makeConfigFileJson(std::filesystem::path const& configFilePath);
|
||||
|
||||
/**
|
||||
* @brief Get the inner representation of json file.
|
||||
* @note This method is mostly used for testing purposes.
|
||||
*
|
||||
* @return The inner representation of json file.
|
||||
*/
|
||||
[[nodiscard]] boost::json::object const&
|
||||
inner() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Recursive function to flatten a JSON object into the same structure as the Clio Config.
|
||||
* @brief Method 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.
|
||||
* The keys will end up having the same naming conventions 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_;
|
||||
flattenJson(boost::json::object const& jsonRootObject);
|
||||
};
|
||||
|
||||
} // namespace util::config
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
namespace util::config {
|
||||
@@ -80,18 +81,31 @@ public:
|
||||
[[nodiscard]] std::optional<Error>
|
||||
setValue(Value value, std::optional<std::string_view> key = std::nullopt)
|
||||
{
|
||||
if (std::holds_alternative<NullType>(value)) {
|
||||
if (hasValue()) {
|
||||
// Using default value
|
||||
return std::nullopt;
|
||||
}
|
||||
if (not isOptional()) {
|
||||
return Error{
|
||||
key.value_or("Unknown_key"),
|
||||
"Provided value is null but ConfigValue is not optional and doesn't have a default value."
|
||||
};
|
||||
}
|
||||
value_ = std::move(value);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto err = checkTypeConsistency(type_, value);
|
||||
if (err.has_value()) {
|
||||
if (key.has_value())
|
||||
err->error = fmt::format("{} {}", key.value(), err->error);
|
||||
err->error = fmt::format("{} {}", key.value_or("Unknown_key"), 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);
|
||||
constraintCheck->error = fmt::format("{} {}", key.value_or("Unknown_key"), constraintCheck->error);
|
||||
return constraintCheck;
|
||||
}
|
||||
}
|
||||
@@ -127,7 +141,8 @@ public:
|
||||
[&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); }
|
||||
[&type](int64_t tmp) { type = fmt::format("int {}", tmp); },
|
||||
[&type](NullType) { type = "null"; },
|
||||
},
|
||||
value_.value()
|
||||
);
|
||||
@@ -199,6 +214,7 @@ public:
|
||||
[[nodiscard]] Value const&
|
||||
getValue() const
|
||||
{
|
||||
ASSERT(value_.has_value(), "getValue() is called when there is no value set");
|
||||
return value_.value();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
#include "util/UnsupportedType.hpp"
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <ostream>
|
||||
@@ -30,7 +32,23 @@
|
||||
namespace util::config {
|
||||
|
||||
/** @brief Custom clio config types */
|
||||
enum class ConfigType { Integer, String, Double, Boolean };
|
||||
enum class ConfigType { Integer, String, Double, Boolean, Null };
|
||||
|
||||
/**
|
||||
* @brief A type that represents a null value
|
||||
*/
|
||||
struct NullType {
|
||||
/**
|
||||
* @brief Compare two NullType objects
|
||||
*
|
||||
* @return true always. Any two NullType objects are equal
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
operator==(NullType const&) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Prints the specified config type to output stream
|
||||
@@ -43,7 +61,7 @@ std::ostream&
|
||||
operator<<(std::ostream& stream, ConfigType type);
|
||||
|
||||
/** @brief Represents the supported Config Values */
|
||||
using Value = std::variant<int64_t, std::string, bool, double>;
|
||||
using Value = std::variant<int64_t, std::string, bool, double, NullType>;
|
||||
|
||||
/**
|
||||
* @brief Prints the specified value to output stream
|
||||
@@ -73,9 +91,24 @@ getType()
|
||||
return ConfigType::Double;
|
||||
} else if constexpr (std::is_same_v<Type, bool>) {
|
||||
return ConfigType::Boolean;
|
||||
} else if constexpr (std::is_same_v<Type, NullType>) {
|
||||
return ConfigType::Null;
|
||||
} else {
|
||||
static_assert(util::Unsupported<Type>, "Wrong config type");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace util::config
|
||||
|
||||
/** @cond */
|
||||
// Doxygen could not parse this
|
||||
template <>
|
||||
struct fmt::formatter<util::config::NullType> : fmt::formatter<char const*> {
|
||||
[[nodiscard]]
|
||||
auto
|
||||
format(util::config::NullType const&, fmt::format_context& ctx)
|
||||
{
|
||||
return fmt::formatter<char const*>::format("null", ctx);
|
||||
}
|
||||
};
|
||||
/** @endcond */
|
||||
|
||||
Reference in New Issue
Block a user