mod choice;
mod path;
mod registry;
mod value;
mod walk;
pub use choice::{Choice, choices, property_choices};
pub use path::{PathError, PropertyPath};
pub use registry::{NodeKind, Property, properties};
pub use value::{Value, ValueType};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap};
use crate::artist::Artist;
use crate::axes::Axes;
use crate::data::NdArray;
use crate::error::IrError;
use crate::figure::Figure;
use crate::ids::{DataId, NodeId};
use crate::validate::{IssueKind, ValidationIssue, ValidationReport};
use crate::wire;
use walk::Step;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Transaction {
pub edits: Vec<Edit>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Edit {
Set {
node: NodeId,
path: PropertyPath,
value: Value,
},
Insert {
parent: NodeId,
index: Option<u32>,
node: Node,
},
Remove {
node: NodeId,
},
Move {
node: NodeId,
parent: NodeId,
index: Option<u32>,
},
PutData {
id: DataId,
array: NdArray,
},
AppendData {
id: DataId,
array: NdArray,
retain: Option<u64>,
},
RemoveData {
id: DataId,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Node {
Axes(Box<Axes>),
Artist(Artist),
}
impl Node {
pub fn id(&self) -> NodeId {
match self {
Node::Axes(axes) => axes.id,
Node::Artist(artist) => artist.id(),
}
}
fn subtree_ids(&self) -> Vec<NodeId> {
match self {
Node::Axes(axes) => std::iter::once(axes.id)
.chain(axes.artists.iter().map(Artist::id))
.collect(),
Node::Artist(artist) => vec![artist.id()],
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum EditError {
#[error("edit {edit:?}: {node} is not in the figure")]
UnknownNode {
edit: Option<usize>,
node: NodeId,
},
#[error("edit {edit:?}: {id} is not in the data table")]
UnknownData {
edit: Option<usize>,
id: DataId,
},
#[error("edit {edit:?}: {node} has no property {path}")]
UnknownPath {
edit: Option<usize>,
node: NodeId,
path: PropertyPath,
},
#[error("edit {edit:?}: {path} of {node} takes a value of type {expected:?}, not {found:?}")]
TypeMismatch {
edit: Option<usize>,
node: NodeId,
path: PropertyPath,
expected: ValueType,
found: Option<ValueType>,
},
#[error("edit {edit:?}: {path} of {node} names a variant that is not set")]
InactiveVariant {
edit: Option<usize>,
node: NodeId,
path: PropertyPath,
},
#[error("edit {edit:?}: {path} of {node} descends through an absent value")]
AbsentValue {
edit: Option<usize>,
node: NodeId,
path: PropertyPath,
},
#[error("edit {edit:?}: {path} of {node} is read-only")]
ReadOnly {
edit: Option<usize>,
node: NodeId,
path: PropertyPath,
},
#[error("edit {edit:?}: {node} is already in use")]
DuplicateId {
edit: Option<usize>,
node: NodeId,
},
#[error("edit {edit:?}: {parent} cannot hold {node}")]
InvalidParent {
edit: Option<usize>,
node: NodeId,
parent: NodeId,
},
#[error("edit {edit:?}: the figure node {node} cannot be removed or moved")]
RootNode {
edit: Option<usize>,
node: NodeId,
},
#[error("edit {edit:?}: index {index} is beyond the {len} children of {parent}")]
IndexOutOfRange {
edit: Option<usize>,
parent: NodeId,
index: u32,
len: usize,
},
#[error(
"edit {edit:?}: entries of shape {appended:?} cannot be appended to {id} of shape {existing:?}"
)]
ShapeMismatch {
edit: Option<usize>,
id: DataId,
existing: Vec<usize>,
appended: Vec<usize>,
},
#[error("edit {edit:?} is not a set, and an overlay holds only sets")]
NotASet {
edit: Option<usize>,
},
#[error("the transaction would introduce validation errors: {0:?}")]
Invalid(Vec<ValidationIssue>),
}
impl EditError {
pub(crate) fn at_edit(mut self, index: Option<usize>) -> Self {
let edit = match &mut self {
EditError::UnknownNode { edit, .. }
| EditError::UnknownData { edit, .. }
| EditError::UnknownPath { edit, .. }
| EditError::TypeMismatch { edit, .. }
| EditError::InactiveVariant { edit, .. }
| EditError::AbsentValue { edit, .. }
| EditError::ReadOnly { edit, .. }
| EditError::DuplicateId { edit, .. }
| EditError::InvalidParent { edit, .. }
| EditError::RootNode { edit, .. }
| EditError::IndexOutOfRange { edit, .. }
| EditError::ShapeMismatch { edit, .. }
| EditError::NotASet { edit } => edit,
EditError::Invalid(_) => return self,
};
*edit = index;
self
}
}
impl Figure {
pub fn apply(&mut self, transaction: &Transaction) -> Result<Transaction, EditError> {
let before = error_counts(&self.validate());
let mut inverses: Vec<Edit> = Vec::with_capacity(transaction.edits.len());
for (index, edit) in transaction.edits.iter().enumerate() {
match self.apply_edit(edit, Some(index)) {
Ok(inverse) => inverses.push(inverse),
Err(error) => {
self.roll_back(inverses);
return Err(error);
}
}
}
let introduced = introduced_errors(before, self.validate());
if !introduced.is_empty() {
self.roll_back(inverses);
return Err(EditError::Invalid(introduced));
}
inverses.reverse();
Ok(Transaction { edits: inverses })
}
pub fn get(&self, node: NodeId, path: &PropertyPath) -> Result<Value, EditError> {
walk::get_in(self, node, path.segments())
.ok_or(EditError::UnknownNode { edit: None, node })?
.map_err(|step| self.edit_error(None, node, path, step))
}
pub fn node_kind(&self, node: NodeId) -> Option<NodeKind> {
walk::kind_of(self, node)
}
fn edit_error(
&self,
index: Option<usize>,
node: NodeId,
path: &PropertyPath,
step: Step,
) -> EditError {
let listed = || {
self.node_kind(node)
.is_some_and(|kind| properties(kind).iter().any(|p| p.path == *path))
};
let unknown = EditError::UnknownPath {
edit: index,
node,
path: path.clone(),
};
match step {
Step::Unknown => unknown,
Step::ReadOnly => EditError::ReadOnly {
edit: index,
node,
path: path.clone(),
},
Step::Inactive if listed() => EditError::InactiveVariant {
edit: index,
node,
path: path.clone(),
},
Step::Absent if listed() => EditError::AbsentValue {
edit: index,
node,
path: path.clone(),
},
Step::Inactive | Step::Absent => unknown,
Step::Type { expected, found } => EditError::TypeMismatch {
edit: index,
node,
path: path.clone(),
expected,
found,
},
}
}
pub(crate) fn apply_edit(
&mut self,
edit: &Edit,
index: Option<usize>,
) -> Result<Edit, EditError> {
match edit {
Edit::Set { node, path, value } => self.apply_set(*node, path, value.clone(), index),
Edit::Insert {
parent,
index: at,
node,
} => self.apply_insert(*parent, *at, node.clone(), index),
Edit::Remove { node } => self.apply_remove(*node, index),
Edit::Move {
node,
parent,
index: at,
} => self.apply_move(*node, *parent, *at, index),
Edit::PutData { id, array } => Ok(self.apply_put_data(*id, array.clone())),
Edit::AppendData { id, array, retain } => {
self.apply_append_data(*id, array, *retain, index)
}
Edit::RemoveData { id } => match self.data.remove(id) {
Some(array) => Ok(Edit::PutData { id: *id, array }),
None => Err(EditError::UnknownData {
edit: index,
id: *id,
}),
},
}
}
fn roll_back(&mut self, inverses: Vec<Edit>) {
for inverse in inverses.into_iter().rev() {
self.apply_edit(&inverse, None)
.expect("the inverse of an applied edit always applies");
}
}
fn apply_set(
&mut self,
node: NodeId,
path: &PropertyPath,
value: Value,
index: Option<usize>,
) -> Result<Edit, EditError> {
let missing = || EditError::UnknownNode { edit: index, node };
let old = walk::get_in(self, node, path.segments())
.ok_or_else(missing)?
.map_err(|step| self.edit_error(index, node, path, step))?;
let written = walk::set_in(self, node, path.segments(), value).ok_or_else(missing)?;
written.map_err(|step| self.edit_error(index, node, path, step))?;
Ok(Edit::Set {
node,
path: path.clone(),
value: old,
})
}
fn apply_insert(
&mut self,
parent: NodeId,
at: Option<u32>,
node: Node,
index: Option<usize>,
) -> Result<Edit, EditError> {
let id = node.id();
let parent_kind = self.node_kind(parent).ok_or(EditError::UnknownNode {
edit: index,
node: parent,
})?;
let fits = match node {
Node::Axes(_) => parent_kind == NodeKind::Figure,
Node::Artist(_) => parent_kind == NodeKind::Axes,
};
if !fits {
return Err(EditError::InvalidParent {
edit: index,
node: id,
parent,
});
}
let mut used: BTreeSet<NodeId> = self.node_ids().collect();
for new in node.subtree_ids() {
if !used.insert(new) {
return Err(EditError::DuplicateId {
edit: index,
node: new,
});
}
}
let len = match &node {
Node::Axes(_) => self.axes.len(),
Node::Artist(_) => self
.axes(parent)
.expect("the parent was found to be an axes")
.artists
.len(),
};
let at = at.unwrap_or_else(|| as_index(len));
if at as usize > len {
return Err(EditError::IndexOutOfRange {
edit: index,
parent,
index: at,
len,
});
}
let ids = node.subtree_ids();
match node {
Node::Axes(axes) => self.axes.insert(at as usize, *axes),
Node::Artist(artist) => self
.axes_mut(parent)
.expect("the parent was found to be an axes")
.artists
.insert(at as usize, artist),
}
for new in ids {
self.id_allocator.next = self.id_allocator.next.max(new.0.saturating_add(1));
}
Ok(Edit::Remove { node: id })
}
fn apply_remove(&mut self, node: NodeId, index: Option<usize>) -> Result<Edit, EditError> {
let figure = self.id;
if node == figure {
return Err(EditError::RootNode { edit: index, node });
}
if let Some(position) = self.axes.iter().position(|axes| axes.id == node) {
let axes = self.axes.remove(position);
return Ok(Edit::Insert {
parent: figure,
index: Some(as_index(position)),
node: Node::Axes(Box::new(axes)),
});
}
for axes in &mut self.axes {
if let Some(position) = axes.artists.iter().position(|artist| artist.id() == node) {
let artist = axes.artists.remove(position);
return Ok(Edit::Insert {
parent: axes.id,
index: Some(as_index(position)),
node: Node::Artist(artist),
});
}
}
Err(EditError::UnknownNode { edit: index, node })
}
fn apply_move(
&mut self,
node: NodeId,
parent: NodeId,
at: Option<u32>,
index: Option<usize>,
) -> Result<Edit, EditError> {
if node == self.id {
return Err(EditError::RootNode { edit: index, node });
}
let kind = self
.node_kind(node)
.ok_or(EditError::UnknownNode { edit: index, node })?;
let parent_kind = self.node_kind(parent).ok_or(EditError::UnknownNode {
edit: index,
node: parent,
})?;
let wanted = if kind == NodeKind::Axes {
NodeKind::Figure
} else {
NodeKind::Axes
};
if parent_kind != wanted {
return Err(EditError::InvalidParent {
edit: index,
node,
parent,
});
}
if kind == NodeKind::Axes {
let from = self
.axes
.iter()
.position(|axes| axes.id == node)
.expect("the node was found to be an axes");
let len = self.axes.len() - 1;
let to = at.unwrap_or_else(|| as_index(len));
if to as usize > len {
return Err(EditError::IndexOutOfRange {
edit: index,
parent,
index: to,
len,
});
}
let axes = self.axes.remove(from);
self.axes.insert(to as usize, axes);
return Ok(Edit::Move {
node,
parent,
index: Some(as_index(from)),
});
}
let (source, from) = self
.axes
.iter()
.enumerate()
.find_map(|(a, axes)| {
let position = axes.artists.iter().position(|artist| artist.id() == node)?;
Some((a, position))
})
.expect("the node was found to be an artist");
let target = self
.axes
.iter()
.position(|axes| axes.id == parent)
.expect("the parent was found to be an axes");
let len = self.axes[target].artists.len() - usize::from(target == source);
let to = at.unwrap_or_else(|| as_index(len));
if to as usize > len {
return Err(EditError::IndexOutOfRange {
edit: index,
parent,
index: to,
len,
});
}
let artist = self.axes[source].artists.remove(from);
self.axes[target].artists.insert(to as usize, artist);
Ok(Edit::Move {
node,
parent: self.axes[source].id,
index: Some(as_index(from)),
})
}
fn apply_put_data(&mut self, id: DataId, array: NdArray) -> Edit {
match self.data.insert(id, array) {
Some(old) => Edit::PutData { id, array: old },
None => Edit::RemoveData { id },
}
}
fn apply_append_data(
&mut self,
id: DataId,
array: &NdArray,
retain: Option<u64>,
index: Option<usize>,
) -> Result<Edit, EditError> {
let existing = self
.data
.get_mut(&id)
.ok_or(EditError::UnknownData { edit: index, id })?;
let mismatched = existing.shape.is_empty()
|| array.shape.is_empty()
|| existing.shape[1..] != array.shape[1..];
if mismatched {
return Err(EditError::ShapeMismatch {
edit: index,
id,
existing: existing.shape.clone(),
appended: array.shape.clone(),
});
}
let old = existing.clone();
existing.shape[0] += array.shape[0];
existing.values.extend_from_slice(&array.values);
if let Some(retain) = retain {
let kept = usize::try_from(retain).unwrap_or(usize::MAX);
if kept < existing.shape[0] {
let discarded = existing.shape[0] - kept;
let entry: usize = existing.shape[1..].iter().product();
existing.values.drain(..discarded * entry);
existing.shape[0] = kept;
}
}
Ok(Edit::PutData { id, array: old })
}
}
fn as_index(position: usize) -> u32 {
u32::try_from(position).unwrap_or(u32::MAX)
}
fn error_counts(report: &ValidationReport) -> HashMap<(Option<NodeId>, IssueKind), usize> {
let mut counts = HashMap::new();
for issue in &report.errors {
*counts.entry((issue.node, issue.kind)).or_insert(0) += 1;
}
counts
}
fn introduced_errors(
mut before: HashMap<(Option<NodeId>, IssueKind), usize>,
after: ValidationReport,
) -> Vec<ValidationIssue> {
let mut introduced = Vec::new();
for issue in after.errors {
match before.get_mut(&(issue.node, issue.kind)) {
Some(remaining) if *remaining > 0 => *remaining -= 1,
_ => introduced.push(issue),
}
}
introduced
}
impl Transaction {
pub fn to_protobuf(&self) -> Vec<u8> {
use prost::Message;
wire::Transaction::from(self).encode_to_vec()
}
pub fn from_protobuf(bytes: &[u8]) -> Result<Transaction, IrError> {
use prost::Message;
Transaction::try_from(wire::Transaction::decode(bytes)?)
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).expect("a transaction always serialises to JSON")
}
pub fn from_json(json: &str) -> Result<Transaction, IrError> {
Ok(serde_json::from_str(json)?)
}
}
pub(crate) struct NodeTree {
axes: Vec<(NodeId, Vec<NodeId>)>,
}
impl NodeTree {
pub(crate) fn of(figure: &Figure) -> Self {
NodeTree {
axes: figure
.axes
.iter()
.map(|axes| (axes.id, axes.artists.iter().map(Artist::id).collect()))
.collect(),
}
}
pub(crate) fn subtree(&self, node: NodeId) -> Vec<NodeId> {
for (axes, artists) in &self.axes {
if *axes == node {
return std::iter::once(node)
.chain(artists.iter().copied())
.collect();
}
if artists.contains(&node) {
return vec![node];
}
}
Vec::new()
}
pub(crate) fn apply(&mut self, edit: &Edit) {
match edit {
Edit::Insert { parent, node, .. } => match node {
Node::Axes(axes) => self
.axes
.push((axes.id, axes.artists.iter().map(Artist::id).collect())),
Node::Artist(artist) => self.push_artist(*parent, artist.id()),
},
Edit::Remove { node } => self.remove(*node),
Edit::Move { node, parent, .. } if !self.axes.iter().any(|(id, _)| id == node) => {
self.remove(*node);
self.push_artist(*parent, *node);
}
_ => {}
}
}
fn push_artist(&mut self, parent: NodeId, artist: NodeId) {
if let Some((_, artists)) = self.axes.iter_mut().find(|(id, _)| *id == parent) {
artists.push(artist);
}
}
fn remove(&mut self, node: NodeId) {
if let Some(position) = self.axes.iter().position(|(id, _)| *id == node) {
self.axes.remove(position);
return;
}
for (_, artists) in &mut self.axes {
artists.retain(|id| *id != node);
}
}
}