mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
feat: Add JSON-based configuration service implementation
This commit is contained in:
@@ -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
|
||||
|
||||
157
src/services/impl/json_config_service.cpp
Normal file
157
src/services/impl/json_config_service.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "json_config_service.hpp"
|
||||
#include "../../logging/logger.hpp"
|
||||
#include "../../logging/string_utils.hpp"
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/istreamwrapper.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/prettywriter.h>
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
// Default Vulkan device extensions
|
||||
static const std::vector<const char*> 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<const char*> 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<rapidjson::StringBuffer> 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<std::filesystem::path> 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<uint32_t>(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
|
||||
76
src/services/impl/json_config_service.hpp
Normal file
76
src/services/impl/json_config_service.hpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "../interfaces/i_config_service.hpp"
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<const char*> 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
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user