use std::collections::BTreeMap;
use khive_types::{Entity, Id128, Link, Note, NoteStatus};
use crate::{
ChangeSet, CreateOp, CreateTarget, DeleteOp, DeletePreimage, EdgePatch, EdgePreimage,
EntityPatch, EntityPreimage, Envelope, LinkOp, NotePatch, NotePreimage, Op, UpdateOp,
UpdatePatch, UpdatePreimage,
};
#[derive(Clone, Debug, Default)]
pub struct CurrentState {
pub entities: BTreeMap<Id128, Entity>,
pub notes: BTreeMap<Id128, Note>,
pub edges: BTreeMap<Id128, Link>,
}
#[derive(Debug, thiserror::Error)]
pub enum InvertError {
#[error("the inverse envelope must differ from the forward envelope")]
ReusedEnvelope,
#[error("operation {index} ({op}) is not invertible")]
NotInvertible {
index: usize,
op: &'static str,
},
#[error("operation {index} is missing current record {id}")]
MissingCurrent {
index: usize,
id: Id128,
},
#[error("operation {index} record {id} differs in field {field}")]
Invalidated {
index: usize,
id: Id128,
field: &'static str,
},
#[error("operation {index} inverse update is invalid: {reason}")]
InverseUpdate {
index: usize,
reason: String,
},
}
impl ChangeSet {
pub fn invert(
&self,
envelope: Envelope,
current: &CurrentState,
) -> Result<ChangeSet, InvertError> {
if envelope == self.envelope {
return Err(InvertError::ReusedEnvelope);
}
let mut working = current.clone();
let mut ops = Vec::with_capacity(self.ops.len());
for (index, op) in self.ops.iter().enumerate().rev() {
let inverse = match op {
Op::Create(create) => invert_create(index, create, &mut working)?,
Op::Link(link) => invert_link(index, link, &mut working)?,
Op::Update(update) => invert_update(index, update, &mut working)?,
Op::Delete(_) => {
return Err(InvertError::NotInvertible {
index,
op: "delete",
});
}
Op::Merge(_) => {
return Err(InvertError::NotInvertible { index, op: "merge" });
}
};
ops.push(inverse);
}
Ok(ChangeSet::new(envelope, ops))
}
}
fn check(index: usize, id: Id128, field: &'static str, matches: bool) -> Result<(), InvertError> {
if matches {
Ok(())
} else {
Err(InvertError::Invalidated { index, id, field })
}
}
fn invert_create(
index: usize,
create: &CreateOp,
working: &mut CurrentState,
) -> Result<Op, InvertError> {
let id = create.id;
let preimage = match &create.target {
CreateTarget::Entity(fields) => {
let row = working
.entities
.get(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.header.id == id)?;
check(
index,
id,
"namespace",
row.header.namespace == create.namespace,
)?;
check(index, id, "deleted_at", row.deleted_at.is_none())?;
check(index, id, "kind", row.kind == fields.entity_kind)?;
check(
index,
id,
"entity_type",
row.entity_type == fields.entity_type,
)?;
check(index, id, "name", row.name == fields.name)?;
check(
index,
id,
"description",
row.description == fields.description,
)?;
check(index, id, "properties", row.properties == fields.properties)?;
check(index, id, "tags", row.tags == fields.tags)?;
let captured = row.clone();
working.entities.remove(&id);
DeletePreimage::Entity(Box::new(captured))
}
CreateTarget::Note(fields) => {
let row = working
.notes
.get(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.header.id == id)?;
check(
index,
id,
"namespace",
row.header.namespace == create.namespace,
)?;
check(index, id, "deleted_at", row.deleted_at.is_none())?;
check(index, id, "kind", row.kind == fields.note_kind)?;
check(index, id, "status", row.status == NoteStatus::default())?;
check(index, id, "expires_at", row.expires_at.is_none())?;
check(index, id, "content", row.content == fields.content)?;
check(index, id, "properties", row.properties == fields.properties)?;
check(index, id, "tags", row.tags == fields.tags)?;
check(index, id, "salience", row.salience == fields.salience)?;
check(
index,
id,
"decay_factor",
row.decay_factor == fields.decay_factor,
)?;
let captured = row.clone();
working.notes.remove(&id);
DeletePreimage::Note(Box::new(captured))
}
};
Ok(Op::Delete(DeleteOp {
target_id: id,
hard: false,
preimage,
}))
}
fn invert_link(index: usize, link: &LinkOp, working: &mut CurrentState) -> Result<Op, InvertError> {
let id = link.id;
let row = working
.edges
.get(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.id == id)?;
check(
index,
id,
"namespace",
row.namespace == link.namespace.as_str(),
)?;
check(index, id, "deleted_at", row.deleted_at.is_none())?;
check(index, id, "source", row.source == link.source)?;
check(index, id, "target", row.target == link.target)?;
check(index, id, "relation", row.relation == link.relation)?;
check(index, id, "weight", row.weight == link.weight)?;
check(index, id, "properties", row.properties == link.properties)?;
let captured = row.clone();
working.edges.remove(&id);
Ok(Op::Delete(DeleteOp {
target_id: id,
hard: false,
preimage: DeletePreimage::Edge(Box::new(captured)),
}))
}
fn invert_update(
index: usize,
update: &UpdateOp,
working: &mut CurrentState,
) -> Result<Op, InvertError> {
let id = update.target_id();
match (update.patch(), update.preimage()) {
(UpdatePatch::Entity(patch), UpdatePreimage::Entity(prior)) => {
let row = working
.entities
.get_mut(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.header.id == id)?;
if let Some(value) = &patch.name {
check(index, id, "name", &row.name == value)?;
}
if let Some(value) = &patch.description {
check(
index,
id,
"description",
row.description.as_ref() == value.as_ref(),
)?;
}
if let Some(value) = &patch.entity_type {
check(
index,
id,
"entity_type",
row.entity_type.as_ref() == value.as_ref(),
)?;
}
if let Some(value) = &patch.properties {
check(index, id, "properties", &row.properties == value)?;
}
if let Some(value) = &patch.tags {
check(index, id, "tags", &row.tags == value)?;
}
let inverse = swapped_update(index, update)?;
if let Some(value) = &prior.name {
row.name = value.clone();
}
if let Some(value) = &prior.description {
row.description = value.clone();
}
if let Some(value) = &prior.entity_type {
row.entity_type = value.clone();
}
if let Some(value) = &prior.properties {
row.properties = value.clone();
}
if let Some(value) = &prior.tags {
row.tags = value.clone();
}
Ok(Op::Update(inverse))
}
(UpdatePatch::Note(patch), UpdatePreimage::Note(prior)) => {
let row = working
.notes
.get_mut(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.header.id == id)?;
if let Some(value) = &patch.content {
check(index, id, "content", &row.content == value)?;
}
if let Some(value) = patch.salience {
check(index, id, "salience", row.salience == value)?;
}
if let Some(value) = patch.decay_factor {
check(index, id, "decay_factor", row.decay_factor == value)?;
}
if let Some(value) = &patch.properties {
check(index, id, "properties", &row.properties == value)?;
}
if let Some(value) = &patch.tags {
check(index, id, "tags", &row.tags == value)?;
}
let inverse = swapped_update(index, update)?;
if let Some(value) = &prior.content {
row.content = value.clone();
}
if let Some(value) = prior.salience {
row.salience = value;
}
if let Some(value) = prior.decay_factor {
row.decay_factor = value;
}
if let Some(value) = &prior.properties {
row.properties = value.clone();
}
if let Some(value) = &prior.tags {
row.tags = value.clone();
}
Ok(Op::Update(inverse))
}
(UpdatePatch::Edge(patch), UpdatePreimage::Edge(prior)) => {
let row = working
.edges
.get_mut(&id)
.ok_or(InvertError::MissingCurrent { index, id })?;
check(index, id, "id", row.id == id)?;
if let Some(value) = patch.relation {
check(index, id, "relation", row.relation == value)?;
}
if let Some(value) = patch.weight {
check(index, id, "weight", row.weight == value)?;
}
let inverse = swapped_update(index, update)?;
if let Some(value) = prior.relation {
row.relation = value;
}
if let Some(value) = prior.weight {
row.weight = value;
}
Ok(Op::Update(inverse))
}
_ => swapped_update(index, update).map(Op::Update),
}
}
fn swapped_update(index: usize, update: &UpdateOp) -> Result<UpdateOp, InvertError> {
let patch = match update.preimage() {
UpdatePreimage::Entity(prior) => UpdatePatch::Entity(EntityPatch {
name: prior.name.clone(),
description: prior.description.clone(),
properties: prior.properties.clone(),
tags: prior.tags.clone(),
entity_type: prior.entity_type.clone(),
}),
UpdatePreimage::Note(prior) => UpdatePatch::Note(NotePatch {
content: prior.content.clone(),
salience: prior.salience,
decay_factor: prior.decay_factor,
properties: prior.properties.clone(),
tags: prior.tags.clone(),
}),
UpdatePreimage::Edge(prior) => UpdatePatch::Edge(EdgePatch {
relation: prior.relation,
weight: prior.weight,
}),
};
let preimage = match update.patch() {
UpdatePatch::Entity(patch) => UpdatePreimage::Entity(EntityPreimage {
name: patch.name.clone(),
description: patch.description.clone(),
properties: patch.properties.clone(),
tags: patch.tags.clone(),
entity_type: patch.entity_type.clone(),
}),
UpdatePatch::Note(patch) => UpdatePreimage::Note(NotePreimage {
content: patch.content.clone(),
salience: patch.salience,
decay_factor: patch.decay_factor,
properties: patch.properties.clone(),
tags: patch.tags.clone(),
}),
UpdatePatch::Edge(patch) => UpdatePreimage::Edge(EdgePreimage {
relation: patch.relation,
weight: patch.weight,
}),
};
UpdateOp::new(update.target_id(), patch, preimage)
.map_err(|reason| InvertError::InverseUpdate { index, reason })
}