diff --git a/CMakeLists.txt b/CMakeLists.txt index 22d601e..fca3220 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,8 +289,11 @@ set(WORKFLOW_SOURCES src/services/impl/workflow_step_registry.cpp src/services/impl/workflow_executor.cpp src/services/impl/workflow_definition_parser.cpp + src/services/impl/workflow_template_resolver.cpp + src/services/impl/workflow_config_pipeline.cpp src/services/impl/workflow_config_load_step.cpp src/services/impl/workflow_config_version_step.cpp + src/services/impl/workflow_config_migration_step.cpp src/services/impl/workflow_config_schema_step.cpp src/services/impl/workflow_default_step_registrar.cpp ) @@ -579,6 +582,7 @@ add_executable(vulkan_shader_linking_test src/services/impl/bgfx_gui_service.cpp src/services/impl/bgfx_shader_compiler.cpp ${JSON_CONFIG_SOURCES} + ${WORKFLOW_SOURCES} src/services/impl/materialx_shader_generator.cpp src/services/impl/shader_pipeline_validator.cpp src/services/impl/platform_service.cpp @@ -767,6 +771,7 @@ add_test(NAME render_graph_service_test COMMAND render_graph_service_test) add_executable(json_config_merge_test tests/json_config_merge_test.cpp ${JSON_CONFIG_SOURCES} + ${WORKFLOW_SOURCES} ) target_include_directories(json_config_merge_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(json_config_merge_test PRIVATE @@ -780,6 +785,7 @@ add_test(NAME json_config_merge_test COMMAND json_config_merge_test) add_executable(json_config_schema_validation_test tests/json_config_schema_validation_test.cpp ${JSON_CONFIG_SOURCES} + ${WORKFLOW_SOURCES} ) 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/ROADMAP.md b/ROADMAP.md index 0fd73b9..df512d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -237,11 +237,12 @@ Option B: per-shader only ### Status - [~] Workflow core: step registry + executor + JSON definition parser. - [~] Default step package: `config.load`, `config.version.validate`, `config.schema.validate`. +- [~] Boot config workflow execution (load/version/migrate/schema); runtime config parsing still outside workflow. - [x] Workflow schema: `config/schema/workflow_v1.schema.json`. - [x] Template package: `config/workflows/templates/boot_default.json`. ### Next Steps -- Wire boot pipeline to use workflow executor (config load/validate/migrate). +- Move RuntimeConfig parsing into a workflow step. - Add frame workflow template (BeginFrame → RenderGraph → Capture → Validate). ## Feature Matrix (What You Get, When You Get It) diff --git a/config/workflows/templates/boot_default.json b/config/workflows/templates/boot_default.json index 19e684f..1d058d0 100644 --- a/config/workflows/templates/boot_default.json +++ b/config/workflows/templates/boot_default.json @@ -22,6 +22,19 @@ "version": "config.version" } }, + { + "id": "migrate_version", + "plugin": "config.migrate", + "inputs": { + "document": "config.document", + "path": "config.path", + "version": "config.version" + }, + "outputs": { + "document": "config.document", + "version": "config.version" + } + }, { "id": "validate_schema", "plugin": "config.schema.validate", diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index e2d0318..ee89b91 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -1,9 +1,6 @@ #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 "workflow_config_pipeline.hpp" #include "../interfaces/i_logger.hpp" #include #include @@ -120,27 +117,12 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, ", dumpConfig=" + (dumpConfig ? "true" : "false"); logger->Trace("JsonConfigService", "LoadFromJson", args); - 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(json_config::kRuntimeConfigSchemaVersion) + - " (see config/schema/MIGRATIONS.md)"); - } + WorkflowConfigPipeline pipeline(logger, probeService); + std::shared_ptr documentHandle = pipeline.Execute(configPath, nullptr); + if (!documentHandle) { + throw std::runtime_error("JsonConfigService::LoadFromJson: workflow pipeline returned null document"); } - - json_config::JsonConfigSchemaValidator schemaValidator(logger, probeService); - schemaValidator.ValidateOrThrow(document, configPath); + const rapidjson::Document& document = *documentHandle; if (dumpConfig || configJson) { rapidjson::StringBuffer buffer; diff --git a/src/services/impl/workflow_config_migration_step.cpp b/src/services/impl/workflow_config_migration_step.cpp new file mode 100644 index 0000000..07cfe3d --- /dev/null +++ b/src/services/impl/workflow_config_migration_step.cpp @@ -0,0 +1,92 @@ +#include "workflow_config_migration_step.hpp" + +#include "json_config_migration_service.hpp" +#include "json_config_schema_version.hpp" +#include "workflow_step_io_resolver.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowConfigMigrationStep::WorkflowConfigMigrationStep(std::shared_ptr logger, + std::shared_ptr probeService) + : logger_(std::move(logger)), + probeService_(std::move(probeService)) {} + +std::string WorkflowConfigMigrationStep::GetPluginId() const { + return "config.migrate"; +} + +void WorkflowConfigMigrationStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) { + WorkflowStepIoResolver resolver; + const std::string documentKey = resolver.GetRequiredInputKey(step, "document"); + const std::string pathKey = resolver.GetRequiredInputKey(step, "path"); + const std::string versionKey = resolver.GetRequiredInputKey(step, "version"); + const std::string outputDocumentKey = resolver.GetRequiredOutputKey(step, "document"); + const std::string outputVersionKey = resolver.GetRequiredOutputKey(step, "version"); + + const auto* documentHandle = context.TryGet>(documentKey); + if (!documentHandle || !(*documentHandle)) { + throw std::runtime_error("Workflow config.migrate missing document input '" + documentKey + "'"); + } + std::shared_ptr document = *documentHandle; + + const auto* versionHandle = context.TryGet>(versionKey); + if (!versionHandle) { + throw std::runtime_error("Workflow config.migrate missing version input '" + versionKey + "'"); + } + std::optional version = *versionHandle; + + std::filesystem::path pathValue; + if (const auto* path = context.TryGet(pathKey)) { + pathValue = *path; + } else if (const auto* pathString = context.TryGet(pathKey)) { + pathValue = *pathString; + } else { + throw std::runtime_error("Workflow config.migrate missing path input '" + pathKey + "'"); + } + + if (!version) { + if (logger_) { + logger_->Trace("WorkflowConfigMigrationStep", "Execute", + "configPath=" + pathValue.string(), + "No schema version provided; skipping migration"); + } + } else if (*version == json_config::kRuntimeConfigSchemaVersion) { + if (logger_) { + logger_->Trace("WorkflowConfigMigrationStep", "Execute", + "version=" + std::to_string(*version), + "Schema version matches runtime; skipping migration"); + } + } else { + if (logger_) { + logger_->Info("WorkflowConfigMigrationStep: Migrating config from version " + + std::to_string(*version) + " to " + + std::to_string(json_config::kRuntimeConfigSchemaVersion)); + } + json_config::JsonConfigMigrationService migrationService(logger_, probeService_); + const bool migrated = migrationService.Apply(*document, + *version, + json_config::kRuntimeConfigSchemaVersion, + pathValue); + if (!migrated) { + throw std::runtime_error("Unsupported schema version " + std::to_string(*version) + + " in " + pathValue.string() + + "; expected " + std::to_string(json_config::kRuntimeConfigSchemaVersion) + + " (see config/schema/MIGRATIONS.md)"); + } + version = json_config::kRuntimeConfigSchemaVersion; + } + + context.Set(outputDocumentKey, document); + context.Set(outputVersionKey, version); +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_config_migration_step.hpp b/src/services/impl/workflow_config_migration_step.hpp new file mode 100644 index 0000000..b63d9f0 --- /dev/null +++ b/src/services/impl/workflow_config_migration_step.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "../interfaces/i_workflow_step.hpp" +#include "../interfaces/i_logger.hpp" +#include "../interfaces/i_probe_service.hpp" + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowConfigMigrationStep final : public IWorkflowStep { +public: + WorkflowConfigMigrationStep(std::shared_ptr logger, + std::shared_ptr probeService); + + std::string GetPluginId() const override; + void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override; + +private: + std::shared_ptr logger_; + std::shared_ptr probeService_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_config_pipeline.cpp b/src/services/impl/workflow_config_pipeline.cpp new file mode 100644 index 0000000..8a69280 --- /dev/null +++ b/src/services/impl/workflow_config_pipeline.cpp @@ -0,0 +1,68 @@ +#include "workflow_config_pipeline.hpp" + +#include "workflow_default_step_registrar.hpp" +#include "workflow_definition_parser.hpp" +#include "workflow_executor.hpp" +#include "workflow_step_registry.hpp" +#include "workflow_template_resolver.hpp" +#include "../interfaces/i_logger.hpp" +#include "../interfaces/i_probe_service.hpp" + +#include +#include +#include + +namespace sdl3cpp::services::impl { + +WorkflowConfigPipeline::WorkflowConfigPipeline(std::shared_ptr logger, + std::shared_ptr probeService) + : logger_(std::move(logger)), + probeService_(std::move(probeService)) {} + +std::shared_ptr WorkflowConfigPipeline::Execute( + const std::filesystem::path& configPath, + std::optional* versionOut) const { + if (logger_) { + logger_->Trace("WorkflowConfigPipeline", "Execute", + "configPath=" + configPath.string(), + "Starting boot workflow"); + } + + WorkflowTemplateResolver resolver; + const std::filesystem::path templatePath = resolver.ResolveBootTemplate(configPath); + if (templatePath.empty()) { + throw std::runtime_error("WorkflowConfigPipeline: boot workflow template not found for " + + configPath.string()); + } + + WorkflowDefinitionParser parser; + const WorkflowDefinition workflow = parser.ParseFile(templatePath); + + auto registry = std::make_shared(); + WorkflowDefaultStepRegistrar registrar(logger_, probeService_); + registrar.RegisterUsedSteps(workflow, registry); + + WorkflowExecutor executor(registry, logger_); + WorkflowContext context; + context.Set("config.path", configPath); + executor.Execute(workflow, context); + + const auto* documentHandle = context.TryGet>("config.document"); + if (!documentHandle || !(*documentHandle)) { + throw std::runtime_error("WorkflowConfigPipeline: boot workflow did not provide config.document"); + } + if (versionOut) { + const auto* versionHandle = context.TryGet>("config.version"); + *versionOut = versionHandle ? *versionHandle : std::nullopt; + } + + if (logger_) { + logger_->Trace("WorkflowConfigPipeline", "Execute", + "templatePath=" + templatePath.string(), + "Boot workflow complete"); + } + + return *documentHandle; +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_config_pipeline.hpp b/src/services/impl/workflow_config_pipeline.hpp new file mode 100644 index 0000000..a6b3728 --- /dev/null +++ b/src/services/impl/workflow_config_pipeline.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include + +namespace sdl3cpp::services { +class ILogger; +class IProbeService; +} + +namespace sdl3cpp::services::impl { + +class WorkflowConfigPipeline { +public: + WorkflowConfigPipeline(std::shared_ptr logger, + std::shared_ptr probeService); + + std::shared_ptr Execute(const std::filesystem::path& configPath, + std::optional* versionOut) const; + +private: + std::shared_ptr logger_; + std::shared_ptr probeService_; +}; + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_default_step_registrar.cpp b/src/services/impl/workflow_default_step_registrar.cpp index a4918ee..8a629b1 100644 --- a/src/services/impl/workflow_default_step_registrar.cpp +++ b/src/services/impl/workflow_default_step_registrar.cpp @@ -1,5 +1,6 @@ #include "workflow_default_step_registrar.hpp" #include "workflow_config_load_step.hpp" +#include "workflow_config_migration_step.hpp" #include "workflow_config_schema_step.hpp" #include "workflow_config_version_step.hpp" @@ -32,6 +33,9 @@ void WorkflowDefaultStepRegistrar::RegisterUsedSteps( if (plugins.contains("config.version.validate")) { registry->RegisterStep(std::make_shared(logger_)); } + if (plugins.contains("config.migrate")) { + registry->RegisterStep(std::make_shared(logger_, probeService_)); + } if (plugins.contains("config.schema.validate")) { registry->RegisterStep(std::make_shared(logger_, probeService_)); } diff --git a/src/services/impl/workflow_template_resolver.cpp b/src/services/impl/workflow_template_resolver.cpp new file mode 100644 index 0000000..1d25583 --- /dev/null +++ b/src/services/impl/workflow_template_resolver.cpp @@ -0,0 +1,26 @@ +#include "workflow_template_resolver.hpp" + +#include +#include + +namespace sdl3cpp::services::impl { + +std::filesystem::path WorkflowTemplateResolver::ResolveBootTemplate( + const std::filesystem::path& configPath) const { + const std::filesystem::path templateRelative = "workflows/templates/boot_default.json"; + std::vector candidates; + if (!configPath.empty()) { + candidates.push_back(configPath.parent_path() / templateRelative); + } + candidates.push_back(std::filesystem::current_path() / "config" / templateRelative); + + std::error_code ec; + for (const auto& candidate : candidates) { + if (!candidate.empty() && std::filesystem::exists(candidate, ec)) { + return candidate; + } + } + return {}; +} + +} // namespace sdl3cpp::services::impl diff --git a/src/services/impl/workflow_template_resolver.hpp b/src/services/impl/workflow_template_resolver.hpp new file mode 100644 index 0000000..bbf8089 --- /dev/null +++ b/src/services/impl/workflow_template_resolver.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace sdl3cpp::services::impl { + +class WorkflowTemplateResolver { +public: + std::filesystem::path ResolveBootTemplate(const std::filesystem::path& configPath) const; +}; + +} // namespace sdl3cpp::services::impl diff --git a/tests/json_config_merge_test.cpp b/tests/json_config_merge_test.cpp index d5bb9b8..9db0208 100644 --- a/tests/json_config_merge_test.cpp +++ b/tests/json_config_merge_test.cpp @@ -3,10 +3,12 @@ #include "services/impl/json_config_service.hpp" #include "services/interfaces/i_logger.hpp" +#include #include #include #include #include +#include namespace { @@ -67,15 +69,22 @@ void WriteFile(const std::filesystem::path& path, const std::string& contents) { output << contents; } -void CopySchema(const std::filesystem::path& targetDir) { - auto schemaSource = GetRepoRoot() / "config" / "schema" / "runtime_config_v2.schema.json"; - auto schemaTarget = targetDir / "schema" / "runtime_config_v2.schema.json"; - std::filesystem::create_directories(schemaTarget.parent_path()); - std::ifstream input(schemaSource); - ASSERT_TRUE(input.is_open()) << "Missing schema source: " << schemaSource; - std::ofstream output(schemaTarget); - ASSERT_TRUE(output.is_open()) << "Failed to write schema target: " << schemaTarget; - output << input.rdbuf(); +void CopyConfigAssets(const std::filesystem::path& targetDir) { + const auto repoRoot = GetRepoRoot(); + const std::array, 2> assets = { + std::make_pair(repoRoot / "config" / "schema" / "runtime_config_v2.schema.json", + targetDir / "schema" / "runtime_config_v2.schema.json"), + std::make_pair(repoRoot / "config" / "workflows" / "templates" / "boot_default.json", + targetDir / "workflows" / "templates" / "boot_default.json") + }; + for (const auto& asset : assets) { + std::filesystem::create_directories(asset.second.parent_path()); + std::ifstream input(asset.first); + ASSERT_TRUE(input.is_open()) << "Missing config asset: " << asset.first; + std::ofstream output(asset.second); + ASSERT_TRUE(output.is_open()) << "Failed to write config asset: " << asset.second; + output << input.rdbuf(); + } } std::filesystem::path WriteLuaScript(const std::filesystem::path& rootDir) { @@ -86,7 +95,7 @@ std::filesystem::path WriteLuaScript(const std::filesystem::path& rootDir) { TEST(JsonConfigMergeTest, OverlayOverridesBaseFields) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared(); @@ -117,7 +126,7 @@ TEST(JsonConfigMergeTest, OverlayOverridesBaseFields) { TEST(JsonConfigMergeTest, DeleteDirectiveRemovesObject) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared(); @@ -154,7 +163,7 @@ TEST(JsonConfigMergeTest, DeleteDirectiveRemovesObject) { TEST(JsonConfigMergeTest, ExtendsArrayAppliesInOrder) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared(); @@ -190,7 +199,7 @@ TEST(JsonConfigMergeTest, ExtendsArrayAppliesInOrder) { TEST(JsonConfigMergeTest, ExtendsCycleThrows) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared(); diff --git a/tests/json_config_schema_validation_test.cpp b/tests/json_config_schema_validation_test.cpp index b0195d6..ca2e20e 100644 --- a/tests/json_config_schema_validation_test.cpp +++ b/tests/json_config_schema_validation_test.cpp @@ -3,10 +3,12 @@ #include "services/impl/json_config_service.hpp" #include "services/interfaces/i_logger.hpp" +#include #include #include #include #include +#include namespace { @@ -67,15 +69,22 @@ void WriteFile(const std::filesystem::path& path, const std::string& contents) { output << contents; } -void CopySchema(const std::filesystem::path& targetDir) { - auto schemaSource = GetRepoRoot() / "config" / "schema" / "runtime_config_v2.schema.json"; - auto schemaTarget = targetDir / "schema" / "runtime_config_v2.schema.json"; - std::filesystem::create_directories(schemaTarget.parent_path()); - std::ifstream input(schemaSource); - ASSERT_TRUE(input.is_open()) << "Missing schema source: " << schemaSource; - std::ofstream output(schemaTarget); - ASSERT_TRUE(output.is_open()) << "Failed to write schema target: " << schemaTarget; - output << input.rdbuf(); +void CopyConfigAssets(const std::filesystem::path& targetDir) { + const auto repoRoot = GetRepoRoot(); + const std::array, 2> assets = { + std::make_pair(repoRoot / "config" / "schema" / "runtime_config_v2.schema.json", + targetDir / "schema" / "runtime_config_v2.schema.json"), + std::make_pair(repoRoot / "config" / "workflows" / "templates" / "boot_default.json", + targetDir / "workflows" / "templates" / "boot_default.json") + }; + for (const auto& asset : assets) { + std::filesystem::create_directories(asset.second.parent_path()); + std::ifstream input(asset.first); + ASSERT_TRUE(input.is_open()) << "Missing config asset: " << asset.first; + std::ofstream output(asset.second); + ASSERT_TRUE(output.is_open()) << "Failed to write config asset: " << asset.second; + output << input.rdbuf(); + } } void WriteLuaScript(const std::filesystem::path& rootDir) { @@ -84,7 +93,7 @@ void WriteLuaScript(const std::filesystem::path& rootDir) { TEST(JsonConfigSchemaValidationTest, RejectsInvalidWindowWidthType) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared(); @@ -104,7 +113,7 @@ TEST(JsonConfigSchemaValidationTest, RejectsInvalidWindowWidthType) { TEST(JsonConfigSchemaValidationTest, RejectsInvalidSceneSourceEnum) { ScopedTempDir tempDir; - CopySchema(tempDir.Path()); + CopyConfigAssets(tempDir.Path()); WriteLuaScript(tempDir.Path()); auto logger = std::make_shared();