diff --git a/CMakeLists.txt b/CMakeLists.txt index f4d5f40..15fe6b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,11 @@ cmake_minimum_required(VERSION 3.20) +# Auto-detect vcpkg if VCPKG_ROOT environment variable is set +if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) + set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + CACHE STRING "vcpkg toolchain file") +endif() + # If no variable is passed from the environment/command line, use a fallback if(NOT DEFINED BUILD_VERSION) set(BUILD_VERSION "0.1.0") @@ -16,12 +22,6 @@ project(xtrpg_cpp_server VERSION ${BUILD_VERSION} LANGUAGES CXX) # Generate the actual hpp file into the build binary directory configure_file("generated/version.hpp.in" ${CMAKE_BINARY_DIR}/generated/version.hpp) -# Auto-detect vcpkg if VCPKG_ROOT environment variable is set -if(DEFINED ENV{VCPKG_ROOT} AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) - set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" - CACHE STRING "vcpkg toolchain file") -endif() - set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -87,9 +87,15 @@ if(UserModules_FOUND) endforeach() endif() +# Config Server Target +add_library(xtrpg_config STATIC + src/config/ConfigManager.cpp +) +target_include_directories(xtrpg_config PUBLIC include) + # Executable Target add_executable(xtrpg_cpp_server apps/main.cpp) -# target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_core) +target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_config) target_include_directories(xtrpg_cpp_server PRIVATE ${CMAKE_BINARY_DIR}/generated) # If modules produce targets registered via xmpp_register_user_module diff --git a/apps/main.cpp b/apps/main.cpp index e0e347a..2ecab76 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,3 +1,35 @@ +#include #include -int main() { std::cout << "Hello World" << std::endl; } \ No newline at end of file +#include "xtrpg/config/ConfigManager.hpp" + +int main(int argc, char *argv[]) { + + // Output preamble information about the application and its configuration + // system. + std::cout << "XTRPG: A XMPP Server" << std::endl + << "Version: 0.1.0" << std::endl + << "Copyright (C) 2026 XTRPG Contributors" << std::endl + << "License: MIT" << std::endl + << " " + "" + << std::endl + << std::endl + << "This is free and open-source software. You are free to use, " + "modify, and redistribute it under the terms of the MIT License." + << std::endl + << "There is NO WARRANTY for this software." << std::endl + << std::endl; + + // Discover and register all modules' configuration schemas, then load the + // configuration file and parse command-line arguments. + xtrpg::config::ConfigManager configManager; + configManager.registerAllDiscoveredModules(); + std::ifstream configFile("./config.toml"); + if (configFile) { + configManager.loadTomlFile(configFile); + } + configManager.parseCLI(argc, argv); + + return 0; +} \ No newline at end of file diff --git a/include/xtrpg/config/ConfigManager.hpp b/include/xtrpg/config/ConfigManager.hpp new file mode 100644 index 0000000..bd270c2 --- /dev/null +++ b/include/xtrpg/config/ConfigManager.hpp @@ -0,0 +1,268 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REGISTER_MODULE_CONFIG(ModuleClass) \ + static struct ModuleClass##ConfigRegistrar { \ + ModuleClass##ConfigRegistrar() { \ + xtrpg::config::ConfigManager::registerStaticModule( \ + []() { return std::make_unique(); }); \ + } \ + } global_##ModuleClass##_config_registrar; + +namespace xtrpg::config { + +/** + * Supporting basic primitive types. + */ +using ConfigValue = std::variant; + +/** + * Represents a configuration value with tiered overrides, allowing for + * system defaults, file-based overrides, and command-line argument overrides. + */ +struct TieredConfigValue { + /** The default value for the configuration option. */ + ConfigValue systemDefault; + + /** The value overridden by a configuration file. */ + std::optional fileOverride{std::nullopt}; + + /** The value overridden by a command-line argument. */ + std::optional cliOverride{std::nullopt}; + + /** + * Retrieves the effective value for the configuration option, considering + * the hierarchy of overrides: CLI > File > System Default. + * + * @returns The effective ConfigValue based on the override hierarchy. + */ + ConfigValue getEffectiveValue() const { + return cliOverride.value_or(fileOverride.value_or(systemDefault)); + } +}; + +/** + * Represents a single configuration option with its metadata. + */ +struct ConfigOption { + /** + * Key name for the option, used in TOML and CLI (e.g., "threads" or + * "engine.render_quality"). + */ + std::string key; + + /** + * Default value for the option, used if not overridden by TOML or CLI. + */ + ConfigValue defaultValue; + + /** + * Description of the option, used for help output and documentation. + */ + std::string description; +}; + +/** + * Represents a module's configuration schema, including its name and the + * options it provides. + */ +struct ModuleConfig { + /** + * Name of the module, used as a section header in TOML and CLI (e.g., + * "engine" or "network"). + */ + std::string name; + + /** + * Optional description of the module, used for help output and + * documentation. + */ + std::string description; + + /** + * List of configuration options provided by the module, including their + * keys, default values, descriptions, and optional CLI flags. + */ + std::vector options; +}; + +/** + * Interface for modules to provide their configuration schema to the + * ConfigManager. + */ +class IModuleConfigProvider { +public: + /** + * Virtual destructor to ensure proper cleanup of derived classes. + */ + virtual ~IModuleConfigProvider() = default; + + /** + * Returns the configuration schema for the module, including its name and + * the options it provides. + * + * @returns A ModuleConfig object containing the module's name and its + * configuration options. + */ + virtual ModuleConfig getConfigSchema() const = 0; +}; + +/** + * Manages the configuration system, including loading from TOML files, + * parsing command-line arguments, and providing access to resolved values. + */ +class ConfigManager { +private: + /** + * Stores the resolved configuration values, organized by section and key. + */ + std::unordered_map> + m_values; + + /** + * Stores the configuration schemas for registered modules, organized by + * section name. + */ + std::unordered_map> m_schemas; + + /** + * Maps CLI flags to their corresponding section.key for easy lookup during + * command-line parsing. --flag -> "section.key" + */ + std::unordered_map m_cliFlagMap; + + /** + * Formats a ConfigValue into a string representation for display in help + * output and documentation. + * + * @param val The ConfigValue to format. + * @returns A string representation of the value, suitable for display. + */ + std::string formatValue(const ConfigValue &val) const { + return std::visit( + [](auto &&arg) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) + return arg ? "true" : "false"; + else if constexpr (std::is_same_v) + return "\"" + arg + "\""; + else + return std::to_string(arg); + }, + val); + } + +public: + /** + * Type alias for a factory function that creates an instance of + * IModuleConfigProvider. + */ + using ProviderFactory = + std::function()>; + + /** + * Returns a reference to the static registry of module configuration + * provider factories, allowing modules to register their configuration + * schemas at compile time. + */ + static std::vector &getRegistry() { + static std::vector registry; + return registry; + } + + /** + * Registers a module's configuration provider factory in the static + * registry, allowing it to be discovered and included in the final resolved + * configuration values. + * + * @param factory A factory function that returns a unique_ptr to an + * IModuleConfigProvider instance for the module. + */ + static void registerStaticModule(ProviderFactory factory) { + ConfigManager::getRegistry().push_back(factory); + } + + /** + * Registers all discovered modules from the static registry, allowing their + * configuration schemas to be included in the final resolved configuration + * values. This should be called after all modules have been registered and + * before loading any configuration files or parsing command-line arguments. + */ + void registerAllDiscoveredModules() { + for (const auto &factory : ConfigManager::getRegistry()) { + auto provider = factory(); + registerModule(*provider); + } + } + + /** + * Registers a module's configuration schema with the ConfigManager, + * allowing it to be included in the final resolved configuration values. + */ + void registerModule(const IModuleConfigProvider &provider); + + /** + * Prints a formatted help menu to the console, including usage instructions + * and descriptions of all registered configuration options. + * + * @param os The output stream to which the help menu will be printed. + * Defaults to std::cout. + */ + void printHelp(std::ostream &os) const; + + /** + * Loads configuration values from a TOML file, parsing the content and + * applying the values to the appropriate sections and keys. + * + * @param input The stream containing the TOML configuration. + */ + bool loadTomlFile(std::istream &input); + + /** + * Parses command-line arguments, updating configuration values based on the + * provided flags and their associated values. + */ + void parseCLI(int argc, char *argv[]); + + /** + * Retrieves the resolved configuration value for a given section and key, + * returning it as the specified type T. Throws an exception if the section + * or key does not exist or if the type does not match. + */ + template + std::optional get(const std::string §ion, + const std::string &key) const { + auto sectionIt = m_values.find(section); + if (sectionIt == m_values.end()) { + return std::nullopt; + } + + auto keyIt = sectionIt->second.find(key); + if (keyIt == sectionIt->second.end()) { + return std::nullopt; + } + + return std::get(keyIt->second.getEffectiveValue()); + } + + /** + * Dumps the resolved configuration values to the specified output stream, + * formatting them in a human-readable way for inspection or debugging. + * + * @param os The output stream to which the resolved configuration will be + * dumped. Defaults to std::cout. + */ + void dumpResolvedConfig(std::ostream &os) const; +}; + +} // namespace xtrpg::config \ No newline at end of file diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp new file mode 100644 index 0000000..9cb86fa --- /dev/null +++ b/src/config/ConfigManager.cpp @@ -0,0 +1,178 @@ +#include "xtrpg/config/ConfigManager.hpp" + +namespace xtrpg::config { + +/** + * Registers a module's configuration schema with the ConfigManager, allowing + * it to be included in the final resolved configuration values. + */ +void ConfigManager::registerModule(const IModuleConfigProvider &provider) { + ModuleConfig schema = provider.getConfigSchema(); + m_schemas[schema.name] = schema.options; + for (const auto &opt : schema.options) { + m_values[schema.name][opt.key] = {.systemDefault = opt.defaultValue}; + } +} + +/** + * Prints a formatted help menu to the console, including usage instructions + * and descriptions of all registered configuration options. + * + * @param os The output stream to which the help menu will be printed. + */ +void ConfigManager::printHelp(std::ostream &os) const { + os << "Usage: app [options]" << std::endl << std::endl; + os << "Options:" << std::endl; + os << " --help, -h Show this help message and exit" << std::endl; + for (const auto &[section, options] : m_schemas) { + os << "[" << section << "]" << std::endl; + for (const auto &opt : options) { + os << "--" << section << "." << opt.key << std::endl + << " " << opt.description + << " (Default: " << formatValue(opt.defaultValue) << ")\n"; + } + os << std::endl; + } +} + +/** + * Loads configuration values from a TOML file, parsing the content and + * applying the values to the appropriate sections and keys. + */ +bool ConfigManager::loadTomlFile(std::istream &input) { + std::string line; + std::string currentSection = "global"; + + while (std::getline(input, line)) { + // Trim simple whitespace + line.erase(0, line.find_first_not_of(" \t\r\n")); + line.erase(line.find_last_not_of(" \t\r\n") + 1); + + if (line.empty() || line[0] == '#') + continue; + + // Section parsing [section] + if (line.front() == '[' && line.back() == ']') { + currentSection = line.substr(1, line.size() - 2); + continue; + } + + // Key-value parsing (key = value) + auto eqPos = line.find('='); + if (eqPos != std::string::npos) { + std::string key = line.substr(0, eqPos); + std::string valStr = line.substr(eqPos + 1); + + // Basic string clean up + key.erase(key.find_last_not_of(" \t") + 1); + valStr.erase(0, valStr.find_first_not_of(" \t")); + + // Deduce type & assign (Overwrites module/compile defaults) + if (valStr == "true") + // Boolean type set to true + m_values[currentSection][key].fileOverride = true; + else if (valStr == "false") + // Boolean type set to false + m_values[currentSection][key].fileOverride = false; + else if (valStr.front() == '"' && valStr.back() == '"') { + // String type + m_values[currentSection][key].fileOverride = + valStr.substr(1, valStr.size() - 2); + } else if (valStr.find('.') != std::string::npos) { + // Double type + m_values[currentSection][key].fileOverride = std::stod(valStr); + } else { + // Integer type + m_values[currentSection][key].fileOverride = std::stoll(valStr); + } + } + } + return true; +} + +/** + * Parses command-line arguments, updating configuration values based on the + * provided flags and their associated values. + */ +void ConfigManager::parseCLI(int argc, char *argv[]) { + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--help" || arg == "-h") { + printHelp(std::cout); + std::exit(0); + } + + // Check registered short/custom flags or dot-notation + // (--section.key=value) + std::string keyPath; + std::string valueStr; + + auto eqPos = arg.find('='); + if (eqPos != std::string::npos) { + keyPath = arg.substr(0, eqPos); + valueStr = arg.substr(eqPos + 1); + } else { + keyPath = arg; + if (i + 1 < argc && argv[i + 1][0] != '-') { + valueStr = argv[++i]; + } + } + + if (keyPath.rfind("--", 0) == 0) { + keyPath = keyPath.substr(2); // Strip leading -- + } + + auto dotPos = keyPath.find('.'); + if (dotPos != std::string::npos) { + std::string section = keyPath.substr(0, dotPos); + std::string key = keyPath.substr(dotPos + 1); + + if (!valueStr.empty()) { + if (valueStr == "true") + m_values[section][key].cliOverride = true; + else if (valueStr == "false") + m_values[section][key].cliOverride = false; + else if (std::all_of(valueStr.begin(), valueStr.end(), ::isdigit)) { + m_values[section][key].cliOverride = std::stoll(valueStr); + } else { + m_values[section][key].cliOverride = valueStr; + } + } + } + } +} + +/** + * Dumps the resolved configuration values to the specified output stream, + * formatting them in a human-readable way for inspection or debugging. + * + * @param os The output stream to which the resolved configuration will be + * dumped. Defaults to std::cout. + */ +void ConfigManager::dumpResolvedConfig(std::ostream &os) const { + + for (const auto &[section, options] : m_values) { + os << "[" << section << "]" << std::endl; + for (const auto &[key, value] : options) { + + // output the key's documentation/description if available + auto schemaIt = m_schemas.find(section); + if (schemaIt != m_schemas.end()) { + const auto &schemaOptions = schemaIt->second; + auto optIt = std::find_if( + schemaOptions.begin(), schemaOptions.end(), + [&key](const ConfigOption &opt) { return opt.key == key; }); + if (optIt != schemaOptions.end()) { + os << "# " << optIt->description << std::endl; + } + } + + // output the key and its corresponding value in a readable format + os << key << " = " << formatValue(value.getEffectiveValue()) << std::endl; + } + os << std::endl; + } +} + +} // namespace xtrpg::config \ No newline at end of file