From af01d6daafab7b1aa0d21f7c58792cddde0674ca Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 4 Jan 2026 12:40:36 +0000 Subject: [PATCH] feat: Add JSON-based configuration service implementation --- CMakeLists.txt | 1 + src/services/impl/json_config_service.cpp | 157 +++++++++++++++++++ src/services/impl/json_config_service.hpp | 76 +++++++++ src/services/interfaces/i_config_service.hpp | 1 + 4 files changed, 235 insertions(+) create mode 100644 src/services/impl/json_config_service.cpp create mode 100644 src/services/impl/json_config_service.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d2747fe..8cbf859 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,7 @@ if(BUILD_SDL3_APP) src/core/platform.cpp src/di/service_registry.cpp src/events/event_bus.cpp + src/services/impl/json_config_service.cpp src/app/sdl3_app_core.cpp src/app/audio_player.cpp src/app/sdl3_app_device.cpp diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp new file mode 100644 index 0000000..b36d213 --- /dev/null +++ b/src/services/impl/json_config_service.cpp @@ -0,0 +1,157 @@ +#include "json_config_service.hpp" +#include "../../logging/logger.hpp" +#include "../../logging/string_utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +// Default Vulkan device extensions +static const std::vector kDeviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, +}; + +JsonConfigService::JsonConfigService(const char* argv0) { + config_.scriptPath = FindScriptPath(argv0); +} + +JsonConfigService::JsonConfigService(const std::filesystem::path& configPath, bool dumpConfig) { + config_ = LoadFromJson(configPath, dumpConfig); +} + +JsonConfigService::JsonConfigService(const RuntimeConfig& config) + : config_(config) { +} + +std::vector JsonConfigService::GetDeviceExtensions() const { + return kDeviceExtensions; +} + +std::filesystem::path JsonConfigService::FindScriptPath(const char* argv0) { + std::filesystem::path executable; + if (argv0 && *argv0 != '\0') { + executable = std::filesystem::path(argv0); + if (executable.is_relative()) { + executable = std::filesystem::current_path() / executable; + } + } else { + executable = std::filesystem::current_path(); + } + executable = std::filesystem::weakly_canonical(executable); + std::filesystem::path scriptPath = executable.parent_path() / "scripts" / "cube_logic.lua"; + if (!std::filesystem::exists(scriptPath)) { + throw std::runtime_error("Could not find Lua script at " + scriptPath.string()); + } + return scriptPath; +} + +RuntimeConfig JsonConfigService::LoadFromJson(const std::filesystem::path& configPath, bool dumpConfig) { + using logging::ToString; + logging::Logger::GetInstance().TraceFunctionWithArgs( + "JsonConfigService::LoadFromJson", + configPath.string() + " " + ToString(dumpConfig) + ); + + std::ifstream configStream(configPath); + if (!configStream) { + throw std::runtime_error("Failed to open config file: " + configPath.string()); + } + + rapidjson::IStreamWrapper inputWrapper(configStream); + rapidjson::Document document; + document.ParseStream(inputWrapper); + if (document.HasParseError()) { + throw std::runtime_error("Failed to parse JSON config at " + configPath.string()); + } + if (!document.IsObject()) { + throw std::runtime_error("JSON config must contain an object at the root"); + } + + if (dumpConfig) { + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + writer.SetIndent(' ', 2); + document.Accept(writer); + std::cout << "Loaded runtime config (" << configPath << "):\n" + << buffer.GetString() << '\n'; + } + + const char* scriptField = "lua_script"; + if (!document.HasMember(scriptField) || !document[scriptField].IsString()) { + throw std::runtime_error("JSON config requires a string member '" + std::string(scriptField) + "'"); + } + + std::optional projectRoot; + const char* projectRootField = "project_root"; + if (document.HasMember(projectRootField) && document[projectRootField].IsString()) { + std::filesystem::path candidate(document[projectRootField].GetString()); + if (candidate.is_absolute()) { + projectRoot = std::filesystem::weakly_canonical(candidate); + } else { + projectRoot = std::filesystem::weakly_canonical(configPath.parent_path() / candidate); + } + } + + RuntimeConfig config; + const auto& scriptValue = document[scriptField]; + std::filesystem::path scriptPath(scriptValue.GetString()); + if (!scriptPath.is_absolute()) { + if (projectRoot) { + scriptPath = *projectRoot / scriptPath; + } else { + scriptPath = configPath.parent_path() / scriptPath; + } + } + scriptPath = std::filesystem::weakly_canonical(scriptPath); + if (!std::filesystem::exists(scriptPath)) { + throw std::runtime_error("Lua script not found at " + scriptPath.string()); + } + config.scriptPath = scriptPath; + + auto parseDimension = [&](const char* name, uint32_t defaultValue) -> uint32_t { + if (!document.HasMember(name)) { + return defaultValue; + } + const auto& value = document[name]; + if (value.IsUint()) { + return value.GetUint(); + } + if (value.IsInt()) { + int maybeValue = value.GetInt(); + if (maybeValue >= 0) { + return static_cast(maybeValue); + } + } + throw std::runtime_error(std::string("JSON member '") + name + "' must be a non-negative integer"); + }; + + config.width = parseDimension("window_width", config.width); + config.height = parseDimension("window_height", config.height); + + if (document.HasMember("lua_debug")) { + const auto& value = document["lua_debug"]; + if (!value.IsBool()) { + throw std::runtime_error("JSON member 'lua_debug' must be a boolean"); + } + config.luaDebug = value.GetBool(); + } + + if (document.HasMember("window_title")) { + const auto& value = document["window_title"]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'window_title' must be a string"); + } + config.windowTitle = value.GetString(); + } + + return config; +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp new file mode 100644 index 0000000..9065782 --- /dev/null +++ b/src/services/impl/json_config_service.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "../interfaces/i_config_service.hpp" +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +/** + * @brief Runtime configuration structure. + */ +struct RuntimeConfig { + uint32_t width = 1024; + uint32_t height = 768; + std::filesystem::path scriptPath; + bool luaDebug = false; + std::string windowTitle = "SDL3 Vulkan Demo"; +}; + +/** + * @brief JSON-based configuration service implementation. + * + * Loads application configuration from JSON files or provides defaults. + * Implements the IConfigService interface. + */ +class JsonConfigService : public IConfigService { +public: + /** + * @brief Construct with default configuration. + * + * @param argv0 First command-line argument (for finding default script path) + */ + explicit JsonConfigService(const char* argv0); + + /** + * @brief Construct by loading configuration from JSON. + * + * @param configPath Path to JSON configuration file + * @param dumpConfig Whether to print loaded config to stdout + * @throws std::runtime_error if config file cannot be loaded or is invalid + */ + JsonConfigService(const std::filesystem::path& configPath, bool dumpConfig); + + /** + * @brief Construct with explicit configuration. + * + * @param config Runtime configuration to use + */ + explicit JsonConfigService(const RuntimeConfig& config); + + // IConfigService interface implementation + uint32_t GetWindowWidth() const override { return config_.width; } + uint32_t GetWindowHeight() const override { return config_.height; } + std::filesystem::path GetScriptPath() const override { return config_.scriptPath; } + bool IsLuaDebugEnabled() const override { return config_.luaDebug; } + std::string GetWindowTitle() const override { return config_.windowTitle; } + std::vector GetDeviceExtensions() const override; + + /** + * @brief Get the full runtime configuration. + * + * @return Reference to the config structure + */ + const RuntimeConfig& GetConfig() const { return config_; } + +private: + RuntimeConfig config_; + + // Helper methods moved from main.cpp + static std::filesystem::path FindScriptPath(const char* argv0); + static RuntimeConfig LoadFromJson(const std::filesystem::path& configPath, bool dumpConfig); +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 2ab2608..b6bdbc7 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace sdl3cpp::services {