feat(loading): robust error reporting and mutation
cfg now reports which fields are missing when loading. Further, the mutate method has been added for easier mutation of the underlying configuration struct
This commit is contained in:
62
src/config/include/fourdst/config/ansi.h
Normal file
62
src/config/include/fourdst/config/ansi.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#define ISATTY _isatty
|
||||
#define FILENO _fileno
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define ISATTY isatty
|
||||
#define FILENO fileno
|
||||
#endif
|
||||
|
||||
namespace fourdst::config::utils {
|
||||
|
||||
bool supports_ansi_colors() {
|
||||
if (std::getenv("NO_COLOR")) return false;
|
||||
if (std::getenv("FORCE_COLOR")) return true;
|
||||
|
||||
if (!ISATTY(FILENO(stdout))) return false;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (hOut == INVALID_HANDLE_VALUE) return false;
|
||||
DWORD dwMode = 0;
|
||||
if (!GetConsoleMode(hOut, &dwMode)) return false;
|
||||
return (dwMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
|
||||
#else
|
||||
const char* term = std::getenv("TERM");
|
||||
if (!term) return false;
|
||||
const std::string term_str(term);
|
||||
return term_str != "dumb";
|
||||
#endif
|
||||
}
|
||||
|
||||
const static bool TERM_COLOR_SUPPORT = supports_ansi_colors();
|
||||
|
||||
class ANSIColor {
|
||||
public:
|
||||
explicit ANSIColor(const std::string& value) : m_value(value) {}
|
||||
|
||||
std::string_view get() const {
|
||||
if (TERM_COLOR_SUPPORT) {
|
||||
return m_value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
private:
|
||||
std::string m_value{""};
|
||||
ANSIColor() = default;
|
||||
|
||||
};
|
||||
|
||||
static ANSIColor RED{"\033[31m"};
|
||||
static ANSIColor GREEN{"\033[32m"};
|
||||
static ANSIColor BLUE{"\033[34m"};
|
||||
static ANSIColor CYAN{"\033[36m"};
|
||||
static ANSIColor RESET{"\033[0m"};
|
||||
}
|
||||
@@ -14,8 +14,10 @@
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <mutex>
|
||||
|
||||
#include "fourdst/config/exceptions/exceptions.h"
|
||||
#include "fourdst/config/validate.h"
|
||||
|
||||
#include "rfl.hpp"
|
||||
#include "rfl/toml.hpp"
|
||||
@@ -65,7 +67,9 @@ namespace fourdst::config {
|
||||
/**
|
||||
* @brief Configuration has been successfully populated from a file.
|
||||
*/
|
||||
LOADED_FROM_FILE
|
||||
LOADED_FROM_FILE,
|
||||
|
||||
MODIFIED
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +131,7 @@ namespace fourdst::config {
|
||||
* @brief Get a mutable pointer to the configuration content.
|
||||
* @return Pointer to the mutable configuration content.
|
||||
*/
|
||||
T* write() const { return &m_content; }
|
||||
[[deprecated("write has been depreceated for mutate() and will be removed in versions >= v3.0.0")]] T* write() const { return &m_content; }
|
||||
|
||||
/**
|
||||
* @brief Dereference operator to access the underlying configuration struct.
|
||||
@@ -246,7 +250,7 @@ namespace fourdst::config {
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
void load(const std::string_view path) {
|
||||
void load(const std::string_view path, const bool verbose = false) {
|
||||
if (m_state == ConfigState::LOADED_FROM_FILE) {
|
||||
throw exceptions::ConfigLoadError(
|
||||
"Config has already been loaded from file. Reloading is not supported.");
|
||||
@@ -261,8 +265,32 @@ namespace fourdst::config {
|
||||
const rfl::Result<wrapper> result = rfl::toml::load<wrapper>(std::string(path));
|
||||
|
||||
if (!result) {
|
||||
std::vector<std::string> missing_fields;
|
||||
|
||||
try {
|
||||
toml::table root_tbl = toml::parse_file(std::string(path));
|
||||
|
||||
if (!root_tbl.empty()) {
|
||||
const auto loaded_root_name = std::string(root_tbl.begin()->first);
|
||||
const toml::table* t_tbl = root_tbl[loaded_root_name].as_table();
|
||||
|
||||
if (t_tbl) {
|
||||
validate::ConfigValidator<T>::check(t_tbl, loaded_root_name, missing_fields);
|
||||
}
|
||||
}
|
||||
} catch (const toml::parse_error&) {
|
||||
throw exceptions::ConfigParseError("Unable to parse TOML file for an unknown reason. This normally means the toml file is empty or completely malformed. Please check the file content and ensure it is valid TOML. If the file is empty, consider adding at least an empty table (e.g., [main]) to it.");
|
||||
}
|
||||
|
||||
if (!missing_fields.empty() && verbose) {
|
||||
std::cerr << validate::report_all_missing_fields(missing_fields) << std::endl;
|
||||
}
|
||||
|
||||
throw exceptions::ConfigParseError(
|
||||
std::format("Failed to load config from file: {}", path));
|
||||
std::format("Failed to load config from file: {}. Reason: {}",
|
||||
path,
|
||||
result.error().what())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -328,13 +356,35 @@ namespace fourdst::config {
|
||||
return "DEFAULT";
|
||||
case ConfigState::LOADED_FROM_FILE:
|
||||
return "LOADED_FROM_FILE";
|
||||
case ConfigState::MODIFIED:
|
||||
return "MODIFIED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
template <typename MutatorFunc>
|
||||
void mutate(MutatorFunc&& mutator) {
|
||||
m_content_mutex.lock();
|
||||
m_content_orig = m_content;
|
||||
mutator(m_content);
|
||||
m_state = ConfigState::MODIFIED;
|
||||
m_content_mutex.unlock();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_content_mutex.lock();
|
||||
if (m_state == ConfigState::MODIFIED) {
|
||||
m_content = m_content_orig;
|
||||
m_state = ConfigState::LOADED_FROM_FILE;
|
||||
}
|
||||
m_content_mutex.unlock();
|
||||
}
|
||||
|
||||
private:
|
||||
T m_content;
|
||||
T m_content_orig;
|
||||
std::mutex m_content_mutex;
|
||||
std::string m_root_name = "main";
|
||||
ConfigState m_state = ConfigState::DEFAULT;
|
||||
RootNameLoadPolicy m_root_name_load_policy = RootNameLoadPolicy::KEEP_CURRENT;
|
||||
|
||||
157
src/config/include/fourdst/config/validate.h
Normal file
157
src/config/include/fourdst/config/validate.h
Normal file
@@ -0,0 +1,157 @@
|
||||
#pragma once
|
||||
|
||||
#include "fourdst/config/ansi.h"
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <toml++/toml.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <format>
|
||||
#include <type_traits>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <sstream>
|
||||
|
||||
namespace fourdst::config::validate {
|
||||
|
||||
template <typename T> struct is_optional_impl : std::false_type {};
|
||||
template <typename T> struct is_optional_impl<std::optional<T>> : std::true_type {};
|
||||
template <typename Type> constexpr bool is_optional_v = is_optional_impl<std::remove_cvref_t<Type>>::value;
|
||||
|
||||
template <typename T> struct is_vector_impl : std::false_type {};
|
||||
template <typename T, typename A> struct is_vector_impl<std::vector<T, A>> : std::true_type {};
|
||||
template <typename Type> constexpr bool is_vector_v = is_vector_impl<std::remove_cvref_t<Type>>::value;
|
||||
|
||||
template <typename T> struct is_map_impl : std::false_type {};
|
||||
template <typename K, typename V, typename C, typename A> struct is_map_impl<std::map<K, V, C, A>> : std::true_type {};
|
||||
template <typename K, typename V, typename H, typename E, typename A> struct is_map_impl<std::unordered_map<K, V, H, E, A>> : std::true_type {};
|
||||
template <typename Type> constexpr bool is_map_v = is_map_impl<std::remove_cvref_t<Type>>::value;
|
||||
|
||||
template <typename Type>
|
||||
constexpr bool is_string_like_v = std::is_same_v<std::remove_cvref_t<Type>, std::string> ||
|
||||
std::is_same_v<std::remove_cvref_t<Type>, std::string_view>;
|
||||
|
||||
template <typename Type>
|
||||
constexpr bool is_reflectable_struct_v = std::is_class_v<std::remove_cvref_t<Type>> &&
|
||||
!is_string_like_v<Type> &&
|
||||
!is_vector_v<Type> &&
|
||||
!is_optional_v<Type> &&
|
||||
!is_map_v<Type>;
|
||||
|
||||
template <typename StructType>
|
||||
struct ConfigValidator {
|
||||
using NT = rfl::named_tuple_t<StructType>;
|
||||
|
||||
static void check(const toml::table* tbl, const std::string& current_path, std::vector<std::string>& missing) {
|
||||
if (!tbl) return;
|
||||
check_tuple<NT>(tbl, current_path, missing);
|
||||
}
|
||||
private:
|
||||
template <typename Tuple>
|
||||
struct TupleChecker;
|
||||
|
||||
template <typename... Fields>
|
||||
struct TupleChecker<rfl::NamedTuple<Fields...>> {
|
||||
static void check(const toml::table* tbl, const std::string& path, std::vector<std::string>& missing) {
|
||||
(check_field<Fields>(tbl, path, missing), ...);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename TupleType>
|
||||
static void check_tuple(const toml::table* tbl, const std::string& path, std::vector<std::string>& missing) {
|
||||
TupleChecker<TupleType>::check(tbl, path, missing);
|
||||
}
|
||||
|
||||
template <typename Field>
|
||||
static void check_field(const toml::table* tbl, const std::string& path, std::vector<std::string>& missing) {
|
||||
std::string_view name_sv = Field::name();
|
||||
std::string name(name_sv);
|
||||
|
||||
using RawType = typename Field::Type;
|
||||
using Type = std::remove_cvref_t<RawType>;
|
||||
|
||||
std::string full_path = path.empty() ? name : path + "." + name;
|
||||
const toml::node* node = tbl->get(name_sv);
|
||||
|
||||
if (!node) {
|
||||
if constexpr (!is_optional_v<Type>) {
|
||||
missing.push_back(full_path);
|
||||
}
|
||||
} else {
|
||||
if constexpr (is_reflectable_struct_v<Type>) {
|
||||
if (node->is_table()) {
|
||||
ConfigValidator<Type>::check(node->as_table(), full_path, missing);
|
||||
}
|
||||
} else if constexpr (is_vector_v<Type>) {
|
||||
using ElementType = std::remove_cvref_t<typename Type::value_type>;
|
||||
if constexpr (is_reflectable_struct_v<ElementType>) {
|
||||
if (node->is_array()) {
|
||||
const auto& arr = *node->as_array();
|
||||
for (size_t i = 0; i < arr.size(); ++i) {
|
||||
if (arr.get(i)->is_table()) {
|
||||
ConfigValidator<ElementType>::check(
|
||||
arr.get(i)->as_table(),
|
||||
std::format("{}[{}]", full_path, i),
|
||||
missing
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if constexpr (is_optional_v<Type>) {
|
||||
using InnerType = std::remove_cvref_t<typename Type::value_type>;
|
||||
if constexpr (is_reflectable_struct_v<InnerType>) {
|
||||
if (node->is_table()) {
|
||||
ConfigValidator<InnerType>::check(node->as_table(), full_path, missing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct MissingFieldTree {
|
||||
std::map<std::string, MissingFieldTree> children;
|
||||
bool is_missing = false;
|
||||
};
|
||||
|
||||
inline void print_missing_field_tree(const MissingFieldTree& tree, std::string indent, bool is_last, const std::string& name, std::string& output) {
|
||||
if (!name.empty()) {
|
||||
std::string display_name = tree.is_missing ? std::format("{}{}{}", utils::RED.get(), name, utils::RESET.get()) : name;
|
||||
output += std::format("{}{} {}\n", indent, is_last ? "└──" : "├──", display_name);
|
||||
indent += is_last ? " " : "│ ";
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
for (auto const& [child_name, child_tree] : tree.children) {
|
||||
print_missing_field_tree(child_tree, indent, count == tree.children.size() - 1, child_name, output);
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string report_all_missing_fields(const std::vector<std::string>& missing) {
|
||||
if (missing.empty()) return "";
|
||||
|
||||
MissingFieldTree root;
|
||||
for (const auto& path : missing) {
|
||||
std::stringstream ss(path);
|
||||
std::string part;
|
||||
MissingFieldTree* current = &root;
|
||||
while (std::getline(ss, part, '.')) {
|
||||
current = ¤t->children[part];
|
||||
}
|
||||
current->is_missing = true;
|
||||
}
|
||||
|
||||
std::string output = "\nConfiguration Missing Field Path(s):\n";
|
||||
size_t count = 0;
|
||||
for (auto const& [name, child_tree] : root.children) {
|
||||
print_missing_field_tree(child_tree, "", count == root.children.size() - 1, name, output);
|
||||
++count;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user