feat(CLI): CLI integration with CLI11

libconfig can automatically generate command line interfaces using CLI11 based on some schema
This commit is contained in:
2026-02-02 08:41:47 -05:00
parent b034fb6746
commit 68cba402f3
11 changed files with 531 additions and 9 deletions

View File

@@ -0,0 +1,2 @@
cli11_proj = subproject('cli11')
cli11_dep = cli11_proj.get_variable('CLI11_dep')

View File

@@ -1,2 +1,3 @@
cmake = import('cmake')
subdir('reflect-cpp')
subdir('cli11')

33
examples/cli_example.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "CLI/CLI.hpp"
#include "fourdst/config/config.h"
#include <string>
#include <print>
#include <optional>
struct SubConfig{
int a = 0;
double b = 1.0;
std::string c = "default";
};
struct MainConfig {
std::string name = "example";
SubConfig subconfig;
double value = 3.14;
std::optional<std::string> help = std::nullopt;
};
int main(const int argc, char** argv) {
fourdst::config::Config<MainConfig> config;
CLI::App app("Example CLI Application with Config");
fourdst::config::register_as_cli(config, app, "cfg");
app.parse(argc, argv);
std::println("Configuration: \n{}", config);
return 0;
}

View File

@@ -1,4 +1,5 @@
executable('simple_config_test', 'simple.cpp', dependencies: [config_dep])
executable('cli_example', 'cli_example.cpp', dependencies: [config_dep, cli11_dep])
if meson.is_cross_build() and host_machine.system() == 'wasm'
executable('simple_config_wasm_test', 'wasm.cpp', dependencies: [config_dep])

View File

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

View File

@@ -1,9 +1,19 @@
/**
* @file base.h
* @brief Core configuration management classes and concepts.
*
* This file defines the `Config` template class which serves as the primary interface
* for managing typed configuration structures. It handles serialization (save), deserialization (load),
* and schema generation using the `reflect-cpp` library.
*/
#pragma once
#include <filesystem>
#include <string>
#include <map>
#include <optional>
#include <format>
#include <vector>
#include <string_view>
#include <type_traits>
#include "fourdst/config/exceptions/exceptions.h"
@@ -11,24 +21,146 @@
#include "rfl/toml.hpp"
#include "rfl/json.hpp"
namespace fourdst::config {
/**
* @brief Concept ensuring a type is suitable for configuration schema.
*
* A valid configuration schema must be:
* - A class or struct (`std::is_class_v`)
* - An aggregate type (`std::is_aggregate_v`), i.e., strict POD-like structure without user-declared constructors.
* - Not a `std::string`.
*
* @tparam T The type to check.
*/
template <typename T>
concept IsConfigSchema =
std::is_class_v<std::decay_t<T>> && // Must be a class/struct
std::is_aggregate_v<std::decay_t<T>> && // Must be an aggregate (POD-like)
!std::same_as<std::decay_t<T>, std::string>; // Explicitly exclude strings
/**
* @brief Policies for handling the root name during configuration loading.
*/
enum class RootNameLoadPolicy {
/**
* @brief Updates the internal root name to match what is found in the file.
*/
FROM_FILE,
/**
* @brief Enforces the current internal root name; loading fails if the file's root name differs.
*/
KEEP_CURRENT
};
/**
* @brief Represents the current state of a Config object.
*/
enum class ConfigState {
/**
* @brief Configuration contains default values and has not been loaded from a file.
*/
DEFAULT,
/**
* @brief Configuration has been successfully populated from a file.
*/
LOADED_FROM_FILE
};
template <typename T>
/**
* @brief Wrapper class for managing strongly-typed configuration structures.
*
* The `Config` class wraps a user-defined aggregate struct `T` and provides methods
* to save/load it to/from TOML files, as well as generate JSON schemas.
*
* It uses `reflect-cpp` to automatically inspect the fields of `T`.
*
* @tparam T The configuration structure type. Must satisfy `IsConfigSchema`.
*
* @par Examples
* Defining a config struct and using `Config`:
* @code
* #include "fourdst/config/config.h"
*
* struct MySettings {
* int threads = 4;
* double timeout = 30.5;
* };
*
* int main() {
* fourdst::config::Config<MySettings> cfg;
*
* // Access values (default)
* std::cout << "Threads: " << cfg->threads << "\n";
*
* // Save default config
* cfg.save("settings.toml");
*
* // Load from file
* cfg.load("settings.toml");
*
* // Save JSON Schema for editors
* cfg.save_schema("schema.json");
*
* return 0;
* }
* @endcode
*/
template <IsConfigSchema T>
class Config {
public:
/**
* @brief Default constructor. Initializes the inner content with default values.
*/
Config() = default;
/**
* @brief Access member of the underlying configuration struct.
* @return Pointer to the constant configuration content.
*/
const T* operator->() const { return &m_content; }
/**
* @brief Get a mutable pointer to the configuration content.
* @return Pointer to the mutable configuration content.
*/
T* write() const { return &m_content; }
/**
* @brief Dereference operator to access the underlying configuration struct.
* @return Reference to the mutable configuration content.
*/
T& operator*() { return m_content; }
/**
* @brief Dereference operator to access the underlying configuration struct.
* @return Reference to the constant configuration content.
*/
const T& operator*() const { return m_content; }
/**
* @brief Explicit accessor for the main configuration content.
* @return Reference to the constant configuration content.
*/
const T& main() const { return m_content; }
/**
* @brief Saves the current configuration to a TOML file.
*
* Wraps the configuration content under the current root name (default "main")
* and writes it to the specified path.
*
* @param path The file path to write to.
* @throws exceptions::ConfigSaveError If the file cannot be opened.
*
* @par Examples
* @code
* cfg.save("config.toml");
* @endcode
*/
void save(std::string_view path) const {
T default_instance{};
std::unordered_map<std::string, T> wrapper;
@@ -46,23 +178,46 @@ namespace fourdst::config {
ofs.close();
}
/**
* @brief Sets the root name/key used in the TOML file.
*
* The default root name is "main". This name appears as the top-level table in the TOML file (e.g., `[main]`).
*
* @param name The new root name.
*/
void set_root_name(const std::string_view name) {
m_root_name = name;
}
/**
* @brief Gets the current root name.
* @return The root name string view.
*/
[[nodiscard]] std::string_view get_root_name() const {
return m_root_name;
}
/**
* @brief Sets the policy for handling root name mismatches during load.
* @param policy The policy (FROM_FILE or KEEP_CURRENT).
*/
void set_root_name_load_policy(const RootNameLoadPolicy policy) {
m_root_name_load_policy = policy;
}
RootNameLoadPolicy get_root_name_load_policy() const {
/**
* @brief Gets the current root name load policy.
* @return The current policy.
*/
[[nodiscard]] RootNameLoadPolicy get_root_name_load_policy() const {
return m_root_name_load_policy;
}
std::string describe_root_name_load_policy() const {
/**
* @brief Returns a string description of the current root name load policy.
* @return "FROM_FILE", "KEEP_CURRENT", or "UNKNOWN".
*/
[[nodiscard]] std::string describe_root_name_load_policy() const {
switch (m_root_name_load_policy) {
case RootNameLoadPolicy::FROM_FILE:
return "FROM_FILE";
@@ -73,6 +228,24 @@ namespace fourdst::config {
}
}
/**
* @brief Loads configuration from a TOML file.
*
* Reads the file, parses it, and updates the internal configuration state.
*
* @param path The file path to read from.
* @throws exceptions::ConfigLoadError If the config is already loaded, file doesn't exist, or root name mismatch (under KEEP_CURRENT policy).
* @throws exceptions::ConfigParseError If the file content is invalid TOML or doesn't match the schema.
*
* @par Examples
* @code
* try {
* cfg.load("config.toml");
* } catch (const fourdst::config::exceptions::ConfigError& e) {
* std::cerr << "Error loading config: " << e.what() << std::endl;
* }
* @endcode
*/
void load(const std::string_view path) {
if (m_state == ConfigState::LOADED_FROM_FILE) {
throw exceptions::ConfigLoadError(
@@ -111,6 +284,19 @@ namespace fourdst::config {
m_state = ConfigState::LOADED_FROM_FILE;
}
/**
* @brief Generates and saves a JSON schema for the configuration structure.
*
* Useful for enabling autocompletion and validation in editors (e.g., VS Code).
*
* @param path The path to save the schema file to.
* @throws exceptions::SchemaSaveError If the file cannot be opened.
*
* @par Examples
* @code
* Config<MyConfig>::save_schema("MyConfig.schema.json");
* @endcode
*/
static void save_schema(const std::string& path) {
using wrapper = std::unordered_map<std::string, T>;
const std::string json_schema = rfl::json::to_schema<wrapper>(rfl::json::pretty);
@@ -126,8 +312,16 @@ namespace fourdst::config {
ofs.close();
}
/**
* @brief Gets the current state of the configuration object.
* @return The current state (DEFAULT or LOADED_FROM_FILE).
*/
[[nodiscard]] ConfigState get_state() const { return m_state; }
/**
* @brief Returns a string description of the current configuration state.
* @return "DEFAULT", "LOADED_FROM_FILE", or "UNKNOWN".
*/
[[nodiscard]] std::string describe_state() const {
switch (m_state) {
case ConfigState::DEFAULT:
@@ -147,16 +341,31 @@ namespace fourdst::config {
};
}
/**
* @brief Formatter specialization for Config<T> to allow easy printing.
*
* This allows a Config object to be directly formatted/printed (e.g., via std::print or std::format).
* It outputs the configuration in its TOML representation.
*
* @par Example
* @code
* std::println("Current Setup:\n{}", config);
* @endcode
*/
template <typename T, typename CharT>
struct std::formatter<fourdst::config::Config<T>, CharT> {
static constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const fourdst::config::Config<T>& config, auto& ctx) const {
const T& inner_value = config.main();
// Create a wrapper map to preserve the root name in the output
std::map<std::string, T> wrapper;
wrapper[std::string(config.get_root_name())] = inner_value;
std::string buffer;
return buffer;
wrapper[std::string(config.get_root_name())] = config.main();
// Serialize to TOML using reflect-cpp
const std::string toml_string = rfl::toml::write(wrapper);
// Write to the formatter output
return std::format_to(ctx.out(), "{}", toml_string);
}
};

View File

@@ -0,0 +1,144 @@
/**
* @file cli.h
* @brief Integration layer between libconfig and CLI applications.
*
* This file contains utilities for automatically mapping C++ configuration structures
* to command-line arguments, primarily supporting the CLI11 library.
*/
#pragma once
#include <type_traits>
#include "fourdst/config/base.h"
#include "rfl.hpp"
namespace fourdst::config {
template <typename T>
struct InspectType;
/**
* @brief Concept that defines the requirements for a CLI application class.
*
* This concept ensures that the CLI application class `T` has an `add_option` member function
* compatible with the signature expected by `register_as_cli`. It is satisfied by `CLI::App` from CLI11.
*
* @tparam T The type to check against the concept.
*/
template <typename T>
concept IsCLIApp = requires(T app, std::string name, std::string description)
{
{app.add_option(name, std::declval<int&>(), description)};
};
/**
* @brief Type trait to determine if a type is a Config wrapper.
*
* This is the base case which inherits from `std::false_type`.
*
* @tparam T The type to inspect.
*/
template <typename T>
struct is_config_wrapper : std::false_type {};
/**
* @brief Specialization of `is_config_wrapper` for `Config<T>`.
*
* This specialization inherits from `std::true_type`, identifying instances of the `Config` class.
*
* @tparam T The underlying configuration struct type.
*/
template <typename T>
struct is_config_wrapper<Config<T>> : std::true_type {};
/**
* @brief Registers configuration structure fields as CLI options.
*
* This function iterates over the members of the provided configuration object using reflection
* and registers each member as a command-line option in the provided CLI application.
*
* If the configuration object contains nested structures, field names are flattened using dot notation
* (e.g., `parent.child.field`).
*
* If `T` is a `Config<U>` wrapper, it automatically unwraps the inner value and adds a footer note
* to the CLI application's help message indicating that options were auto-generated.
*
* @tparam T The type of the configuration object. Can be a raw struct or a `Config<Struct>` wrapper.
* @tparam CliApp The type of the CLI application object. Must satisfy the `IsCLIApp` concept (e.g., `CLI::App`).
* @param config The configuration object to register.
* @param app The CLI application instance to add options to.
* @param prefix Optional prefix for option names. Used internally for recursion; usually omitted by the caller.
*
* @par Examples
* Basic usage with CLI11:
* @code
* #include "CLI/CLI.hpp"
* #include "fourdst/config/config.h"
*
* struct MyOptions {
* int verbosity = 0;
* std::string input_file = "data.txt";
* };
*
* int main(int argc, char** argv) {
* fourdst::config::Config<MyOptions> cfg;
* CLI::App app{"My Application"};
*
* // Automatically adds flags: --verbosity, --input_file
* fourdst::config::register_as_cli(cfg, app);
*
* CLI11_PARSE(app, argc, argv);
*
* // cfg is now populated with values from CLI
* return 0;
* }
* @endcode
*
* Nested structures:
* @code
* struct Server {
* int port = 8080;
* std::string host = "localhost";
* };
*
* struct AppConfig {
* Server server;
* bool dry_run = false;
* };
*
* // In main...
* fourdst::config::Config<AppConfig> cfg;
* // Registers: --server.port, --server.host, --dry_run
* fourdst::config::register_as_cli(cfg, app);
* @endcode
*/
template <typename T, typename CliApp>
void register_as_cli(T& config, CliApp& app, const std::string& prefix="") {
if constexpr (is_config_wrapper<T>::value) {
app.footer("\nNOTE:\n"
"Configuration options were automatically generated from the config schema.\n"
"Use the --help flag to see all available options."
);
register_as_cli(*config, app, prefix);
} else {
auto view = rfl::to_view(config);
view.apply([&](auto f) {
auto& value = f.value();
using ValueType = std::remove_pointer_t<std::decay_t<decltype(value)>>;
const auto name = std::string(f.name());
std::string field_name = prefix.empty() ? name : prefix + "." + name;
if constexpr (IsConfigSchema<ValueType>) {
register_as_cli(*value, app, field_name);
}
else {
app.add_option(
"--" + field_name,
*value,
"Configuration option for " + field_name
);
}
});
}
}
}

View File

@@ -18,8 +18,86 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
/**
* @file config.h
* @brief Main entry point for the fourdst::config library.
*
* This header includes all necessary components of the configuration library,
* providing a unified interface for defining, loading, saving, and integrating
* configuration structures.
*
* @section features Features
* - **Type-safe Configuration**: Define configs using standard C++ structs.
* - **Serialization**: Built-in support for TOML loading and saving via `reflect-cpp`.
* - **Schema Generation**: Generate JSON schemas for editor autocompletion (VS Code, etc.).
* - **CLI Integration**: Seamlessly expose config fields as command-line arguments (supports CLI11).
* - **Error Handling**: Comprehensive exception hierarchy for parsing and I/O errors.
*
* @par Examples
*
* **1. Basic Definition and I/O**
* @code
* #include "fourdst/config/config.h"
*
* struct Physics {
* double gravity = 9.81;
* bool enable_drag = true;
* };
*
* struct AppConfig {
* std::string name = "My Simulation";
* int max_steps = 1000;
* Physics physics;
* };
*
* int main() {
* fourdst::config::Config<AppConfig> cfg;
*
* // Access defaults
* if (cfg->physics.enable_drag) { ... }
*
* // Save to file
* cfg.save("config.toml");
*
* // Load from file
* cfg.load("config.toml");
* }
* @endcode
*
* **2. CLI Integration (CLI11)**
* @code
* #include "CLI/CLI.hpp"
* #include "fourdst/config/config.h"
*
* int main(int argc, char** argv) {
* CLI::App app("Simulation App");
* fourdst::config::Config<AppConfig> cfg;
*
* // Automatically registers flags like --name, --max_steps, --physics.gravity
* fourdst::config::register_as_cli(cfg, app);
*
* CLI11_PARSE(app, argc, argv);
*
* std::cout << "Starting simulation: " << cfg->name << "\n";
* }
* @endcode
*
* **3. Error Handling**
* @code
* try {
* cfg.load("missing_file.toml");
* } catch (const fourdst::config::exceptions::ConfigLoadError& e) {
* std::cerr << "Could not load config: " << e.what() << "\n";
* // Falls back to default values
* } catch (const fourdst::config::exceptions::ConfigParseError& e) {
* std::cerr << "Invalid config file format: " << e.what() << "\n";
* return 1;
* }
* @endcode
*/
#pragma once
#include "fourdst/config/base.h"
#include "fourdst/config/exceptions/exceptions.h"
#include "fourdst/config/cli.h"

View File

@@ -1,13 +1,34 @@
/**
* @file exceptions.h
* @brief Exception classes for the configuration library.
*
* This file defines the hierarchy of exceptions thrown by the `fourdst::config` library.
* All exceptions inherit from `ConfigError`, which in turn inherits from `std::exception`.
*/
#pragma once
#include <stdexcept>
#include <string>
namespace fourdst::config::exceptions {
/**
* @brief Base exception class for all configuration-related errors.
*
* Provides a standard way to catch any error originating from the configuration library.
* Stores a string message describing the error.
*/
class ConfigError : public std::exception {
public:
/**
* @brief Constructs a ConfigError with a specific message.
* @param what The error message.
*/
explicit ConfigError(const std::string & what): m_msg(what) {}
/**
* @brief Returns the error message.
* @return C-style string containing the error message.
*/
[[nodiscard]] const char* what() const noexcept override {
return m_msg.c_str();
}
@@ -15,18 +36,40 @@ namespace fourdst::config::exceptions {
std::string m_msg;
};
/**
* @brief Thrown when saving the configuration to a file fails.
*
* This usually indicates file I/O errors (e.g., permission denied, disk full).
*/
class ConfigSaveError final : public ConfigError {
using ConfigError::ConfigError;
};
/**
* @brief Thrown when loading the configuration from a file fails.
*
* This can occur if the file does not exist, or if there are policy violations
* (e.g., root name mismatch when `KEEP_CURRENT` is set).
*/
class ConfigLoadError final : public ConfigError {
using ConfigError::ConfigError;
};
/**
* @brief Thrown when parsing the configuration file fails.
*
* This indicates that the file exists but contains invalid TOML syntax or
* data that does not match the expected schema type.
*/
class ConfigParseError final : public ConfigError {
using ConfigError::ConfigError;
};
/**
* @brief Thrown when generating or saving the JSON schema fails.
*
* This typically indicates file I/O errors when writing the schema file.
*/
class SchemaSaveError final : public ConfigError {
using ConfigError::ConfigError;
};

View File

@@ -7,5 +7,6 @@ config_headers = files(
'include/fourdst/config/config.h',
'include/fourdst/config/exceptions/exceptions.h',
'include/fourdst/config/base.h',
'include/fourdst/config/cli.h'
)
install_headers(config_headers, subdir : 'fourdst/fourdst/config')

10
subprojects/cli11.wrap Normal file
View File

@@ -0,0 +1,10 @@
[wrap-file]
directory = CLI11-2.6.1
source_url = https://github.com/CLIUtils/CLI11/archive/refs/tags/v2.6.1.tar.gz
source_filename = CLI11-2.6.1.tar.gz
source_hash = 377691f3fac2b340f12a2f79f523c780564578ba3d6eaf5238e9f35895d5ba95
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/cli11_2.6.1-1/CLI11-2.6.1.tar.gz
wrapdb_version = 2.6.1-1
[provide]
dependency_names = CLI11