Skip to content
Merged
20 changes: 13 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
34 changes: 33 additions & 1 deletion apps/main.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
#include <fstream>
#include <iostream>

int main() { std::cout << "Hello World" << std::endl; }
#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
<< " "
"<https://github.com/xtrpg/xtrpg-cpp-server/blob/main/LICENSE>"
<< 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;
}
268 changes: 268 additions & 0 deletions include/xtrpg/config/ConfigManager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#pragma once

#include <algorithm>
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>

#define REGISTER_MODULE_CONFIG(ModuleClass) \
static struct ModuleClass##ConfigRegistrar { \
ModuleClass##ConfigRegistrar() { \
xtrpg::config::ConfigManager::registerStaticModule( \
[]() { return std::make_unique<ModuleClass>(); }); \
} \
} global_##ModuleClass##_config_registrar;

namespace xtrpg::config {

/**
* Supporting basic primitive types.
*/
using ConfigValue = std::variant<bool, int64_t, double, std::string>;

/**
* 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<ConfigValue> fileOverride{std::nullopt};

/** The value overridden by a command-line argument. */
std::optional<ConfigValue> 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<ConfigOption> 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<std::string,
std::unordered_map<std::string, TieredConfigValue>>
m_values;

/**
* Stores the configuration schemas for registered modules, organized by
* section name.
*/
std::unordered_map<std::string, std::vector<ConfigOption>> m_schemas;

/**
* Maps CLI flags to their corresponding section.key for easy lookup during
* command-line parsing. --flag -> "section.key"
*/
std::unordered_map<std::string, std::string> 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<decltype(arg)>;
if constexpr (std::is_same_v<T, bool>)
return arg ? "true" : "false";
else if constexpr (std::is_same_v<T, std::string>)
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<std::unique_ptr<IModuleConfigProvider>()>;

/**
* 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<ProviderFactory> &getRegistry() {
static std::vector<ProviderFactory> 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 <typename T>
std::optional<T> get(const std::string &section,
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<T>(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
Loading
Loading