///|
fn entity_position(entities : Array[Entity], id : Int) -> Int? {
for index, entity in entities {
if entity.id == id {
return Some(index)
}
}
None
}
///|
fn entity_patch_id(operation : EntityPatch) -> Int {
match operation {
Add(entity) => entity.id
Remove(id) => id
Replace(entity) => entity.id
SetAttribute(id~, ..) => id
}
}
///|
fn entity_patch_name(operation : EntityPatch) -> String {
match operation {
Add(entity) => "add " + entity.name
Remove(id) => "remove #" + id.to_string()
Replace(entity) => "replace #" + entity.id.to_string() + " " + entity.name
SetAttribute(id~, index~, ..) =>
"set #" + id.to_string() + " attribute " + index.to_string()
}
}
///|
pub fn EntityPatch::id(self : EntityPatch) -> Int {
entity_patch_id(self)
}
///|
pub fn EntityPatch::description(self : EntityPatch) -> String {
entity_patch_name(self)
}
///|
pub fn ModelPatch::new() -> ModelPatch {
{ operations: [], added: 0, removed: 0, replaced: 0, changed_attributes: 0 }
}
///|
pub fn ModelPatch::operation_count(self : ModelPatch) -> Int {
self.operations.length()
}
///|
pub fn ModelPatch::is_empty(self : ModelPatch) -> Bool {
self.operations.length() == 0
}
///|
pub fn ModelPatch::push(self : ModelPatch, operation : EntityPatch) -> Unit {
self.operations.push(operation)
match operation {
Add(_) => self.added += 1
Remove(_) => self.removed += 1
Replace(_) => self.replaced += 1
SetAttribute(..) => self.changed_attributes += 1
}
}
///|
pub fn ModelPatch::add(self : ModelPatch, entity : Entity) -> Unit {
self.push(Add(entity))
}
///|
pub fn ModelPatch::remove(self : ModelPatch, id : Int) -> Unit {
self.push(Remove(id))
}
///|
pub fn ModelPatch::replace(self : ModelPatch, entity : Entity) -> Unit {
self.push(Replace(entity))
}
///|
pub fn ModelPatch::set_attribute(
self : ModelPatch,
id : Int,
index : Int,
value : Value,
) -> Unit {
self.push(SetAttribute(id~, index~, value~))
}
///|
fn add_entity(
entities : Array[Entity],
entity : Entity,
) -> Unit raise EditError {
if entity_position(entities, entity.id) is Some(_) {
raise DuplicateEntity(id=entity.id)
}
entities.push(entity)
}
///|
fn remove_entity(entities : Array[Entity], id : Int) -> Unit raise EditError {
guard entity_position(entities, id) is Some(_) else {
raise MissingEntity(id~)
}
let remaining = entities.filter(entity => entity.id != id)
entities.clear()
entities.append(remaining)
}
///|
fn replace_entity(
entities : Array[Entity],
replacement : Entity,
) -> Unit raise EditError {
guard entity_position(entities, replacement.id) is Some(position) else {
raise MissingEntity(id=replacement.id)
}
entities[position] = replacement
}
///|
fn set_entity_attribute(
entities : Array[Entity],
id : Int,
index : Int,
value : Value,
) -> Unit raise EditError {
guard entity_position(entities, id) is Some(position) else {
raise MissingEntity(id~)
}
if index < 0 || index >= entities[position].arguments.length() {
raise InvalidAttribute(index~)
}
let entity = entities[position]
let arguments = entity.arguments.copy()
arguments[index] = value
entities[position] = { ..entity, arguments, }
}
///|
/// Apply a sequence of edits while preserving the model header and entity order.
pub fn apply_patch(model : Model, patch : ModelPatch) -> Model raise EditError {
if patch.is_empty() {
raise EmptyPatch
}
let entities = model.entities.copy()
for operation in patch.operations {
match operation {
Add(entity) => add_entity(entities, entity)
Remove(id) => remove_entity(entities, id)
Replace(entity) => replace_entity(entities, entity)
SetAttribute(id~, index~, value~) =>
set_entity_attribute(entities, id, index, value)
}
}
{ ..model, entities, }
}
///|
pub fn Model::apply_patch(
self : Model,
patch : ModelPatch,
) -> Model raise EditError {
apply_patch(self, patch)
}
///|
fn patch_from_change(
change : ModelChange,
patch : ModelPatch,
) -> Unit raise EditError {
match change.kind {
Added =>
match change.after {
Some(entity) => patch.add(entity)
None => raise MissingEntity(id=change.entity_id)
}
Removed => patch.remove(change.entity_id)
Modified =>
match change.after {
Some(entity) => patch.replace(entity)
None => raise MissingEntity(id=change.entity_id)
}
}
}
///|
/// Convert a structural diff into a replayable patch.
pub fn patch_between(left : Model, right : Model) -> ModelPatch raise EditError {
let diff = diff_models(left, right)
let patch = ModelPatch::new()
for change in diff.changes {
patch_from_change(change, patch)
}
patch
}
///|
pub fn patch_summary(patch : ModelPatch) -> String {
"operations=" +
patch.operation_count().to_string() +
" added=" +
patch.added.to_string() +
" removed=" +
patch.removed.to_string() +
" replaced=" +
patch.replaced.to_string() +
" attributes=" +
patch.changed_attributes.to_string()
}
///|
fn reverse_operation(operation : EntityPatch, before : Model) -> EntityPatch? {
match operation {
Add(entity) => Some(Remove(entity.id))
Remove(id) => before.index().entity(id).map(entity => Add(entity))
Replace(entity) => before.index().entity(entity.id).map(old => Replace(old))
SetAttribute(id~, index~, ..) =>
match before.index().entity(id) {
Some(entity) =>
entity.arguments
.get(index)
.map(value => SetAttribute(id~, index~, value~))
None => None
}
}
}
///|
/// Build a patch that reverts `patch` against the original model.
pub fn reverse_patch(
before : Model,
patch : ModelPatch,
) -> ModelPatch raise EditError {
let result = ModelPatch::new()
for operation in patch.operations.rev() {
match reverse_operation(operation, before) {
Some(reverse) => result.push(reverse)
None => raise MissingEntity(id=operation.id())
}
}
result
}
///|
pub fn patch_round_trip(before : Model, after : Model) -> Bool raise EditError {
let forward = patch_between(before, after)
let changed = apply_patch(before, forward)
changed.entities.length() == after.entities.length() &&
diff_models(changed, after).is_empty()
}
///|
pub fn patch_descriptions(patch : ModelPatch) -> Array[String] {
patch.operations.map(operation => operation.description())
}
///|
pub fn model_with_entity(
model : Model,
entity : Entity,
) -> Model raise EditError {
let patch = ModelPatch::new()
patch.add(entity)
apply_patch(model, patch)
}
///|
pub fn model_without_entity(model : Model, id : Int) -> Model raise EditError {
let patch = ModelPatch::new()
patch.remove(id)
apply_patch(model, patch)
}