#include "editor/EditorSocialDocument.h" #include #include #include namespace eve::editor { namespace { template EditorResult fail(EditorStatus status,const char* rule,std::string message){return EditorResult::error(status,RuleId(rule),std::move(message));} const EditorValue* field(const EditorValue& value,const char* key){const auto* o=value.getIf();if(!o)return nullptr;const auto f=o->find(key);return f==o->end()?nullptr:&f->second;} EditorValue entityValue(const SocialEntityRecord& e){return EditorValue::Object{{"id",e.id.value()},{"label",e.label},{"category",e.category}};} EditorResult parseEntity(const EditorValue& value){const auto* i=field(value,"id");const auto*l=field(value,"label");const auto*c=field(value,"category");const auto*id=i?i->getIf():nullptr;const auto*label=l?l->getIf():nullptr;const auto*category=c?c->getIf():nullptr;if(!id||id->empty()||!label||label->empty()||!category)return fail(EditorStatus::Rejected,"editor.social.invalid-entity","Social entity requires identity, label and category");return EditorResult::applied({StableId(*id),*label,*category});} EditorValue edgeValue(const SocialEdgeRecord& e){return EditorValue::Object{{"id",e.id.value()},{"source",e.source.value()},{"target",e.target.value()},{"kind",e.kind},{"type",e.type},{"weight",e.weight}};} EditorResult parseEdge(const EditorValue& value){auto str=[&](const char*k)->const std::string*{const auto*v=field(value,k);return v?v->getIf():nullptr;};const auto*w=field(value,"weight");const auto*weight=w?w->getIf():nullptr;const auto*id=str("id"),*source=str("source"),*target=str("target"),*kind=str("kind"),*type=str("type");static const std::set kinds{"owner","controller","assignment","relation"};if(!id||id->empty()||!source||source->empty()||!target||target->empty()||!kind||!kinds.contains(*kind)||!type||!weight||!std::isfinite(*weight)||((*kind=="assignment"||*kind=="relation")&&type->empty()))return fail(EditorStatus::Rejected,"editor.social.invalid-edge","Social edge requires ids, supported kind/type and finite weight");return EditorResult::applied({StableId(*id),StableId(*source),StableId(*target),*kind,*type,*weight});} DomainOperation op(const char*t,const char*inverse,const std::string&target,EditorValue payload,EditorValue undo,const StableId&id){DomainOperation r;r.type=t;r.inverseType=inverse;r.target=TargetId(target);r.payload=std::move(payload);r.inverse=std::move(undo);r.hasInverse=true;r.affectedObjects.push_back({TargetId(target),id.value(),0});return r;} templatestd::vector values(const std::map&m){std::vectorr;for(const auto&[id,v]:m){(void)id;r.push_back(v);}return r;} } SocialDocumentTarget::SocialDocumentTarget(std::string id):id_(std::move(id)){} TargetDescriptor SocialDocumentTarget::describe()const{return{TargetId(id_),"social-document",revision_,false,{editorCapabilityId()}};} void* SocialDocumentTarget::queryCapability(const CapabilityId& capability){return capability==editorCapabilityId()?static_cast(this):nullptr;} EditorResult SocialDocumentTarget::applyDomainOperation(const DomainOperation&o){if(o.target!=TargetId(id_))return fail(EditorStatus::Rejected,"editor.social.target","Operation targets another social document");if(o.type=="social.entity.set.v1"){auto v=parseEntity(o.payload);if(!v.value)return fail(v.status,"editor.social.entity-payload","Invalid entity payload");entities_.insert_or_assign(v.value->id,*v.value);}else if(o.type=="social.edge.set.v1"){auto v=parseEdge(o.payload);if(!v.value)return fail(v.status,"editor.social.edge-payload","Invalid edge payload");if(!entities_.contains(v.value->source)||!entities_.contains(v.value->target))return fail(EditorStatus::Conflict,"editor.social.missing-endpoint","Social edge endpoints must exist");edges_.insert_or_assign(v.value->id,*v.value);}else if(o.type=="social.entity.delete.v1"||o.type=="social.edge.delete.v1"){const auto*id=o.payload.getIf();if(!id)return fail(EditorStatus::Rejected,"editor.social.delete-payload","Delete requires a stable id");const StableId stable(*id);if(o.type=="social.entity.delete.v1"){for(const auto&[edgeId,e]:edges_){(void)edgeId;if(e.source==stable||e.target==stable)return fail(EditorStatus::Conflict,"editor.social.entity-in-use","Entity is referenced by an edge");}if(!entities_.erase(stable))return fail(EditorStatus::NotFound,"editor.social.entity-not-found","Entity was not found");}else if(!edges_.erase(stable))return fail(EditorStatus::NotFound,"editor.social.edge-not-found","Edge was not found");}else return fail(EditorStatus::Rejected,"editor.social.operation","Unsupported social operation");++revision_;dirty_.include(0,0);return EditorResult::applied();} std::unique_ptr SocialDocumentTarget::cloneDomainState()const{return std::make_unique(*this);} EditorResult SocialDocumentTarget::commitDomainState(std::unique_ptrcandidate){auto*t=dynamic_cast(candidate.get());if(!t||t->id_!=id_)return fail(EditorStatus::Rejected,"editor.social.staging","Invalid staged social state");*this=*t;return EditorResult::applied();} std::vector SocialDocumentTarget::entities()const{return values(entities_);}std::vector SocialDocumentTarget::edges()const{return values(edges_);} EditorResult SocialDocumentTarget::makeSetEntity(const SocialEntityRecord&e)const{if(!parseEntity(entityValue(e)).value)return fail(EditorStatus::Rejected,"editor.social.invalid-entity","Cannot plan invalid entity");const auto f=entities_.find(e.id);return EditorResult::applied(op("social.entity.set.v1",f==entities_.end()?"social.entity.delete.v1":"social.entity.set.v1",id_,entityValue(e),f==entities_.end()?EditorValue(e.id.value()):entityValue(f->second),e.id));} EditorResult SocialDocumentTarget::makeDeleteEntity(const StableId&id)const{const auto f=entities_.find(id);if(f==entities_.end())return fail(EditorStatus::NotFound,"editor.social.entity-not-found","Entity was not found");for(const auto&[edgeId,e]:edges_){(void)edgeId;if(e.source==id||e.target==id)return fail(EditorStatus::Conflict,"editor.social.entity-in-use","Entity is referenced by an edge");}return EditorResult::applied(op("social.entity.delete.v1","social.entity.set.v1",id_,id.value(),entityValue(f->second),id));} EditorResult SocialDocumentTarget::makeSetEdge(const SocialEdgeRecord&e)const{if(!parseEdge(edgeValue(e)).value)return fail(EditorStatus::Rejected,"editor.social.invalid-edge","Cannot plan invalid edge");if(!entities_.contains(e.source)||!entities_.contains(e.target))return fail(EditorStatus::Conflict,"editor.social.missing-endpoint","Social edge endpoints must exist");for(const auto&[id,current]:edges_)if(id!=e.id&¤t.source==e.source&¤t.target==e.target&¤t.kind==e.kind&¤t.type==e.type)return fail(EditorStatus::Conflict,"editor.social.duplicate-edge","Equivalent social edge already exists");const auto f=edges_.find(e.id);return EditorResult::applied(op("social.edge.set.v1",f==edges_.end()?"social.edge.delete.v1":"social.edge.set.v1",id_,edgeValue(e),f==edges_.end()?EditorValue(e.id.value()):edgeValue(f->second),e.id));} EditorResult SocialDocumentTarget::makeDeleteEdge(const StableId&id)const{const auto f=edges_.find(id);if(f==edges_.end())return fail(EditorStatus::NotFound,"editor.social.edge-not-found","Edge was not found");return EditorResult::applied(op("social.edge.delete.v1","social.edge.set.v1",id_,id.value(),edgeValue(f->second),id));} std::vector SocialDocumentTarget::validate()const{std::vectorr;std::set>unique;std::mapowners,controllers;for(const auto&[id,e]:edges_){if(!entities_.contains(e.source)||!entities_.contains(e.target))r.push_back({RuleId("editor.social.missing-endpoint"),DiagnosticSeverity::Error,"Edge "+id.value()+" has a missing endpoint"});if(!unique.emplace(e.source,e.target,e.kind,e.type).second)r.push_back({RuleId("editor.social.duplicate-edge"),DiagnosticSeverity::Error,"Duplicate social edge semantics"});if(e.kind=="owner"&&++owners[e.source]>1)r.push_back({RuleId("editor.social.multiple-owners"),DiagnosticSeverity::Error,"Entity has multiple owners"});if(e.kind=="controller"&&++controllers[e.source]>1)r.push_back({RuleId("editor.social.multiple-controllers"),DiagnosticSeverity::Error,"Entity has multiple controllers"});}return r;} EditorValue SocialDocumentTarget::snapshotValue()const{EditorValue::Array entities,edges;for(const auto&[id,v]:entities_){(void)id;entities.push_back(entityValue(v));}for(const auto&[id,v]:edges_){(void)id;edges.push_back(edgeValue(v));}return EditorValue::Object{{"schemaVersion",int64_t{1}},{"entities",std::move(entities)},{"edges",std::move(edges)}};} EditorResult SocialDocumentTarget::loadSnapshot(const EditorValue&s){const auto*v=field(s,"schemaVersion"),*ns=field(s,"entities"),*es=field(s,"edges");const auto*version=v?v->getIf():nullptr;const auto*nodes=ns?ns->getIf():nullptr;const auto*edges=es?es->getIf():nullptr;if(!version||*version!=1||!nodes||!edges)return fail(EditorStatus::Rejected,"editor.social.snapshot","Invalid social snapshot envelope");SocialDocumentTarget c(id_);for(const auto&e:*nodes){auto p=parseEntity(e);if(!p.value||!c.entities_.emplace(p.value->id,*p.value).second)return fail(EditorStatus::Rejected,"editor.social.snapshot-entity","Invalid or duplicate snapshot entity");}for(const auto&e:*edges){auto p=parseEdge(e);if(!p.value||!c.edges_.emplace(p.value->id,*p.value).second)return fail(EditorStatus::Rejected,"editor.social.snapshot-edge","Invalid or duplicate snapshot edge");}for(const auto&d:c.validate())if(d.severity==DiagnosticSeverity::Error)return fail(EditorStatus::Rejected,"editor.social.snapshot-invariant","Social snapshot violates graph invariants");entities_=std::move(c.entities_);edges_=std::move(c.edges_);++revision_;dirty_.include(0,0);return EditorResult::applied();} } // namespace eve::editor