ROADMAP.md

This commit is contained in:
2026-01-09 20:29:15 +00:00
parent 7097592e64
commit 8018b9225d
8 changed files with 284 additions and 146 deletions

View File

@@ -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

View File

@@ -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<ILogger> logger,
std::shared_ptr<IProbeService> 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

View File

@@ -0,0 +1,30 @@
#pragma once
#include <filesystem>
#include <memory>
#include <rapidjson/document.h>
namespace sdl3cpp::services {
class ILogger;
class IProbeService;
}
namespace sdl3cpp::services::impl::json_config {
class JsonConfigMigrationService {
public:
JsonConfigMigrationService(std::shared_ptr<ILogger> logger,
std::shared_ptr<IProbeService> probeService);
bool Apply(rapidjson::Document& document,
int fromVersion,
int toVersion,
const std::filesystem::path& configPath) const;
private:
std::shared_ptr<ILogger> logger_;
std::shared_ptr<IProbeService> probeService_;
};
} // namespace sdl3cpp::services::impl::json_config

View File

@@ -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 <rapidjson/schema.h>
#include <rapidjson/stringbuffer.h>
#include <stdexcept>
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<ILogger> logger,
std::shared_ptr<IProbeService> 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

View File

@@ -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 <rapidjson/document.h>
#include <rapidjson/schema.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <rapidjson/prettywriter.h>
@@ -10,15 +13,10 @@
#include <iostream>
#include <optional>
#include <stdexcept>
#include <unordered_set>
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<int> 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<int>(value.GetUint());
}
throw std::runtime_error("JSON member '" + std::string(fieldName) + "' must be an integer in " +
configPath.string());
}
std::optional<int> ValidateSchemaVersion(const rapidjson::Value& document,
const std::filesystem::path& configPath,
const std::shared_ptr<ILogger>& 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<ILogger>& logger,
const std::shared_ptr<IProbeService>& 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<ILogger>& logger,
const std::shared_ptr<IProbeService>& 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<ILogger> logger,
@@ -245,24 +120,27 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr<ILogger> logger,
", dumpConfig=" + (dumpConfig ? "true" : "false");
logger->Trace("JsonConfigService", "LoadFromJson", args);
std::unordered_set<std::string> 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());

View File

@@ -0,0 +1,60 @@
#include "json_config_version_validator.hpp"
#include "json_config_schema_version.hpp"
#include "../interfaces/i_logger.hpp"
#include <stdexcept>
namespace sdl3cpp::services::impl::json_config {
namespace {
constexpr const char* kSchemaVersionKey = "schema_version";
constexpr const char* kConfigVersionKey = "configVersion";
}
JsonConfigVersionValidator::JsonConfigVersionValidator(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::optional<int> 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<int>(value.GetUint());
}
throw std::runtime_error("JSON member '" + std::string(fieldName) + "' must be an integer in " +
configPath.string());
}
std::optional<int> 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

View File

@@ -0,0 +1,30 @@
#pragma once
#include <filesystem>
#include <memory>
#include <optional>
#include <rapidjson/document.h>
namespace sdl3cpp::services {
class ILogger;
}
namespace sdl3cpp::services::impl::json_config {
class JsonConfigVersionValidator {
public:
explicit JsonConfigVersionValidator(std::shared_ptr<ILogger> logger);
std::optional<int> Validate(const rapidjson::Value& document,
const std::filesystem::path& configPath) const;
private:
std::optional<int> ReadVersionField(const rapidjson::Value& document,
const char* fieldName,
const std::filesystem::path& configPath) const;
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl::json_config

View File

@@ -1,4 +1,5 @@
#include "json_config_writer_service.hpp"
#include "json_config_schema_version.hpp"
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
@@ -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());