121 lines
8.6 KiB
C++
121 lines
8.6 KiB
C++
#include <faset/authoring/schema.hpp>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <set>
|
|
|
|
namespace faset::authoring {
|
|
void validate_field(const Json& value,const Json& descriptor) {
|
|
const auto kind=descriptor.value("type",std::string("any"));
|
|
bool valid=true;
|
|
if(kind=="number"||kind=="float") valid=value.is_number()&&std::isfinite(value.get<double>());
|
|
else if(kind=="integer"||kind=="int") valid=value.is_number_integer();
|
|
else if(kind=="boolean"||kind=="bool") valid=value.is_boolean();
|
|
else if(kind=="string"||kind=="asset_ref"||kind=="entity_ref") valid=value.is_string();
|
|
else if(kind=="vec2"||kind=="vec3"||kind=="vec4"||kind=="color") {
|
|
const auto size=kind=="vec2"?2u:(kind=="vec3"?3u:4u);
|
|
valid=value.is_array()&&value.size()==size;
|
|
if(valid) for(const auto& entry:value) valid=valid&&entry.is_number()&&std::isfinite(entry.get<double>());
|
|
} else if(kind=="array") valid=value.is_array();
|
|
else if(kind=="object") valid=value.is_object();
|
|
else require(kind=="any","schema.field_type","Unsupported schema field type: "+kind);
|
|
require(valid,"validation.field_type","Invalid value for field "+descriptor.value("id",std::string("?"))+" (expected "+kind+")");
|
|
if(value.is_number()) {
|
|
if(descriptor.contains("min")) require(value.get<double>()>=descriptor["min"].get<double>(),"validation.minimum","Field is below its minimum");
|
|
if(descriptor.contains("max")) require(value.get<double>()<=descriptor["max"].get<double>(),"validation.maximum","Field exceeds its maximum");
|
|
}
|
|
if(descriptor.contains("enum")) {
|
|
bool found=false;for(const auto& option:descriptor["enum"])found=found||option==value;
|
|
require(found,"validation.enum","Field value is not an allowed choice");
|
|
}
|
|
}
|
|
void SchemaRegistry::register_schema(const Json& value) {
|
|
require(value.is_object()&&value.contains("id")&&value["id"].is_string()&&value.contains("fields")&&value["fields"].is_object(),"schema.invalid","Invalid component schema");
|
|
Json normalized=value;
|
|
const auto id=value.at("id").get<std::string>();
|
|
require(!id.empty(),"schema.invalid","TypeId cannot be empty");
|
|
require(value.value("version",1)>0,"schema.invalid","Schema version must be positive");
|
|
for(auto& [key,field]:normalized["fields"].items()) {
|
|
require(field.is_object()&&field.contains("default"),"schema.invalid","Each field requires a typed default");
|
|
require(field.value("id",key)==key,"schema.field_id","Field map keys must be stable FieldIds");
|
|
field["id"]=key;validate_field(field["default"],field);
|
|
}
|
|
if(auto found=schemas_.find(id);found!=schemas_.end()) require(found->second==normalized,"schema.duplicate_type","A different schema is already registered for "+id);
|
|
schemas_[id]=std::move(normalized);
|
|
}
|
|
void SchemaRegistry::register_schemas(const Json& values) {
|
|
const auto& array=values.is_array()?values:values.at("types");
|
|
auto candidate=*this;for(const auto& schema:array)candidate.register_schema(schema);*this=std::move(candidate);
|
|
}
|
|
bool SchemaRegistry::contains(const std::string& type)const{return schemas_.contains(type);}
|
|
Json SchemaRegistry::schema(const std::string& type)const {
|
|
const auto found=schemas_.find(type);require(found!=schemas_.end(),"schema.missing","Component schema unavailable: "+type);return found->second;
|
|
}
|
|
Json SchemaRegistry::manifest()const {Json types=Json::array();for(const auto& [id,type]:schemas_)types.push_back(type);return {{"format","faset.schema"},{"version",1},{"types",types}};}
|
|
Json SchemaRegistry::default_fields(const std::string& type)const {Json fields=Json::object();const auto metadata=schema(type);for(const auto& [id,field]:metadata["fields"].items())fields[id]=field["default"];return fields;}
|
|
void SchemaRegistry::validate_component(const Json& component)const {
|
|
require(component.is_object()&&component.contains("type")&&component["type"].is_string()&&component.contains("fields")&&component["fields"].is_object(),"component.invalid","Invalid component record");
|
|
const auto type=component.at("type").get<std::string>();
|
|
if(!contains(type))return;
|
|
const auto metadata=schema(type);
|
|
// Future or missing-module schemas are preserved, not interpreted with the wrong version.
|
|
if(component.value("version",1)!=metadata.value("version",1))return;
|
|
for(const auto& [id,value]:component["fields"].items())if(metadata["fields"].contains(id))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(!migrations_.contains({type,from_version}),"migration.duplicate","Migration already exists");
|
|
migrations_[{type,from_version}]=std::move(rules);
|
|
}
|
|
Json SchemaRegistry::migrate_component(const Json& source)const {
|
|
Json result=source;const auto type=result.at("type").get<std::string>();
|
|
if(!contains(type))return result;
|
|
const auto current=schema(type).value("version",1);
|
|
auto version=result.value("version",1);
|
|
if(version>current)return result;
|
|
while(version<current) {
|
|
const auto found=migrations_.find({type,version});
|
|
require(found!=migrations_.end(),"migration.required","Explicit migration required for "+type);
|
|
for(const auto& [field,rule]:found->second.items()) {
|
|
if(rule.contains("default")&&!result["fields"].contains(field))result["fields"][field]=rule["default"];
|
|
if(rule.contains("scale")&&result["fields"].contains(field)) {
|
|
require(result["fields"][field].is_number(),"migration.type","Cannot scale a nonnumeric field");
|
|
result["fields"][field]=result["fields"][field].get<double>()*rule["scale"].get<double>();
|
|
}
|
|
if(rule.value("require_manual",false)&&result["fields"].contains(field))throw Error("migration.manual","Field requires explicit manual migration",{{"type",type},{"field",field}});
|
|
}
|
|
result["version"]=++version;
|
|
}
|
|
const auto metadata=schema(type);
|
|
for(const auto& [field,descriptor]:metadata["fields"].items())if(!result["fields"].contains(field))result["fields"][field]=descriptor["default"];
|
|
validate_component(result);return result;
|
|
}
|
|
SchemaRegistry builtin_schemas() {
|
|
SchemaRegistry registry;
|
|
struct Transform {std::array<float,3> position,rotation,scale;};
|
|
TypeRegistration<Transform>(registry,"faset.transform","Transform")
|
|
.field("position","Position",&Transform::position,std::array<float,3>{0,0,0},"vec3")
|
|
.field("rotation","Rotation",&Transform::rotation,std::array<float,3>{0,0,0},"vec3",{{"unit","radians"}})
|
|
.field("scale","Scale",&Transform::scale,std::array<float,3>{1,1,1},"vec3").commit();
|
|
auto add=[&](std::string id,std::string name,Json fields){registry.register_schema({{"id",id},{"name",name},{"version",1},{"fields",fields}});};
|
|
auto field=[](std::string type,Json value){return Json{{"type",type},{"default",value}};};
|
|
add("faset.sprite","Sprite",{{"color",field("color",{0.65,0.6,0.85,1.0})},{"size",field("vec2",{1,1})},{"texture",field("asset_ref","")},{"layer",field("integer",0)}});
|
|
add("faset.mesh","Mesh",{{"asset",field("asset_ref","")},{"color",field("color",{0.65,0.65,0.68,1.0})},{"primitive",Json{{"type","string"},{"default","cube"},{"enum",{"cube","plane","asset"}}}}});
|
|
add("faset.camera","Camera",{{"fov",Json{{"type","number"},{"default",60.0},{"min",1.0},{"max",179.0}}},{"near",Json{{"type","number"},{"default",0.1},{"min",0.001}}},{"far",Json{{"type","number"},{"default",1000.0},{"min",0.01}}}});
|
|
add("faset.light","Directional Light",{{"color",field("color",{1,1,1,1})},{"intensity",Json{{"type","number"},{"default",1.0},{"min",0.0}}}});
|
|
for(int dimension:{2,3}) {
|
|
Json vector=dimension==2?Json{0,0}:Json{0,0,0};Json extents=dimension==2?Json{0.5,0.5}:Json{0.5,0.5,0.5};
|
|
add("faset.rigid_body_"+std::to_string(dimension)+"d","Rigid Body "+std::to_string(dimension)+"D",{
|
|
{"body_type",Json{{"type","string"},{"default","dynamic"},{"enum",{"static","dynamic","kinematic"}}}},
|
|
{"half_extents",field(dimension==2?"vec2":"vec3",extents)},
|
|
{"linear_velocity",field(dimension==2?"vec2":"vec3",vector)},
|
|
{"density",Json{{"type","number"},{"default",1.0},{"min",0.001}}},
|
|
{"friction",Json{{"type","number"},{"default",0.5},{"min",0.0}}},
|
|
{"restitution",Json{{"type","number"},{"default",0.0},{"min",0.0},{"max",1.0}}},
|
|
{"gravity_scale",field("number",1.0)},
|
|
{"category_bits",Json{{"type","integer"},{"default",1},{"min",0}}},
|
|
{"mask_bits",Json{{"type","integer"},{"default",65535},{"min",0}}}});
|
|
}
|
|
return registry;
|
|
}
|
|
}
|