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:
2026-04-07 09:28:27 -04:00
parent 46dce35eaf
commit d4bbd9cb3a
9 changed files with 382 additions and 10 deletions

View File

@@ -18,7 +18,7 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# #
# *********************************************************************** # # *********************************************************************** #
project('libconfig', ['cpp', 'c'], version: 'v2.1.0', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') project('libconfig', ['cpp', 'c'], version: 'v2.2.0', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0')
# Add default visibility for all C++ targets # Add default visibility for all C++ targets
add_project_arguments('-fvisibility=default', language: 'cpp') add_project_arguments('-fvisibility=default', language: 'cpp')

View File

@@ -33,10 +33,12 @@ and strongly typed.
#include "fourdst/config/config.h" #include "fourdst/config/config.h"
#include <string> #include <string>
#include <print> #include <print>
#include <optional>
struct MyPhysicsOptions { struct MyPhysicsOptions {
int gravity = 10; int gravity = 10;
float friction = 0.5f; float friction = 0.5f;
std::optional<float> dampening = 0.1f;
bool enable_wind = false; bool enable_wind = false;
}; };
@@ -64,12 +66,38 @@ int main() {
// You can load a config from a file // You can load a config from a file
try { try {
cfg.load("my_config.toml"); cfg.load("my_config.toml");
// You can also pass an optional bool as a second argument to turn on verbose error
// reporting. This will display a tree of missing or invalid fields. Note that due to limitations
// in C++'s ability to detect default iniailized values vs initializer list values
// missing fields which you set an initializer list for are still considered missing.
// **ONLY fields marked with std::optional are exempt from this rule.**
} catch (const fourdst::config::exceptions::ConfigError& e) { } catch (const fourdst::config::exceptions::ConfigError& e) {
std::println("Error loading config: {}", e.what()); std::println("Error loading config: {}", e.what());
} }
// You can access the config values // You can access the config values
std::println("My Simulation Name: {}, My Simulation Gravity: {}", cfg->name, cfg->physics.gravity); std::println("My Simulation Name: {}, My Simulation Gravity: {}", cfg->name, cfg->physics.gravity);
// libconfig intentioanlly discourages direct modification of config values. However, if you need
// to modify values after loading them you can use the mutate function. This takes a lambda
// which recives a mutable reference to the underlying config struct.
cfg.mutate([](MySimulationConfig& config) {
config->physics.enable_wind = true;
});
// Making these mutations will put the config into a "MODIFIED" state and will cache the unmodified values.
// You can reset the state and revert to the unmodified values with the reset function.
cfg.reset();
// The current state of the config can be checked with the get_state() function or the describe_state() function.
// get_state returns an enum value while describe_state returns a human readable string.
std::println("Config State: {}", cfg.describe_state());
fourdst::config::ConfigState state = cfg.get_state();
// Possible states are DEFAULT, LOADED_FROM_FILE, and MODIFIED
} }
``` ```

View 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"};
}

View File

@@ -14,8 +14,10 @@
#include <vector> #include <vector>
#include <string_view> #include <string_view>
#include <type_traits> #include <type_traits>
#include <mutex>
#include "fourdst/config/exceptions/exceptions.h" #include "fourdst/config/exceptions/exceptions.h"
#include "fourdst/config/validate.h"
#include "rfl.hpp" #include "rfl.hpp"
#include "rfl/toml.hpp" #include "rfl/toml.hpp"
@@ -65,7 +67,9 @@ namespace fourdst::config {
/** /**
* @brief Configuration has been successfully populated from a file. * @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. * @brief Get a mutable pointer to the configuration content.
* @return Pointer to the mutable 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. * @brief Dereference operator to access the underlying configuration struct.
@@ -246,7 +250,7 @@ namespace fourdst::config {
* } * }
* @endcode * @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) { if (m_state == ConfigState::LOADED_FROM_FILE) {
throw exceptions::ConfigLoadError( throw exceptions::ConfigLoadError(
"Config has already been loaded from file. Reloading is not supported."); "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)); const rfl::Result<wrapper> result = rfl::toml::load<wrapper>(std::string(path));
if (!result) { 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( 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"; return "DEFAULT";
case ConfigState::LOADED_FROM_FILE: case ConfigState::LOADED_FROM_FILE:
return "LOADED_FROM_FILE"; return "LOADED_FROM_FILE";
case ConfigState::MODIFIED:
return "MODIFIED";
default: default:
return "UNKNOWN"; 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: private:
T m_content; T m_content;
T m_content_orig;
std::mutex m_content_mutex;
std::string m_root_name = "main"; std::string m_root_name = "main";
ConfigState m_state = ConfigState::DEFAULT; ConfigState m_state = ConfigState::DEFAULT;
RootNameLoadPolicy m_root_name_load_policy = RootNameLoadPolicy::KEEP_CURRENT; RootNameLoadPolicy m_root_name_load_policy = RootNameLoadPolicy::KEEP_CURRENT;

View 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 = &current->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;
}
}

View File

@@ -18,11 +18,20 @@ std::string get_good_example_file() {
return std::string(source_root) + "/tests/config/example_config_files/example.good.toml"; return std::string(source_root) + "/tests/config/example_config_files/example.good.toml";
} }
std::string get_good_missing_keys_example_file() {
const char* source_root = getenv("MESON_SOURCE_ROOT");
if (source_root == nullptr) {
throw std::runtime_error("MESON_SOURCE_ROOT environment variable is not set.");
}
return std::string(source_root) + "/tests/config/example_config_files/example.good.missing.field.toml";
}
enum class BAD_FILES { enum class BAD_FILES {
UNKNOWN_KEY, UNKNOWN_KEY,
INVALID_TYPE, INVALID_TYPE,
INCORRECT_ARRAY_SIZE INCORRECT_ARRAY_SIZE,
MISSING_NONDEFAULT_KEY
}; };
std::string get_bad_example_file(BAD_FILES type) { std::string get_bad_example_file(BAD_FILES type) {
@@ -37,6 +46,8 @@ std::string get_bad_example_file(BAD_FILES type) {
return std::string(source_root) + "/tests/config/example_config_files/example.invalidtype.toml"; return std::string(source_root) + "/tests/config/example_config_files/example.invalidtype.toml";
case BAD_FILES::INCORRECT_ARRAY_SIZE: case BAD_FILES::INCORRECT_ARRAY_SIZE:
return std::string(source_root) + "/tests/config/example_config_files/example.incorrectarraysize.toml"; return std::string(source_root) + "/tests/config/example_config_files/example.incorrectarraysize.toml";
case BAD_FILES::MISSING_NONDEFAULT_KEY:
return std::string(source_root) + "/tests/config/example_config_files/example.missing.nondefault.field.toml";
} }
throw std::runtime_error("Invalid BAD_FILES type."); throw std::runtime_error("Invalid BAD_FILES type.");
} }
@@ -131,3 +142,36 @@ TEST_F(configTest, save_schema) {
Config<TestConfigSchema> cfg; Config<TestConfigSchema> cfg;
EXPECT_NO_THROW(cfg.save_schema("TestConfigSchema.schema.json")); EXPECT_NO_THROW(cfg.save_schema("TestConfigSchema.schema.json"));
} }
TEST_F(configTest, missing_default_keys) {
using namespace fourdst::config;
Config<TestConfigSchema> cfg;
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
}
TEST_F(configTest, missing_nondefault_keys) {
using namespace fourdst::config;
Config<TestConfigSchema> cfg;
EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::MISSING_NONDEFAULT_KEY)), exceptions::ConfigParseError);
}
TEST_F(configTest, mutate_and_reset) {
using namespace fourdst::config;
Config<TestConfigSchema> cfg;
EXPECT_NO_THROW(cfg.load(get_good_example_file()));
EXPECT_TRUE(cfg->physics.diffusion);
EXPECT_EQ(cfg.get_state(), ConfigState::LOADED_FROM_FILE);
EXPECT_NO_THROW(
cfg.mutate([](auto& data) {
data.physics.diffusion = false;
});
);
EXPECT_FALSE(cfg->physics.diffusion);
EXPECT_EQ(cfg.get_state(), ConfigState::MODIFIED);
EXPECT_NO_THROW(cfg.reset());
EXPECT_TRUE(cfg->physics.diffusion);
EXPECT_EQ(cfg.get_state(), ConfigState::LOADED_FROM_FILE);
}

View File

@@ -0,0 +1,16 @@
[main]
description = "This is an example configuration file."
author = "Example Author"
[main.physics]
diffusion = true
convection = false
radiation = true
flags = [1, 0, 1]
[main.simulation]
output_frequency = 10
[main.output]
format = "csv"
directory = "results/"

View File

@@ -0,0 +1,15 @@
[main]
description = "This is an example configuration file."
author = "Example Author"
[main.physics]
flags = [1, 0, 1]
[main.simulation]
time_step = 0.01
total_time = 100.0
output_frequency = 10
[main.output]
format = "csv"
directory = "results/"

View File

@@ -4,8 +4,8 @@
struct PhysicsConfigOptions { struct PhysicsConfigOptions {
bool diffusion; bool diffusion;
bool convection; std::optional<bool> convection;
bool radiation; std::optional<bool> radiation;
std::array<int, 3> flags; std::array<int, 3> flags;
}; };
@@ -18,7 +18,7 @@ struct SimulationConfigOptions {
struct OutputConfigOptions { struct OutputConfigOptions {
std::string directory = "./output"; std::string directory = "./output";
std::string format = "hdf5"; std::string format = "hdf5";
bool save_plots = false; std::optional<bool> save_plots = false;
}; };
struct TestConfigSchema { struct TestConfigSchema {
@@ -28,4 +28,4 @@ struct TestConfigSchema {
PhysicsConfigOptions physics; PhysicsConfigOptions physics;
SimulationConfigOptions simulation; SimulationConfigOptions simulation;
OutputConfigOptions output; OutputConfigOptions output;
}; };