diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a6c046..88d9535 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,6 +280,13 @@ if(BUILD_SDL3_APP) src/events/event_bus.cpp src/services/impl/json_config_service.cpp src/services/impl/json_config_document_loader.cpp + src/services/impl/json_config_document_parser.cpp + src/services/impl/json_config_extend_resolver.cpp + src/services/impl/json_config_merge_service.cpp + src/services/impl/json_config_schema_path_resolver.cpp + src/services/impl/json_config_schema_validator.cpp + src/services/impl/json_config_version_validator.cpp + src/services/impl/json_config_migration_service.cpp src/services/impl/config_compiler_service.cpp src/services/impl/command_line_service.cpp src/services/impl/json_config_writer_service.cpp @@ -547,6 +554,13 @@ add_executable(vulkan_shader_linking_test src/services/impl/bgfx_shader_compiler.cpp src/services/impl/json_config_service.cpp src/services/impl/json_config_document_loader.cpp + src/services/impl/json_config_document_parser.cpp + src/services/impl/json_config_extend_resolver.cpp + src/services/impl/json_config_merge_service.cpp + src/services/impl/json_config_schema_path_resolver.cpp + src/services/impl/json_config_schema_validator.cpp + src/services/impl/json_config_version_validator.cpp + src/services/impl/json_config_migration_service.cpp src/services/impl/materialx_shader_generator.cpp src/services/impl/shader_pipeline_validator.cpp src/services/impl/platform_service.cpp @@ -736,6 +750,13 @@ add_executable(json_config_merge_test tests/json_config_merge_test.cpp src/services/impl/json_config_service.cpp src/services/impl/json_config_document_loader.cpp + src/services/impl/json_config_document_parser.cpp + src/services/impl/json_config_extend_resolver.cpp + src/services/impl/json_config_merge_service.cpp + src/services/impl/json_config_schema_path_resolver.cpp + src/services/impl/json_config_schema_validator.cpp + src/services/impl/json_config_version_validator.cpp + src/services/impl/json_config_migration_service.cpp ) target_include_directories(json_config_merge_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(json_config_merge_test PRIVATE @@ -750,6 +771,13 @@ add_executable(json_config_schema_validation_test tests/json_config_schema_validation_test.cpp src/services/impl/json_config_service.cpp src/services/impl/json_config_document_loader.cpp + src/services/impl/json_config_document_parser.cpp + src/services/impl/json_config_extend_resolver.cpp + src/services/impl/json_config_merge_service.cpp + src/services/impl/json_config_schema_path_resolver.cpp + src/services/impl/json_config_schema_validator.cpp + src/services/impl/json_config_version_validator.cpp + src/services/impl/json_config_migration_service.cpp ) target_include_directories(json_config_schema_validation_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(json_config_schema_validation_test PRIVATE diff --git a/src/services/impl/json_config_migration_service.cpp b/src/services/impl/json_config_migration_service.cpp new file mode 100644 index 0000000..fd97b3d --- /dev/null +++ b/src/services/impl/json_config_migration_service.cpp @@ -0,0 +1,38 @@ +#include "json_config_migration_service.hpp" +#include "../interfaces/i_logger.hpp" +#include "../interfaces/i_probe_service.hpp" + +namespace sdl3cpp::services::impl::json_config { + +JsonConfigMigrationService::JsonConfigMigrationService(std::shared_ptr logger, + std::shared_ptr probeService) + : logger_(std::move(logger)), + probeService_(std::move(probeService)) {} + +bool JsonConfigMigrationService::Apply(rapidjson::Document& document, + int fromVersion, + int toVersion, + const std::filesystem::path& configPath) const { + (void)document; + if (fromVersion == toVersion) { + return true; + } + if (logger_) { + logger_->Warn("JsonConfigService::ApplyMigrations: No migration path from v" + + std::to_string(fromVersion) + " to v" + std::to_string(toVersion) + + " for " + configPath.string()); + } + if (probeService_) { + ProbeReport report{}; + report.severity = ProbeSeverity::Error; + report.code = "CONFIG_MIGRATION_MISSING"; + report.jsonPath = ""; + report.message = "No migration path from v" + std::to_string(fromVersion) + + " to v" + std::to_string(toVersion) + + " (see config/schema/MIGRATIONS.md)"; + probeService_->Report(report); + } + return false; +} + +} // namespace sdl3cpp::services::impl::json_config diff --git a/src/services/impl/json_config_migration_service.hpp b/src/services/impl/json_config_migration_service.hpp new file mode 100644 index 0000000..31c4294 --- /dev/null +++ b/src/services/impl/json_config_migration_service.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +namespace sdl3cpp::services { +class ILogger; +class IProbeService; +} + +namespace sdl3cpp::services::impl::json_config { + +class JsonConfigMigrationService { +public: + JsonConfigMigrationService(std::shared_ptr logger, + std::shared_ptr probeService); + + bool Apply(rapidjson::Document& document, + int fromVersion, + int toVersion, + const std::filesystem::path& configPath) const; + +private: + std::shared_ptr logger_; + std::shared_ptr probeService_; +}; + +} // namespace sdl3cpp::services::impl::json_config diff --git a/src/services/impl/json_config_schema_validator.cpp b/src/services/impl/json_config_schema_validator.cpp new file mode 100644 index 0000000..1fb851b --- /dev/null +++ b/src/services/impl/json_config_schema_validator.cpp @@ -0,0 +1,74 @@ +#include "json_config_schema_validator.hpp" +#include "json_config_document_parser.hpp" +#include "json_config_schema_path_resolver.hpp" +#include "../interfaces/i_logger.hpp" +#include "../interfaces/i_probe_service.hpp" + +#include +#include + +#include + +namespace sdl3cpp::services::impl::json_config { + +namespace { +std::string PointerToString(const rapidjson::Pointer& pointer) { + rapidjson::StringBuffer buffer; + pointer.Stringify(buffer); + return buffer.GetString(); +} +} + +JsonConfigSchemaValidator::JsonConfigSchemaValidator(std::shared_ptr logger, + std::shared_ptr probeService) + : logger_(std::move(logger)), + probeService_(std::move(probeService)) {} + +void JsonConfigSchemaValidator::ValidateOrThrow(const rapidjson::Document& document, + const std::filesystem::path& configPath) const { + JsonConfigSchemaPathResolver resolver; + const auto schemaPath = resolver.Resolve(configPath); + if (schemaPath.empty()) { + if (logger_) { + logger_->Warn("JsonConfigService::ValidateSchemaDocument: Schema file not found for " + + configPath.string()); + } + return; + } + + JsonConfigDocumentParser parser; + rapidjson::Document schemaDocument = parser.Parse(schemaPath, "schema file"); + rapidjson::SchemaDocument schema(schemaDocument); + rapidjson::SchemaValidator validator(schema); + if (!document.Accept(validator)) { + const std::string docPointer = PointerToString(validator.GetInvalidDocumentPointer()); + const std::string schemaPointer = PointerToString(validator.GetInvalidSchemaPointer()); + const std::string keyword = validator.GetInvalidSchemaKeyword(); + const std::string message = "JSON schema validation failed at " + docPointer + + " (schema " + schemaPointer + ", keyword=" + keyword + ")"; + if (logger_) { + logger_->Error("JsonConfigService::ValidateSchemaDocument: " + message + + " configPath=" + configPath.string()); + } + if (probeService_) { + ProbeReport report{}; + report.severity = ProbeSeverity::Error; + report.code = "CONFIG_SCHEMA_INVALID"; + report.jsonPath = docPointer; + report.message = message; + report.details = "schemaPointer=" + schemaPointer + ", keyword=" + keyword; + probeService_->Report(report); + } + throw std::runtime_error("JSON schema validation failed for " + configPath.string() + + " at " + docPointer + " (schema " + schemaPointer + + ", keyword=" + keyword + ")"); + } + if (logger_) { + logger_->Trace("JsonConfigService", "ValidateSchemaDocument", + "schemaPath=" + schemaPath.string() + + ", configPath=" + configPath.string(), + "Schema validation passed"); + } +} + +} // namespace sdl3cpp::services::impl::json_config diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 3f00744..e2d0318 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -1,8 +1,11 @@ #include "json_config_service.hpp" #include "json_config_document_loader.hpp" +#include "json_config_migration_service.hpp" +#include "json_config_schema_validator.hpp" +#include "json_config_schema_version.hpp" +#include "json_config_version_validator.hpp" #include "../interfaces/i_logger.hpp" #include -#include #include #include #include @@ -10,15 +13,10 @@ #include #include #include -#include namespace sdl3cpp::services::impl { namespace { -constexpr int kExpectedSchemaVersion = 2; -constexpr const char* kSchemaVersionKey = "schema_version"; -constexpr const char* kConfigVersionKey = "configVersion"; - const char* SceneSourceName(SceneSource source) { switch (source) { case SceneSource::Config: @@ -39,129 +37,6 @@ SceneSource ParseSceneSource(const std::string& value, const std::string& jsonPa } throw std::runtime_error("JSON member '" + jsonPath + "' must be 'config' or 'lua'"); } - -std::string PointerToString(const rapidjson::Pointer& pointer) { - rapidjson::StringBuffer buffer; - pointer.Stringify(buffer); - return buffer.GetString(); -} - -std::optional ReadVersionField(const rapidjson::Value& document, - const char* fieldName, - const std::filesystem::path& configPath) { - if (!document.HasMember(fieldName)) { - return std::nullopt; - } - const auto& value = document[fieldName]; - if (value.IsInt()) { - return value.GetInt(); - } - if (value.IsUint()) { - return static_cast(value.GetUint()); - } - throw std::runtime_error("JSON member '" + std::string(fieldName) + "' must be an integer in " + - configPath.string()); -} - -std::optional ValidateSchemaVersion(const rapidjson::Value& document, - const std::filesystem::path& configPath, - const std::shared_ptr& logger) { - const auto schemaVersion = ReadVersionField(document, kSchemaVersionKey, configPath); - const auto configVersion = ReadVersionField(document, kConfigVersionKey, configPath); - if (schemaVersion && configVersion && *schemaVersion != *configVersion) { - throw std::runtime_error("JSON members 'schema_version' and 'configVersion' must match in " + - configPath.string()); - } - const auto activeVersion = schemaVersion ? schemaVersion : configVersion; - if (!activeVersion) { - if (logger) { - logger->Warn("JsonConfigService::LoadFromJson: Missing schema version in " + - configPath.string() + "; assuming version " + std::to_string(kExpectedSchemaVersion)); - } - return std::nullopt; - } - if (logger) { - logger->Trace("JsonConfigService", "ValidateSchemaVersion", - "version=" + std::to_string(*activeVersion) + - ", configPath=" + configPath.string()); - } - return activeVersion; -} - -bool ApplyMigrations(rapidjson::Document& document, - int fromVersion, - int toVersion, - const std::filesystem::path& configPath, - const std::shared_ptr& logger, - const std::shared_ptr& probeService) { - (void)document; - if (fromVersion == toVersion) { - return true; - } - if (logger) { - logger->Warn("JsonConfigService::ApplyMigrations: No migration path from v" + - std::to_string(fromVersion) + " to v" + std::to_string(toVersion) + - " for " + configPath.string()); - } - if (probeService) { - ProbeReport report{}; - report.severity = ProbeSeverity::Error; - report.code = "CONFIG_MIGRATION_MISSING"; - report.jsonPath = ""; - report.message = "No migration path from v" + std::to_string(fromVersion) + - " to v" + std::to_string(toVersion) + - " (see config/schema/MIGRATIONS.md)"; - probeService->Report(report); - } - return false; -} - -void ValidateSchemaDocument(const rapidjson::Document& document, - const std::filesystem::path& configPath, - const std::shared_ptr& logger, - const std::shared_ptr& probeService) { - const auto schemaPath = json_config::ResolveSchemaPath(configPath); - if (schemaPath.empty()) { - if (logger) { - logger->Warn("JsonConfigService::ValidateSchemaDocument: Schema file not found for " + - configPath.string()); - } - return; - } - - rapidjson::Document schemaDocument = json_config::ParseJsonDocument(schemaPath, "schema file"); - rapidjson::SchemaDocument schema(schemaDocument); - rapidjson::SchemaValidator validator(schema); - if (!document.Accept(validator)) { - const std::string docPointer = PointerToString(validator.GetInvalidDocumentPointer()); - const std::string schemaPointer = PointerToString(validator.GetInvalidSchemaPointer()); - const std::string keyword = validator.GetInvalidSchemaKeyword(); - const std::string message = "JSON schema validation failed at " + docPointer + - " (schema " + schemaPointer + ", keyword=" + keyword + ")"; - if (logger) { - logger->Error("JsonConfigService::ValidateSchemaDocument: " + message + - " configPath=" + configPath.string()); - } - if (probeService) { - ProbeReport report{}; - report.severity = ProbeSeverity::Error; - report.code = "CONFIG_SCHEMA_INVALID"; - report.jsonPath = docPointer; - report.message = message; - report.details = "schemaPointer=" + schemaPointer + ", keyword=" + keyword; - probeService->Report(report); - } - throw std::runtime_error("JSON schema validation failed for " + configPath.string() + - " at " + docPointer + " (schema " + schemaPointer + - ", keyword=" + keyword + ")"); - } - if (logger) { - logger->Trace("JsonConfigService", "ValidateSchemaDocument", - "schemaPath=" + schemaPath.string() + - ", configPath=" + configPath.string(), - "Schema validation passed"); - } -} } // namespace JsonConfigService::JsonConfigService(std::shared_ptr logger, @@ -245,24 +120,27 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, ", dumpConfig=" + (dumpConfig ? "true" : "false"); logger->Trace("JsonConfigService", "LoadFromJson", args); - std::unordered_set visitedPaths; - rapidjson::Document document = json_config::LoadConfigDocumentRecursive(configPath, logger, visitedPaths); - const auto activeVersion = ValidateSchemaVersion(document, configPath, logger); - if (activeVersion && *activeVersion != kExpectedSchemaVersion) { - const bool migrated = ApplyMigrations(document, - *activeVersion, - kExpectedSchemaVersion, - configPath, - logger, - probeService); + json_config::JsonConfigDocumentLoader documentLoader(logger); + rapidjson::Document document = documentLoader.Load(configPath); + + json_config::JsonConfigVersionValidator versionValidator(logger); + const auto activeVersion = versionValidator.Validate(document, configPath); + if (activeVersion && *activeVersion != json_config::kRuntimeConfigSchemaVersion) { + json_config::JsonConfigMigrationService migrationService(logger, probeService); + const bool migrated = migrationService.Apply(document, + *activeVersion, + json_config::kRuntimeConfigSchemaVersion, + configPath); if (!migrated) { throw std::runtime_error("Unsupported schema version " + std::to_string(*activeVersion) + " in " + configPath.string() + - "; expected " + std::to_string(kExpectedSchemaVersion) + + "; expected " + std::to_string(json_config::kRuntimeConfigSchemaVersion) + " (see config/schema/MIGRATIONS.md)"); } } - ValidateSchemaDocument(document, configPath, logger, probeService); + + json_config::JsonConfigSchemaValidator schemaValidator(logger, probeService); + schemaValidator.ValidateOrThrow(document, configPath); if (dumpConfig || configJson) { rapidjson::StringBuffer buffer; @@ -1308,8 +1186,8 @@ std::string JsonConfigService::BuildConfigJson(const RuntimeConfig& config, target.AddMember(nameValue, stringValue, allocator); }; - document.AddMember("schema_version", kExpectedSchemaVersion, allocator); - document.AddMember("configVersion", kExpectedSchemaVersion, allocator); + document.AddMember("schema_version", json_config::kRuntimeConfigSchemaVersion, allocator); + document.AddMember("configVersion", json_config::kRuntimeConfigSchemaVersion, allocator); rapidjson::Value scriptsObject(rapidjson::kObjectType); addStringMember(scriptsObject, "entry", config.scriptPath.string()); diff --git a/src/services/impl/json_config_version_validator.cpp b/src/services/impl/json_config_version_validator.cpp new file mode 100644 index 0000000..e48ba19 --- /dev/null +++ b/src/services/impl/json_config_version_validator.cpp @@ -0,0 +1,60 @@ +#include "json_config_version_validator.hpp" +#include "json_config_schema_version.hpp" +#include "../interfaces/i_logger.hpp" + +#include + +namespace sdl3cpp::services::impl::json_config { + +namespace { +constexpr const char* kSchemaVersionKey = "schema_version"; +constexpr const char* kConfigVersionKey = "configVersion"; +} + +JsonConfigVersionValidator::JsonConfigVersionValidator(std::shared_ptr logger) + : logger_(std::move(logger)) {} + +std::optional JsonConfigVersionValidator::ReadVersionField( + const rapidjson::Value& document, + const char* fieldName, + const std::filesystem::path& configPath) const { + if (!document.HasMember(fieldName)) { + return std::nullopt; + } + const auto& value = document[fieldName]; + if (value.IsInt()) { + return value.GetInt(); + } + if (value.IsUint()) { + return static_cast(value.GetUint()); + } + throw std::runtime_error("JSON member '" + std::string(fieldName) + "' must be an integer in " + + configPath.string()); +} + +std::optional JsonConfigVersionValidator::Validate(const rapidjson::Value& document, + const std::filesystem::path& configPath) const { + const auto schemaVersion = ReadVersionField(document, kSchemaVersionKey, configPath); + const auto configVersion = ReadVersionField(document, kConfigVersionKey, configPath); + if (schemaVersion && configVersion && *schemaVersion != *configVersion) { + throw std::runtime_error("JSON members 'schema_version' and 'configVersion' must match in " + + configPath.string()); + } + const auto activeVersion = schemaVersion ? schemaVersion : configVersion; + if (!activeVersion) { + if (logger_) { + logger_->Warn("JsonConfigService::LoadFromJson: Missing schema version in " + + configPath.string() + "; assuming version " + + std::to_string(kRuntimeConfigSchemaVersion)); + } + return std::nullopt; + } + if (logger_) { + logger_->Trace("JsonConfigService", "ValidateSchemaVersion", + "version=" + std::to_string(*activeVersion) + + ", configPath=" + configPath.string()); + } + return activeVersion; +} + +} // namespace sdl3cpp::services::impl::json_config diff --git a/src/services/impl/json_config_version_validator.hpp b/src/services/impl/json_config_version_validator.hpp new file mode 100644 index 0000000..a7b2ac6 --- /dev/null +++ b/src/services/impl/json_config_version_validator.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include + +namespace sdl3cpp::services { +class ILogger; +} + +namespace sdl3cpp::services::impl::json_config { + +class JsonConfigVersionValidator { +public: + explicit JsonConfigVersionValidator(std::shared_ptr logger); + + std::optional Validate(const rapidjson::Value& document, + const std::filesystem::path& configPath) const; + +private: + std::optional ReadVersionField(const rapidjson::Value& document, + const char* fieldName, + const std::filesystem::path& configPath) const; + + std::shared_ptr logger_; +}; + +} // namespace sdl3cpp::services::impl::json_config diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index 28af737..7f15534 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -1,4 +1,5 @@ #include "json_config_writer_service.hpp" +#include "json_config_schema_version.hpp" #include #include @@ -41,9 +42,8 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std target.AddMember(nameValue, stringValue, allocator); }; - constexpr int kSchemaVersion = 2; - document.AddMember("schema_version", kSchemaVersion, allocator); - document.AddMember("configVersion", kSchemaVersion, allocator); + document.AddMember("schema_version", json_config::kRuntimeConfigSchemaVersion, allocator); + document.AddMember("configVersion", json_config::kRuntimeConfigSchemaVersion, allocator); rapidjson::Value scriptsObject(rapidjson::kObjectType); addStringMember(scriptsObject, "entry", config.scriptPath.string());