diff --git a/build-config/CLI11/meson.build b/build-config/CLI11/meson.build new file mode 100644 index 0000000..a8549f3 --- /dev/null +++ b/build-config/CLI11/meson.build @@ -0,0 +1,2 @@ +cli11_proj = subproject('cli11') +cli11_dep = cli11_proj.get_variable('CLI11_dep') \ No newline at end of file diff --git a/build-config/meson.build b/build-config/meson.build index cee4b25..f3f8106 100644 --- a/build-config/meson.build +++ b/build-config/meson.build @@ -1,2 +1,3 @@ cmake = import('cmake') subdir('reflect-cpp') +subdir('cli11') diff --git a/examples/cli_example.cpp b/examples/cli_example.cpp new file mode 100644 index 0000000..d621474 --- /dev/null +++ b/examples/cli_example.cpp @@ -0,0 +1,33 @@ +#include "CLI/CLI.hpp" +#include "fourdst/config/config.h" + +#include +#include +#include + +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 help = std::nullopt; +}; + + +int main(const int argc, char** argv) { + fourdst::config::Config 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; +} \ No newline at end of file diff --git a/examples/meson.build b/examples/meson.build index a428bf0..7075610 100644 --- a/examples/meson.build +++ b/examples/meson.build @@ -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]) diff --git a/meson.build b/meson.build index 5b88099..367afbf 100644 --- a/meson.build +++ b/meson.build @@ -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') diff --git a/src/config/include/fourdst/config/base.h b/src/config/include/fourdst/config/base.h index ef0b3ec..a18e278 100644 --- a/src/config/include/fourdst/config/base.h +++ b/src/config/include/fourdst/config/base.h @@ -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 #include #include -#include #include +#include +#include +#include #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 + concept IsConfigSchema = + std::is_class_v> && // Must be a class/struct + std::is_aggregate_v> && // Must be an aggregate (POD-like) + !std::same_as, 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 + + + /** + * @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 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 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 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::save_schema("MyConfig.schema.json"); + * @endcode + */ static void save_schema(const std::string& path) { using wrapper = std::unordered_map; const std::string json_schema = rfl::json::to_schema(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 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 struct std::formatter, CharT> { static constexpr auto parse(auto& ctx) { return ctx.begin(); } auto format(const fourdst::config::Config& config, auto& ctx) const { - const T& inner_value = config.main(); + // Create a wrapper map to preserve the root name in the output std::map 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); } }; diff --git a/src/config/include/fourdst/config/cli.h b/src/config/include/fourdst/config/cli.h new file mode 100644 index 0000000..362901c --- /dev/null +++ b/src/config/include/fourdst/config/cli.h @@ -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 +#include "fourdst/config/base.h" +#include "rfl.hpp" + +namespace fourdst::config { + template + 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 + concept IsCLIApp = requires(T app, std::string name, std::string description) + { + {app.add_option(name, std::declval(), 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 + struct is_config_wrapper : std::false_type {}; + + /** + * @brief Specialization of `is_config_wrapper` for `Config`. + * + * This specialization inherits from `std::true_type`, identifying instances of the `Config` class. + * + * @tparam T The underlying configuration struct type. + */ + template + struct is_config_wrapper> : 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` 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` 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 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 cfg; + * // Registers: --server.port, --server.host, --dry_run + * fourdst::config::register_as_cli(cfg, app); + * @endcode + */ + template + void register_as_cli(T& config, CliApp& app, const std::string& prefix="") { + if constexpr (is_config_wrapper::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>; + + const auto name = std::string(f.name()); + std::string field_name = prefix.empty() ? name : prefix + "." + name; + + if constexpr (IsConfigSchema) { + register_as_cli(*value, app, field_name); + } + else { + app.add_option( + "--" + field_name, + *value, + "Configuration option for " + field_name + ); + } + + }); + } + } +} \ No newline at end of file diff --git a/src/config/include/fourdst/config/config.h b/src/config/include/fourdst/config/config.h index f8f8d0a..56cd417 100644 --- a/src/config/include/fourdst/config/config.h +++ b/src/config/include/fourdst/config/config.h @@ -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 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 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" diff --git a/src/config/include/fourdst/config/exceptions/exceptions.h b/src/config/include/fourdst/config/exceptions/exceptions.h index 79b029b..5c04cb9 100644 --- a/src/config/include/fourdst/config/exceptions/exceptions.h +++ b/src/config/include/fourdst/config/exceptions/exceptions.h @@ -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 #include 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; }; diff --git a/src/config/meson.build b/src/config/meson.build index 846b33a..ecd6997 100644 --- a/src/config/meson.build +++ b/src/config/meson.build @@ -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') diff --git a/subprojects/cli11.wrap b/subprojects/cli11.wrap new file mode 100644 index 0000000..0072590 --- /dev/null +++ b/subprojects/cli11.wrap @@ -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 \ No newline at end of file