Checkpoint 5: complete asset freshness, schema migrations and editor diagnostics
This commit is contained in:
@@ -100,6 +100,25 @@ SchemaRegistry gameplay_schemas(const Json& manifest) {
|
||||
"Gameplay schema duplicates a builtin or gameplay TypeId: " + id);
|
||||
result.register_schema(schema);
|
||||
}
|
||||
for (const auto& schema : types) {
|
||||
if (!schema.contains("migrations"))
|
||||
continue;
|
||||
require(schema.at("migrations").is_array(), "migration.invalid",
|
||||
"Migrations must be an array of version steps");
|
||||
for (const auto& step : schema.at("migrations")) {
|
||||
require(
|
||||
step.is_object() && step.contains("from_version") &&
|
||||
step.at("from_version").is_number_integer() && step.at("from_version") > 0 &&
|
||||
step.at("from_version") < schema.value("version", 1) &&
|
||||
step.contains("fields") && step.at("fields").is_object(),
|
||||
"migration.invalid", "Migration requires a supported earlier version and fields");
|
||||
for (const auto& [key, unused] : step.items())
|
||||
require(key == "from_version" || key == "fields", "migration.invalid",
|
||||
"Unsupported migration step property: " + key);
|
||||
result.add_migration(schema.at("id"), step.at("from_version").get<int>(),
|
||||
step.at("fields"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool SchemaRegistry::contains(const std::string& type) const {
|
||||
@@ -139,9 +158,25 @@ void SchemaRegistry::validate_component(const Json& component) const {
|
||||
validate_field(value, metadata["fields"][id]);
|
||||
}
|
||||
void SchemaRegistry::add_migration(const std::string& type, int from_version, Json rules) {
|
||||
require(from_version > 0 && rules.is_object(), "migration.invalid", "Invalid migration");
|
||||
require(contains(type) && from_version > 0 && from_version < schema(type).value("version", 1) &&
|
||||
rules.is_object(),
|
||||
"migration.invalid", "Migration requires a registered type and an earlier version");
|
||||
require(!migrations_.contains({type, from_version}), "migration.duplicate",
|
||||
"Migration already exists");
|
||||
for (const auto& [field, rule] : rules.items()) {
|
||||
require(!field.empty() && rule.is_object(), "migration.invalid",
|
||||
"Migration fields require nonempty IDs and rule objects");
|
||||
for (const auto& [operation, value] : rule.items()) {
|
||||
require(operation == "default" || operation == "scale" || operation == "require_manual",
|
||||
"migration.invalid", "Unsupported migration operation: " + operation);
|
||||
if (operation == "scale")
|
||||
require(value.is_number() && std::isfinite(value.get<double>()),
|
||||
"migration.invalid", "Migration scale must be a finite number");
|
||||
else if (operation == "require_manual")
|
||||
require(value.is_boolean(), "migration.invalid",
|
||||
"Migration require_manual must be a boolean");
|
||||
}
|
||||
}
|
||||
migrations_[{type, from_version}] = std::move(rules);
|
||||
}
|
||||
Json SchemaRegistry::migrate_component(const Json& source) const {
|
||||
@@ -150,6 +185,10 @@ Json SchemaRegistry::migrate_component(const Json& source) const {
|
||||
if (!contains(type))
|
||||
return result;
|
||||
const auto current = schema(type).value("version", 1);
|
||||
if (result.contains("version"))
|
||||
require(result.at("version").is_number_integer() && result.at("version") > 0 &&
|
||||
result.at("version") <= std::numeric_limits<int>::max(),
|
||||
"migration.invalid", "Component version must be a positive supported integer");
|
||||
auto version = result.value("version", 1);
|
||||
if (version > current)
|
||||
return result;
|
||||
@@ -163,8 +202,11 @@ Json SchemaRegistry::migrate_component(const Json& source) const {
|
||||
if (rule.contains("scale") && result["fields"].contains(field)) {
|
||||
require(result["fields"][field].is_number(), "migration.type",
|
||||
"Cannot scale a nonnumeric field");
|
||||
result["fields"][field] =
|
||||
const auto scaled =
|
||||
result["fields"][field].get<double>() * rule["scale"].get<double>();
|
||||
require(std::isfinite(scaled), "migration.nonfinite",
|
||||
"Migration produced a non-finite field value");
|
||||
result["fields"][field] = scaled;
|
||||
}
|
||||
if (rule.value("require_manual", false) && result["fields"].contains(field))
|
||||
throw Error("migration.manual", "Field requires explicit manual migration",
|
||||
|
||||
@@ -288,9 +288,6 @@ Json AuthoringService::open(const std::filesystem::path& relative, bool recover)
|
||||
value.data = recovered.at("scene");
|
||||
value.revision = recovered.value("revision", 0u);
|
||||
}
|
||||
for (auto& item : value.data["entities"])
|
||||
for (auto& component : item["components"])
|
||||
component = schemas_.migrate_component(component);
|
||||
documents_.emplace(id, std::move(value));
|
||||
return summary(state(id));
|
||||
}
|
||||
@@ -375,6 +372,24 @@ void AuthoringService::apply(Json& scene, const Json& command) {
|
||||
[&](const Json& value) { return value.at("id") == id; });
|
||||
require(found != values.end(), "component.missing", "Component does not exist");
|
||||
values.erase(found);
|
||||
} else if (op == "component.migrate") {
|
||||
Json* object = nullptr;
|
||||
if (command.contains("instance")) {
|
||||
const auto instance_id = command.at("instance").get<std::string>();
|
||||
for (auto& instance : scene.at("instances"))
|
||||
if (instance.at("id") == instance_id && instance.contains("additions"))
|
||||
for (auto& addition : instance.at("additions"))
|
||||
if (addition.at("id") == command.at("entity"))
|
||||
object = &addition;
|
||||
require(object, "template.addition_missing", "Instance-local entity is unavailable");
|
||||
} else
|
||||
object = &entity(scene, command.at("entity").get<std::string>());
|
||||
auto& value = component(*object, command.at("component").get<std::string>());
|
||||
const auto type = value.at("type").get<std::string>();
|
||||
require(schemas_.contains(type), "schema.missing", "Component schema unavailable: " + type);
|
||||
require(value.value("version", 1) <= schemas_.schema(type).value("version", 1),
|
||||
"migration.future", "Cannot migrate a component from a newer schema");
|
||||
value = schemas_.migrate_component(value);
|
||||
} else if (op == "component.set") {
|
||||
auto& value = component(entity(scene, command.at("entity").get<std::string>()),
|
||||
command.at("component").get<std::string>());
|
||||
@@ -601,9 +616,6 @@ Json AuthoringService::recover(const std::string& id,
|
||||
"recovery.disk_conflict",
|
||||
"Scene file changed or disappeared since recovery was written");
|
||||
}
|
||||
for (auto& item : candidate.data["entities"])
|
||||
for (auto& component : item["components"])
|
||||
component = schemas_.migrate_component(component);
|
||||
validate_scene(candidate.data, schemas_);
|
||||
if (documents_.contains(id)) {
|
||||
const auto& current = state(id);
|
||||
|
||||
@@ -17,7 +17,7 @@ struct Resolver {
|
||||
const SceneLoader& loader;
|
||||
std::string root;
|
||||
Json conflicts = Json::array();
|
||||
std::set<std::string> sources;
|
||||
std::set<std::string> sources{};
|
||||
void conflict(const Json& path, std::string code, const Json& record) {
|
||||
conflicts.push_back(
|
||||
{{"instance_path", path}, {"code", std::move(code)}, {"record", record}});
|
||||
|
||||
Reference in New Issue
Block a user