use indexmap::{IndexMap, IndexSet};
use open_gpui::{Bounds, Pixels, Point, Size};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::fmt;
use thiserror::Error;
use crate::format::{
CANVAS_DOCUMENT_FORMAT_VERSION, default_document_format_version, migrate_canvas_snapshot,
};
use crate::geometry_facts::CanvasGeometryFacts;
use crate::mutation::{CanvasCommittedMutation, CanvasMutationJournal, CanvasPreparedMutation};
use crate::relations::{CanvasRecordBindingRelation, CanvasRecordRelations};
use crate::routing::{CanvasDefaultEdgeRouter, CanvasEdgeRouter, CanvasRoutePath};
use crate::schema::{CanvasKindRegistry, CanvasSchemaError};
pub type CanvasValue = Map<String, Value>;
macro_rules! canvas_id {
($name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
};
}
canvas_id!(NodeId);
canvas_id!(EdgeId);
canvas_id!(ShapeId);
canvas_id!(HandleId);
canvas_id!(BindingId);
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CanvasEdgeRouteKind(String);
impl CanvasEdgeRouteKind {
pub const STRAIGHT: &'static str = "straight";
pub const POLYLINE: &'static str = "polyline";
pub const ORTHOGONAL: &'static str = "orthogonal";
pub const CUBIC_BEZIER: &'static str = "cubic-bezier";
pub fn new(kind: impl Into<String>) -> Self {
Self(kind.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for CanvasEdgeRouteKind {
fn default() -> Self {
Self::new(Self::STRAIGHT)
}
}
impl From<&str> for CanvasEdgeRouteKind {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for CanvasEdgeRouteKind {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Display for CanvasEdgeRouteKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum CanvasRecordId {
Node(NodeId),
Edge(EdgeId),
Shape(ShapeId),
}
impl fmt::Display for CanvasRecordId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Node(id) => write!(f, "node:{id}"),
Self::Edge(id) => write!(f, "edge:{id}"),
Self::Shape(id) => write!(f, "shape:{id}"),
}
}
}
impl From<NodeId> for CanvasRecordId {
fn from(value: NodeId) -> Self {
Self::Node(value)
}
}
impl From<EdgeId> for CanvasRecordId {
fn from(value: EdgeId) -> Self {
Self::Edge(value)
}
}
impl From<ShapeId> for CanvasRecordId {
fn from(value: ShapeId) -> Self {
Self::Shape(value)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum HandleRole {
#[default]
Any,
Source,
Target,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum CanvasConnectionEndpointRole {
Source,
Target,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasStyle {
#[serde(default)]
pub fill: Option<String>,
#[serde(default)]
pub stroke: Option<String>,
#[serde(default)]
pub stroke_width: Pixels,
}
impl Default for CanvasStyle {
fn default() -> Self {
Self {
fill: None,
stroke: None,
stroke_width: Pixels::ZERO,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasHandle {
pub id: HandleId,
#[serde(default)]
pub role: HandleRole,
pub position: Point<Pixels>,
#[serde(default = "default_handle_size")]
pub size: Size<Pixels>,
#[serde(default = "default_true")]
pub connectable: bool,
#[serde(default)]
pub hidden: bool,
}
impl CanvasHandle {
pub fn new(id: impl Into<HandleId>, position: Point<Pixels>) -> Self {
Self {
id: id.into(),
role: HandleRole::Any,
position,
size: default_handle_size(),
connectable: true,
hidden: false,
}
}
pub fn bounds_in_node(&self) -> Bounds<Pixels> {
Bounds::centered_at(self.position, self.size)
}
pub fn bounds_in_document(&self, node: &CanvasNode) -> Bounds<Pixels> {
let local = self.bounds_in_node();
Bounds::new(node.position + local.origin, local.size)
}
pub fn accepts_connection_role(&self, role: CanvasConnectionEndpointRole) -> bool {
self.connectable
&& match role {
CanvasConnectionEndpointRole::Source => self.role != HandleRole::Target,
CanvasConnectionEndpointRole::Target => self.role != HandleRole::Source,
}
}
pub fn is_pickable_connection_endpoint(&self, role: CanvasConnectionEndpointRole) -> bool {
!self.hidden && self.accepts_connection_role(role)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasNode {
pub id: NodeId,
#[serde(default = "default_kind")]
pub kind: String,
pub position: Point<Pixels>,
pub size: Size<Pixels>,
#[serde(default)]
pub z_index: i32,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub handles: Vec<CanvasHandle>,
#[serde(default)]
pub data: CanvasValue,
#[serde(default)]
pub style: CanvasStyle,
}
impl CanvasNode {
pub fn new(id: impl Into<NodeId>, position: Point<Pixels>, size: Size<Pixels>) -> Self {
Self {
id: id.into(),
kind: default_kind(),
position,
size,
z_index: 0,
hidden: false,
locked: false,
handles: Vec::new(),
data: CanvasValue::new(),
style: CanvasStyle::default(),
}
}
pub fn bounds(&self) -> Bounds<Pixels> {
Bounds::new(self.position, self.size)
}
pub fn handle(&self, id: Option<&HandleId>) -> Option<&CanvasHandle> {
match id {
Some(id) => self.handles.iter().find(|handle| &handle.id == id),
None => self.handles.first(),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct CanvasEndpoint {
pub node_id: NodeId,
#[serde(default)]
pub handle_id: Option<HandleId>,
}
impl CanvasEndpoint {
pub fn new(node_id: impl Into<NodeId>, handle_id: Option<impl Into<HandleId>>) -> Self {
Self {
node_id: node_id.into(),
handle_id: handle_id.map(Into::into),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasEdgeRoute {
#[serde(default)]
pub kind: CanvasEdgeRouteKind,
#[serde(default)]
pub waypoints: Vec<Point<Pixels>>,
#[serde(default)]
pub control_points: Vec<Point<Pixels>>,
#[serde(default = "default_edge_interaction_width")]
pub interaction_width: Pixels,
#[serde(default)]
pub options: CanvasValue,
}
impl CanvasEdgeRoute {
pub fn new(kind: impl Into<CanvasEdgeRouteKind>) -> Self {
Self {
kind: kind.into(),
..Self::default()
}
}
pub fn straight() -> Self {
Self::new(CanvasEdgeRouteKind::STRAIGHT)
}
pub fn polyline(waypoints: impl IntoIterator<Item = Point<Pixels>>) -> Self {
Self {
kind: CanvasEdgeRouteKind::new(CanvasEdgeRouteKind::POLYLINE),
waypoints: waypoints.into_iter().collect(),
..Self::default()
}
}
pub fn orthogonal() -> Self {
Self::new(CanvasEdgeRouteKind::ORTHOGONAL)
}
}
impl Default for CanvasEdgeRoute {
fn default() -> Self {
Self {
kind: CanvasEdgeRouteKind::default(),
waypoints: Vec::new(),
control_points: Vec::new(),
interaction_width: default_edge_interaction_width(),
options: CanvasValue::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasEdge {
pub id: EdgeId,
#[serde(default = "default_kind")]
pub kind: String,
pub source: CanvasEndpoint,
pub target: CanvasEndpoint,
#[serde(default)]
pub z_index: i32,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub data: CanvasValue,
#[serde(default)]
pub style: CanvasStyle,
#[serde(default)]
pub route: CanvasEdgeRoute,
}
impl CanvasEdge {
pub fn new(id: impl Into<EdgeId>, source: CanvasEndpoint, target: CanvasEndpoint) -> Self {
Self {
id: id.into(),
kind: default_kind(),
source,
target,
z_index: 0,
hidden: false,
locked: false,
data: CanvasValue::new(),
style: CanvasStyle::default(),
route: CanvasEdgeRoute::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasShape {
pub id: ShapeId,
#[serde(default = "default_kind")]
pub kind: String,
pub bounds: Bounds<Pixels>,
#[serde(default)]
pub z_index: i32,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub data: CanvasValue,
#[serde(default)]
pub style: CanvasStyle,
}
impl CanvasShape {
pub fn new(id: impl Into<ShapeId>, bounds: Bounds<Pixels>) -> Self {
Self {
id: id.into(),
kind: default_kind(),
bounds,
z_index: 0,
hidden: false,
locked: false,
data: CanvasValue::new(),
style: CanvasStyle::default(),
}
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum DocumentError {
#[error("unsupported canvas document format version `{found}`, expected `{expected}`")]
UnsupportedFormatVersion { expected: u32, found: u32 },
#[error("node `{0}` already exists")]
DuplicateNode(NodeId),
#[error("edge `{0}` already exists")]
DuplicateEdge(EdgeId),
#[error("shape `{0}` already exists")]
DuplicateShape(ShapeId),
#[error("node `{0}` was not found")]
MissingNode(NodeId),
#[error("edge `{0}` was not found")]
MissingEdge(EdgeId),
#[error("shape `{0}` was not found")]
MissingShape(ShapeId),
#[error("handle `{handle_id}` was not found on node `{node_id}`")]
MissingHandle {
node_id: NodeId,
handle_id: HandleId,
},
#[error("handle `{handle_id}` already exists on node `{node_id}`")]
DuplicateHandle {
node_id: NodeId,
handle_id: HandleId,
},
#[error("handle `{handle_id}` on node `{node_id}` is not connectable")]
NonConnectableHandle {
node_id: NodeId,
handle_id: HandleId,
},
#[error("handle `{handle_id}` on node `{node_id}` cannot be used as an edge source")]
InvalidSourceHandle {
node_id: NodeId,
handle_id: HandleId,
},
#[error("handle `{handle_id}` on node `{node_id}` cannot be used as an edge target")]
InvalidTargetHandle {
node_id: NodeId,
handle_id: HandleId,
},
#[error("edge `{0}` has an empty route kind")]
EmptyEdgeRouteKind(EdgeId),
#[error("edge `{0}` has an invalid route interaction width")]
InvalidEdgeInteractionWidth(EdgeId),
#[error("edge `{0}` has an invalid route point")]
InvalidEdgeRoutePoint(EdgeId),
#[error("canvas relation references missing record `{0}`")]
MissingRelationRecord(CanvasRecordId),
#[error("canvas record `{0}` cannot be its own parent")]
SelfParentRelation(CanvasRecordId),
#[error("canvas record relation cycle includes `{0}`")]
CyclicRecordRelation(CanvasRecordId),
#[error("canvas record `{0}` has more than one parent relation")]
DuplicateParentRelation(CanvasRecordId),
#[error("canvas group relation from `{group}` to `{member}` is duplicated")]
DuplicateGroupRelation {
group: CanvasRecordId,
member: CanvasRecordId,
},
#[error("canvas binding relation `{0}` is duplicated")]
DuplicateBindingRelation(BindingId),
#[error("canvas record `{0}` cannot be bound to itself")]
SelfBindingRelation(CanvasRecordId),
#[error(transparent)]
Schema(#[from] CanvasSchemaError),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum DocumentCommand {
InsertNode(CanvasNode),
UpdateNode(CanvasNode),
RemoveNode(NodeId),
InsertEdge(CanvasEdge),
UpdateEdge(CanvasEdge),
RemoveEdge(EdgeId),
InsertShape(CanvasShape),
UpdateShape(CanvasShape),
RemoveShape(ShapeId),
SetRecordParent {
child: CanvasRecordId,
parent: CanvasRecordId,
},
ClearRecordParent {
child: CanvasRecordId,
},
AddRecordToGroup {
group: CanvasRecordId,
member: CanvasRecordId,
},
RemoveRecordFromGroup {
group: CanvasRecordId,
member: CanvasRecordId,
},
SetRecordBinding(CanvasRecordBindingRelation),
RemoveRecordBinding {
id: BindingId,
},
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CanvasDocumentDiff {
#[serde(default)]
pub inserted: IndexSet<CanvasRecordId>,
#[serde(default)]
pub updated: IndexSet<CanvasRecordId>,
#[serde(default)]
pub removed: IndexSet<CanvasRecordId>,
#[serde(default)]
pub metadata_changed: bool,
#[serde(default)]
pub relations_changed: bool,
}
impl CanvasDocumentDiff {
pub fn is_empty(&self) -> bool {
self.inserted.is_empty()
&& self.updated.is_empty()
&& self.removed.is_empty()
&& !self.metadata_changed
&& !self.relations_changed
}
pub fn record_insert(&mut self, id: impl Into<CanvasRecordId>) {
let id = id.into();
if self.removed.shift_remove(&id) {
self.updated.insert(id);
} else {
self.inserted.insert(id);
}
}
pub fn record_update(&mut self, id: impl Into<CanvasRecordId>) {
let id = id.into();
if !self.inserted.contains(&id) && !self.removed.contains(&id) {
self.updated.insert(id);
}
}
pub fn record_remove(&mut self, id: impl Into<CanvasRecordId>) {
let id = id.into();
if self.inserted.shift_remove(&id) {
self.updated.shift_remove(&id);
} else {
self.updated.shift_remove(&id);
self.removed.insert(id);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CanvasTransaction {
#[serde(default)]
pub commands: Vec<DocumentCommand>,
#[serde(default)]
pub metadata: CanvasValue,
}
impl CanvasTransaction {
pub fn new(commands: impl IntoIterator<Item = DocumentCommand>) -> Self {
Self {
commands: commands.into_iter().collect(),
metadata: CanvasValue::new(),
}
}
pub fn single(command: DocumentCommand) -> Self {
Self::new([command])
}
pub fn push(&mut self, command: DocumentCommand) {
self.commands.push(command);
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
}
impl From<DocumentCommand> for CanvasTransaction {
fn from(value: DocumentCommand) -> Self {
Self::single(value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasSnapshot {
#[serde(default = "default_document_format_version")]
pub format_version: u32,
#[serde(default)]
pub nodes: Vec<CanvasNode>,
#[serde(default)]
pub edges: Vec<CanvasEdge>,
#[serde(default)]
pub shapes: Vec<CanvasShape>,
#[serde(default)]
pub metadata: CanvasValue,
#[serde(default)]
pub relations: CanvasRecordRelations,
}
impl Default for CanvasSnapshot {
fn default() -> Self {
Self {
format_version: CANVAS_DOCUMENT_FORMAT_VERSION,
nodes: Vec::new(),
edges: Vec::new(),
shapes: Vec::new(),
metadata: CanvasValue::new(),
relations: CanvasRecordRelations::default(),
}
}
}
impl CanvasSnapshot {
pub fn migrate_to_current(self) -> Result<Self, DocumentError> {
migrate_canvas_snapshot(self)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CanvasDocument {
#[serde(default = "default_document_format_version")]
format_version: u32,
#[serde(default)]
nodes: IndexMap<NodeId, CanvasNode>,
#[serde(default)]
edges: IndexMap<EdgeId, CanvasEdge>,
#[serde(default)]
shapes: IndexMap<ShapeId, CanvasShape>,
#[serde(default)]
metadata: CanvasValue,
#[serde(default)]
relations: CanvasRecordRelations,
}
impl Default for CanvasDocument {
fn default() -> Self {
Self {
format_version: CANVAS_DOCUMENT_FORMAT_VERSION,
nodes: IndexMap::new(),
edges: IndexMap::new(),
shapes: IndexMap::new(),
metadata: CanvasValue::new(),
relations: CanvasRecordRelations::default(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CanvasDocumentBuilder {
document: CanvasDocument,
}
impl CanvasDocumentBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_format_version(mut self, format_version: u32) -> Self {
self.document.format_version = format_version;
self
}
pub fn with_metadata(mut self, metadata: CanvasValue) -> Self {
self.document.metadata = metadata;
self
}
pub fn with_relations(mut self, relations: CanvasRecordRelations) -> Self {
self.document.relations = relations;
self
}
pub fn add_node(&mut self, node: CanvasNode) -> Result<&mut Self, DocumentError> {
self.document.insert_node_rule(node)?;
Ok(self)
}
pub fn add_edge(&mut self, edge: CanvasEdge) -> Result<&mut Self, DocumentError> {
self.document.insert_edge_rule(edge)?;
Ok(self)
}
pub fn add_shape(&mut self, shape: CanvasShape) -> Result<&mut Self, DocumentError> {
self.document.insert_shape_rule(shape)?;
Ok(self)
}
pub fn build(mut self) -> Result<CanvasDocument, DocumentError> {
self.document.prune_missing_relations();
self.document.validate_relations()?;
Ok(self.document)
}
pub fn build_with_kind_registry(
mut self,
kind_registry: &CanvasKindRegistry,
) -> Result<CanvasDocument, DocumentError> {
self.document.prune_missing_relations();
self.document.validate_relations()?;
kind_registry.validate_document(&self.document)?;
Ok(self.document)
}
}
impl CanvasDocument {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> CanvasDocumentBuilder {
CanvasDocumentBuilder::new()
}
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn metadata(&self) -> &CanvasValue {
&self.metadata
}
pub fn relations(&self) -> &CanvasRecordRelations {
&self.relations
}
pub fn node(&self, id: &NodeId) -> Option<&CanvasNode> {
self.nodes.get(id)
}
pub fn edge(&self, id: &EdgeId) -> Option<&CanvasEdge> {
self.edges.get(id)
}
pub fn shape(&self, id: &ShapeId) -> Option<&CanvasShape> {
self.shapes.get(id)
}
pub fn contains_node(&self, id: &NodeId) -> bool {
self.nodes.contains_key(id)
}
pub fn contains_edge(&self, id: &EdgeId) -> bool {
self.edges.contains_key(id)
}
pub fn contains_shape(&self, id: &ShapeId) -> bool {
self.shapes.contains_key(id)
}
pub fn contains_record(&self, id: &CanvasRecordId) -> bool {
match id {
CanvasRecordId::Node(id) => self.nodes.contains_key(id),
CanvasRecordId::Edge(id) => self.edges.contains_key(id),
CanvasRecordId::Shape(id) => self.shapes.contains_key(id),
}
}
pub fn nodes(&self) -> impl Iterator<Item = &CanvasNode> + '_ {
self.nodes.values()
}
pub fn edges(&self) -> impl Iterator<Item = &CanvasEdge> + '_ {
self.edges.values()
}
pub fn shapes(&self) -> impl Iterator<Item = &CanvasShape> + '_ {
self.shapes.values()
}
pub fn node_entries(&self) -> impl Iterator<Item = (&NodeId, &CanvasNode)> + '_ {
self.nodes.iter()
}
pub fn edge_entries(&self) -> impl Iterator<Item = (&EdgeId, &CanvasEdge)> + '_ {
self.edges.iter()
}
pub fn shape_entries(&self) -> impl Iterator<Item = (&ShapeId, &CanvasShape)> + '_ {
self.shapes.iter()
}
pub fn node_ids(&self) -> impl Iterator<Item = &NodeId> + '_ {
self.nodes.keys()
}
pub fn edge_ids(&self) -> impl Iterator<Item = &EdgeId> + '_ {
self.edges.keys()
}
pub fn shape_ids(&self) -> impl Iterator<Item = &ShapeId> + '_ {
self.shapes.keys()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn shape_count(&self) -> usize {
self.shapes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty() && self.edges.is_empty() && self.shapes.is_empty()
}
pub fn from_snapshot(snapshot: CanvasSnapshot) -> Result<Self, DocumentError> {
Self::from_snapshot_with_kind_registry(snapshot, &CanvasKindRegistry::open())
}
pub fn from_snapshot_with_kind_registry(
snapshot: CanvasSnapshot,
kind_registry: &CanvasKindRegistry,
) -> Result<Self, DocumentError> {
let snapshot = migrate_canvas_snapshot(snapshot)?;
let mut builder = Self::builder()
.with_format_version(snapshot.format_version)
.with_metadata(snapshot.metadata)
.with_relations(snapshot.relations);
for node in snapshot.nodes {
let node = kind_registry.normalize_node(node)?;
builder.add_node(node)?;
}
for shape in snapshot.shapes {
let shape = kind_registry.normalize_shape(shape)?;
builder.add_shape(shape)?;
}
for edge in snapshot.edges {
let edge = kind_registry.normalize_edge(edge)?;
builder.add_edge(edge)?;
}
builder.build_with_kind_registry(kind_registry)
}
pub fn to_snapshot(&self) -> CanvasSnapshot {
CanvasSnapshot {
format_version: self.format_version,
nodes: self.nodes.values().cloned().collect(),
edges: self.edges.values().cloned().collect(),
shapes: self.shapes.values().cloned().collect(),
metadata: self.metadata.clone(),
relations: self.relations.clone(),
}
}
pub(crate) fn apply(&mut self, command: DocumentCommand) -> Result<(), DocumentError> {
match command {
DocumentCommand::InsertNode(node) => self.insert_node_rule(node),
DocumentCommand::UpdateNode(node) => self.update_node_rule(node),
DocumentCommand::RemoveNode(id) => self.remove_node_rule(&id).map(drop),
DocumentCommand::InsertEdge(edge) => self.insert_edge_rule(edge),
DocumentCommand::UpdateEdge(edge) => self.update_edge_rule(edge),
DocumentCommand::RemoveEdge(id) => self.remove_edge_rule(&id).map(drop),
DocumentCommand::InsertShape(shape) => self.insert_shape_rule(shape),
DocumentCommand::UpdateShape(shape) => self.update_shape_rule(shape),
DocumentCommand::RemoveShape(id) => self.remove_shape_rule(&id).map(drop),
DocumentCommand::SetRecordParent { child, parent } => {
self.set_record_parent_rule(child, parent)
}
DocumentCommand::ClearRecordParent { child } => {
self.relations.clear_parent(&child);
Ok(())
}
DocumentCommand::AddRecordToGroup { group, member } => {
self.add_record_to_group_rule(group, member)
}
DocumentCommand::RemoveRecordFromGroup { group, member } => {
self.relations.remove_from_group(&group, &member);
Ok(())
}
DocumentCommand::SetRecordBinding(binding) => self.set_record_binding_rule(binding),
DocumentCommand::RemoveRecordBinding { id } => {
self.relations.remove_binding(&id);
Ok(())
}
}
}
pub fn apply_transaction(
&mut self,
transaction: CanvasTransaction,
) -> Result<(), DocumentError> {
self.apply_transaction_with_diff(transaction).map(drop)
}
pub fn apply_transaction_with_diff(
&mut self,
transaction: CanvasTransaction,
) -> Result<CanvasDocumentDiff, DocumentError> {
self.commit_transaction(transaction)
.map(CanvasCommittedMutation::into_diff)
}
pub fn commit_transaction(
&mut self,
transaction: CanvasTransaction,
) -> Result<CanvasCommittedMutation, DocumentError> {
CanvasMutationJournal::commit(self, transaction)
}
pub fn commit_transaction_with_kind_registry(
&mut self,
transaction: CanvasTransaction,
kind_registry: &CanvasKindRegistry,
) -> Result<CanvasCommittedMutation, DocumentError> {
CanvasMutationJournal::commit_with_kind_registry(self, transaction, kind_registry)
}
pub(crate) fn prepare_transaction_with_kind_registry(
&self,
transaction: CanvasTransaction,
kind_registry: &CanvasKindRegistry,
) -> Result<CanvasPreparedMutation, DocumentError> {
CanvasMutationJournal::prepare_with_kind_registry(self, transaction, kind_registry)
}
pub fn invert_transaction(
&self,
transaction: &CanvasTransaction,
) -> Result<CanvasTransaction, DocumentError> {
let mut draft = self.clone();
let mut inverse_segments = Vec::new();
for command in &transaction.commands {
inverse_segments.push(draft.inverse_for(command)?);
draft.apply(command.clone())?;
}
Ok(CanvasTransaction {
commands: inverse_segments.into_iter().rev().flatten().collect(),
metadata: CanvasValue::new(),
})
}
fn insert_node_rule(&mut self, node: CanvasNode) -> Result<(), DocumentError> {
if self.nodes.contains_key(&node.id) {
return Err(DocumentError::DuplicateNode(node.id));
}
Self::validate_node(&node)?;
self.nodes.insert(node.id.clone(), node);
Ok(())
}
fn update_node_rule(&mut self, node: CanvasNode) -> Result<(), DocumentError> {
if !self.nodes.contains_key(&node.id) {
return Err(DocumentError::MissingNode(node.id));
}
Self::validate_node(&node)?;
let mut draft = self.clone();
draft.nodes.insert(node.id.clone(), node);
draft.validate_integrity()?;
*self = draft;
Ok(())
}
fn remove_node_rule(&mut self, id: &NodeId) -> Result<CanvasNode, DocumentError> {
let Some(node) = self.nodes.shift_remove(id) else {
return Err(DocumentError::MissingNode(id.clone()));
};
self.edges
.retain(|_, edge| edge.source.node_id != *id && edge.target.node_id != *id);
Ok(node)
}
fn insert_edge_rule(&mut self, edge: CanvasEdge) -> Result<(), DocumentError> {
if self.edges.contains_key(&edge.id) {
return Err(DocumentError::DuplicateEdge(edge.id));
}
self.validate_edge(&edge)?;
self.edges.insert(edge.id.clone(), edge);
Ok(())
}
fn update_edge_rule(&mut self, edge: CanvasEdge) -> Result<(), DocumentError> {
if !self.edges.contains_key(&edge.id) {
return Err(DocumentError::MissingEdge(edge.id));
}
self.validate_edge(&edge)?;
self.edges.insert(edge.id.clone(), edge);
Ok(())
}
fn remove_edge_rule(&mut self, id: &EdgeId) -> Result<CanvasEdge, DocumentError> {
self.edges
.shift_remove(id)
.ok_or_else(|| DocumentError::MissingEdge(id.clone()))
}
fn insert_shape_rule(&mut self, shape: CanvasShape) -> Result<(), DocumentError> {
if self.shapes.contains_key(&shape.id) {
return Err(DocumentError::DuplicateShape(shape.id));
}
self.shapes.insert(shape.id.clone(), shape);
Ok(())
}
fn update_shape_rule(&mut self, shape: CanvasShape) -> Result<(), DocumentError> {
if !self.shapes.contains_key(&shape.id) {
return Err(DocumentError::MissingShape(shape.id));
}
self.shapes.insert(shape.id.clone(), shape);
Ok(())
}
fn remove_shape_rule(&mut self, id: &ShapeId) -> Result<CanvasShape, DocumentError> {
self.shapes
.shift_remove(id)
.ok_or_else(|| DocumentError::MissingShape(id.clone()))
}
fn set_record_parent_rule(
&mut self,
child: CanvasRecordId,
parent: CanvasRecordId,
) -> Result<(), DocumentError> {
if child == parent {
return Err(DocumentError::SelfParentRelation(child));
}
self.validate_record_id(&child)?;
self.validate_record_id(&parent)?;
self.relations.set_parent(child, parent);
Ok(())
}
fn add_record_to_group_rule(
&mut self,
group: CanvasRecordId,
member: CanvasRecordId,
) -> Result<(), DocumentError> {
if group == member {
return Err(DocumentError::SelfParentRelation(group));
}
self.validate_record_id(&group)?;
self.validate_record_id(&member)?;
self.relations.add_to_group(group, member);
Ok(())
}
fn set_record_binding_rule(
&mut self,
binding: CanvasRecordBindingRelation,
) -> Result<(), DocumentError> {
if binding.source == binding.target {
return Err(DocumentError::SelfBindingRelation(binding.source));
}
self.validate_record_id(&binding.source)?;
self.validate_record_id(&binding.target)?;
self.relations.set_binding(binding);
Ok(())
}
#[cfg(test)]
pub(crate) fn insert_node(&mut self, node: CanvasNode) -> Result<(), DocumentError> {
self.insert_node_rule(node)
}
#[cfg(test)]
pub(crate) fn remove_node(&mut self, id: &NodeId) -> Result<CanvasNode, DocumentError> {
self.remove_node_rule(id)
}
#[cfg(test)]
pub(crate) fn insert_edge(&mut self, edge: CanvasEdge) -> Result<(), DocumentError> {
self.insert_edge_rule(edge)
}
#[cfg(test)]
pub(crate) fn insert_shape(&mut self, shape: CanvasShape) -> Result<(), DocumentError> {
self.insert_shape_rule(shape)
}
pub fn validate_endpoint(&self, endpoint: &CanvasEndpoint) -> Result<(), DocumentError> {
self.endpoint_parts(endpoint)?;
Ok(())
}
pub fn validate_edge(&self, edge: &CanvasEdge) -> Result<(), DocumentError> {
Self::validate_edge_route(edge)?;
self.validate_source_endpoint(&edge.source)?;
self.validate_target_endpoint(&edge.target)?;
Ok(())
}
pub fn validate_integrity(&self) -> Result<(), DocumentError> {
for node in self.nodes.values() {
Self::validate_node(node)?;
}
for edge in self.edges.values() {
self.validate_edge(edge)?;
}
self.validate_relations()?;
Ok(())
}
pub fn endpoint_position(
&self,
endpoint: &CanvasEndpoint,
) -> Result<Point<Pixels>, DocumentError> {
CanvasGeometryFacts::new(self).endpoint_position(endpoint)
}
pub fn edge_route_path(&self, edge: &CanvasEdge) -> Result<CanvasRoutePath, DocumentError> {
self.edge_route_path_with_router(edge, &CanvasDefaultEdgeRouter)
}
pub fn edge_route_path_with_router<R>(
&self,
edge: &CanvasEdge,
router: &R,
) -> Result<CanvasRoutePath, DocumentError>
where
R: CanvasEdgeRouter + ?Sized,
{
CanvasGeometryFacts::with_router(self, router).edge_route_path(edge)
}
pub fn edge_bounds(&self, edge: &CanvasEdge) -> Result<Bounds<Pixels>, DocumentError> {
self.edge_bounds_with_router(edge, &CanvasDefaultEdgeRouter)
}
pub fn edge_bounds_with_router<R>(
&self,
edge: &CanvasEdge,
router: &R,
) -> Result<Bounds<Pixels>, DocumentError>
where
R: CanvasEdgeRouter + ?Sized,
{
CanvasGeometryFacts::with_router(self, router).edge_bounds(edge)
}
pub fn edge_route_points(
&self,
edge: &CanvasEdge,
) -> Result<Vec<Point<Pixels>>, DocumentError> {
Ok(self.edge_route_path(edge)?.document_points())
}
pub fn diff_against(&self, previous: &CanvasDocument) -> CanvasDocumentDiff {
let mut diff = CanvasDocumentDiff::default();
for id in previous.nodes.keys() {
if !self.nodes.contains_key(id) {
diff.record_remove(id.clone());
}
}
for (id, node) in &self.nodes {
match previous.nodes.get(id) {
None => diff.record_insert(id.clone()),
Some(previous_node) if previous_node != node => diff.record_update(id.clone()),
Some(_) => {}
}
}
for id in previous.edges.keys() {
if !self.edges.contains_key(id) {
diff.record_remove(id.clone());
}
}
for (id, edge) in &self.edges {
match previous.edges.get(id) {
None => diff.record_insert(id.clone()),
Some(previous_edge) if previous_edge != edge => diff.record_update(id.clone()),
Some(_) => {}
}
}
for id in previous.shapes.keys() {
if !self.shapes.contains_key(id) {
diff.record_remove(id.clone());
}
}
for (id, shape) in &self.shapes {
match previous.shapes.get(id) {
None => diff.record_insert(id.clone()),
Some(previous_shape) if previous_shape != shape => diff.record_update(id.clone()),
Some(_) => {}
}
}
diff.metadata_changed = self.metadata != previous.metadata;
diff.relations_changed = self.relations != previous.relations;
diff
}
pub(crate) fn prune_missing_relations(&mut self) -> bool {
let existing = self.record_id_set();
self.relations.prune_missing_records(&existing)
}
pub fn validate_relations(&self) -> Result<(), DocumentError> {
let mut parent_children = IndexSet::new();
for relation in self.relations.parents() {
if !parent_children.insert(relation.child.clone()) {
return Err(DocumentError::DuplicateParentRelation(
relation.child.clone(),
));
}
if relation.child == relation.parent {
return Err(DocumentError::SelfParentRelation(relation.child.clone()));
}
self.validate_record_id(&relation.child)?;
self.validate_record_id(&relation.parent)?;
}
let mut group_relations = IndexSet::new();
for relation in self.relations.groups() {
if !group_relations.insert((relation.group.clone(), relation.member.clone())) {
return Err(DocumentError::DuplicateGroupRelation {
group: relation.group.clone(),
member: relation.member.clone(),
});
}
if relation.group == relation.member {
return Err(DocumentError::SelfParentRelation(relation.group.clone()));
}
self.validate_record_id(&relation.group)?;
self.validate_record_id(&relation.member)?;
}
let mut binding_ids = IndexSet::new();
for relation in self.relations.bindings() {
if !binding_ids.insert(relation.id.clone()) {
return Err(DocumentError::DuplicateBindingRelation(relation.id.clone()));
}
if relation.source == relation.target {
return Err(DocumentError::SelfBindingRelation(relation.source.clone()));
}
self.validate_record_id(&relation.source)?;
self.validate_record_id(&relation.target)?;
}
self.validate_relation_graph_is_acyclic()?;
Ok(())
}
fn validate_relation_graph_is_acyclic(&self) -> Result<(), DocumentError> {
let mut graph = IndexMap::<CanvasRecordId, Vec<CanvasRecordId>>::new();
for relation in self.relations.parents() {
graph
.entry(relation.parent.clone())
.or_default()
.push(relation.child.clone());
}
for relation in self.relations.groups() {
graph
.entry(relation.group.clone())
.or_default()
.push(relation.member.clone());
}
let mut visited = IndexSet::new();
let mut visiting = IndexSet::new();
for record_id in graph.keys() {
self.validate_relation_subgraph_is_acyclic(
record_id,
&graph,
&mut visited,
&mut visiting,
)?;
}
Ok(())
}
fn validate_relation_subgraph_is_acyclic(
&self,
record_id: &CanvasRecordId,
graph: &IndexMap<CanvasRecordId, Vec<CanvasRecordId>>,
visited: &mut IndexSet<CanvasRecordId>,
visiting: &mut IndexSet<CanvasRecordId>,
) -> Result<(), DocumentError> {
if visited.contains(record_id) {
return Ok(());
}
if !visiting.insert(record_id.clone()) {
return Err(DocumentError::CyclicRecordRelation(record_id.clone()));
}
if let Some(children) = graph.get(record_id) {
for child in children {
self.validate_relation_subgraph_is_acyclic(child, graph, visited, visiting)?;
}
}
visiting.shift_remove(record_id);
visited.insert(record_id.clone());
Ok(())
}
fn validate_record_id(&self, id: &CanvasRecordId) -> Result<(), DocumentError> {
if self.contains_record(id) {
Ok(())
} else {
Err(DocumentError::MissingRelationRecord(id.clone()))
}
}
fn record_id_set(&self) -> IndexSet<CanvasRecordId> {
self.nodes
.keys()
.cloned()
.map(CanvasRecordId::Node)
.chain(self.edges.keys().cloned().map(CanvasRecordId::Edge))
.chain(self.shapes.keys().cloned().map(CanvasRecordId::Shape))
.collect()
}
fn validate_node(node: &CanvasNode) -> Result<(), DocumentError> {
let mut handle_ids = IndexSet::new();
for handle in &node.handles {
if !handle_ids.insert(handle.id.clone()) {
return Err(DocumentError::DuplicateHandle {
node_id: node.id.clone(),
handle_id: handle.id.clone(),
});
}
}
Ok(())
}
fn validate_edge_route(edge: &CanvasEdge) -> Result<(), DocumentError> {
if edge.route.kind.as_str().trim().is_empty() {
return Err(DocumentError::EmptyEdgeRouteKind(edge.id.clone()));
}
if !edge.route.interaction_width.as_f32().is_finite()
|| edge.route.interaction_width < Pixels::ZERO
{
return Err(DocumentError::InvalidEdgeInteractionWidth(edge.id.clone()));
}
for point in edge
.route
.waypoints
.iter()
.chain(edge.route.control_points.iter())
{
if !point.x.as_f32().is_finite() || !point.y.as_f32().is_finite() {
return Err(DocumentError::InvalidEdgeRoutePoint(edge.id.clone()));
}
}
Ok(())
}
fn validate_source_endpoint(&self, endpoint: &CanvasEndpoint) -> Result<(), DocumentError> {
let Some(handle) = self.endpoint_parts(endpoint)?.1 else {
return Ok(());
};
self.validate_connectable_handle(endpoint, handle)?;
if handle.role == HandleRole::Target {
return Err(DocumentError::InvalidSourceHandle {
node_id: endpoint.node_id.clone(),
handle_id: handle.id.clone(),
});
}
Ok(())
}
fn validate_target_endpoint(&self, endpoint: &CanvasEndpoint) -> Result<(), DocumentError> {
let Some(handle) = self.endpoint_parts(endpoint)?.1 else {
return Ok(());
};
self.validate_connectable_handle(endpoint, handle)?;
if handle.role == HandleRole::Source {
return Err(DocumentError::InvalidTargetHandle {
node_id: endpoint.node_id.clone(),
handle_id: handle.id.clone(),
});
}
Ok(())
}
fn validate_connectable_handle(
&self,
endpoint: &CanvasEndpoint,
handle: &CanvasHandle,
) -> Result<(), DocumentError> {
if !handle.connectable {
return Err(DocumentError::NonConnectableHandle {
node_id: endpoint.node_id.clone(),
handle_id: handle.id.clone(),
});
}
Ok(())
}
fn endpoint_parts(
&self,
endpoint: &CanvasEndpoint,
) -> Result<(&CanvasNode, Option<&CanvasHandle>), DocumentError> {
let node = self
.nodes
.get(&endpoint.node_id)
.ok_or_else(|| DocumentError::MissingNode(endpoint.node_id.clone()))?;
let Some(handle_id) = &endpoint.handle_id else {
return Ok((node, None));
};
let handle = node
.handle(Some(handle_id))
.ok_or_else(|| DocumentError::MissingHandle {
node_id: endpoint.node_id.clone(),
handle_id: handle_id.clone(),
})?;
Ok((node, Some(handle)))
}
fn inverse_for(
&self,
command: &DocumentCommand,
) -> Result<Vec<DocumentCommand>, DocumentError> {
match command {
DocumentCommand::InsertNode(node) => {
if self.nodes.contains_key(&node.id) {
return Err(DocumentError::DuplicateNode(node.id.clone()));
}
Self::validate_node(node)?;
Ok(vec![DocumentCommand::RemoveNode(node.id.clone())])
}
DocumentCommand::UpdateNode(node) => Ok(vec![DocumentCommand::UpdateNode(
self.nodes
.get(&node.id)
.ok_or_else(|| DocumentError::MissingNode(node.id.clone()))?
.clone(),
)]),
DocumentCommand::RemoveNode(id) => {
let node = self
.nodes
.get(id)
.ok_or_else(|| DocumentError::MissingNode(id.clone()))?
.clone();
let mut inverse = vec![DocumentCommand::InsertNode(node)];
inverse.extend(
self.edges
.values()
.filter(|edge| edge.source.node_id == *id || edge.target.node_id == *id)
.cloned()
.map(DocumentCommand::InsertEdge),
);
Ok(inverse)
}
DocumentCommand::InsertEdge(edge) => {
if self.edges.contains_key(&edge.id) {
return Err(DocumentError::DuplicateEdge(edge.id.clone()));
}
self.validate_edge(edge)?;
Ok(vec![DocumentCommand::RemoveEdge(edge.id.clone())])
}
DocumentCommand::UpdateEdge(edge) => Ok(vec![DocumentCommand::UpdateEdge(
self.edges
.get(&edge.id)
.ok_or_else(|| DocumentError::MissingEdge(edge.id.clone()))?
.clone(),
)]),
DocumentCommand::RemoveEdge(id) => Ok(vec![DocumentCommand::InsertEdge(
self.edges
.get(id)
.ok_or_else(|| DocumentError::MissingEdge(id.clone()))?
.clone(),
)]),
DocumentCommand::InsertShape(shape) => {
if self.shapes.contains_key(&shape.id) {
return Err(DocumentError::DuplicateShape(shape.id.clone()));
}
Ok(vec![DocumentCommand::RemoveShape(shape.id.clone())])
}
DocumentCommand::UpdateShape(shape) => Ok(vec![DocumentCommand::UpdateShape(
self.shapes
.get(&shape.id)
.ok_or_else(|| DocumentError::MissingShape(shape.id.clone()))?
.clone(),
)]),
DocumentCommand::RemoveShape(id) => Ok(vec![DocumentCommand::InsertShape(
self.shapes
.get(id)
.ok_or_else(|| DocumentError::MissingShape(id.clone()))?
.clone(),
)]),
DocumentCommand::SetRecordParent { child, parent } => {
if child == parent {
return Err(DocumentError::SelfParentRelation(child.clone()));
}
self.validate_record_id(child)?;
self.validate_record_id(parent)?;
Ok(match self.relations.parent_of(child).cloned() {
Some(previous) => vec![DocumentCommand::SetRecordParent {
child: child.clone(),
parent: previous,
}],
None => vec![DocumentCommand::ClearRecordParent {
child: child.clone(),
}],
})
}
DocumentCommand::ClearRecordParent { child } => {
self.validate_record_id(child)?;
Ok(match self.relations.parent_of(child).cloned() {
Some(parent) => vec![DocumentCommand::SetRecordParent {
child: child.clone(),
parent,
}],
None => Vec::new(),
})
}
DocumentCommand::AddRecordToGroup { group, member } => {
if group == member {
return Err(DocumentError::SelfParentRelation(group.clone()));
}
self.validate_record_id(group)?;
self.validate_record_id(member)?;
let already_member = self.relations.groups_for(member).any(|id| id == group);
Ok(if already_member {
Vec::new()
} else {
vec![DocumentCommand::RemoveRecordFromGroup {
group: group.clone(),
member: member.clone(),
}]
})
}
DocumentCommand::RemoveRecordFromGroup { group, member } => {
self.validate_record_id(group)?;
self.validate_record_id(member)?;
let already_member = self.relations.groups_for(member).any(|id| id == group);
Ok(if already_member {
vec![DocumentCommand::AddRecordToGroup {
group: group.clone(),
member: member.clone(),
}]
} else {
Vec::new()
})
}
DocumentCommand::SetRecordBinding(binding) => {
if binding.source == binding.target {
return Err(DocumentError::SelfBindingRelation(binding.source.clone()));
}
self.validate_record_id(&binding.source)?;
self.validate_record_id(&binding.target)?;
Ok(match self.relations.binding(&binding.id).cloned() {
Some(previous) => vec![DocumentCommand::SetRecordBinding(previous)],
None => vec![DocumentCommand::RemoveRecordBinding {
id: binding.id.clone(),
}],
})
}
DocumentCommand::RemoveRecordBinding { id } => Ok(match self.relations.binding(id) {
Some(binding) => vec![DocumentCommand::SetRecordBinding(binding.clone())],
None => Vec::new(),
}),
}
}
}
impl TryFrom<CanvasSnapshot> for CanvasDocument {
type Error = DocumentError;
fn try_from(value: CanvasSnapshot) -> Result<Self, Self::Error> {
Self::from_snapshot(value)
}
}
impl From<&CanvasDocument> for CanvasSnapshot {
fn from(value: &CanvasDocument) -> Self {
value.to_snapshot()
}
}
fn default_true() -> bool {
true
}
fn default_kind() -> String {
"default".to_string()
}
fn default_handle_size() -> Size<Pixels> {
Size::new(Pixels::from(12.0), Pixels::from(12.0))
}
fn default_edge_interaction_width() -> Pixels {
Pixels::from(12.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{
CanvasCommandGenerator, TestRng, connected_pair_fixture, document_fixture,
};
use crate::{
CANVAS_DOCUMENT_MIN_SUPPORTED_FORMAT_VERSION, CANVAS_SNAPSHOT_MIGRATIONS,
CanvasRecordParentRelation,
};
use open_gpui::{point, px, size};
#[test]
fn defaults_to_current_format_version() {
let document = CanvasDocument::default();
assert_eq!(document.format_version, CANVAS_DOCUMENT_FORMAT_VERSION);
}
#[test]
fn deserializes_missing_format_version_to_current_version() {
let document: CanvasDocument = serde_json::from_str(
r#"{
"nodes": {},
"edges": {},
"shapes": {},
"metadata": {}
}"#,
)
.unwrap();
assert_eq!(document.format_version, CANVAS_DOCUMENT_FORMAT_VERSION);
}
#[test]
fn snapshot_round_trips_array_records() {
let document = connected_pair_fixture().build();
let snapshot = document.to_snapshot();
assert_eq!(snapshot.nodes.len(), 2);
assert_eq!(snapshot.edges.len(), 1);
let restored = CanvasDocument::from_snapshot(snapshot).unwrap();
assert_eq!(restored.nodes.len(), 2);
assert_eq!(restored.edges.len(), 1);
}
#[test]
fn snapshot_round_trips_record_relations() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
let child = CanvasRecordId::Node(NodeId::from("child"));
let group = CanvasRecordId::Shape(ShapeId::from("group"));
let binding = CanvasRecordBindingRelation::new("binding", child.clone(), group.clone());
document
.apply_transaction(CanvasTransaction::new([
DocumentCommand::SetRecordParent {
child: child.clone(),
parent: group.clone(),
},
DocumentCommand::AddRecordToGroup {
group: group.clone(),
member: child.clone(),
},
DocumentCommand::SetRecordBinding(binding.clone()),
]))
.unwrap();
let restored = CanvasDocument::from_snapshot(document.to_snapshot()).unwrap();
assert_eq!(restored.relations().parent_of(&child), Some(&group));
assert_eq!(
restored
.relations()
.members_of(&group)
.cloned()
.collect::<Vec<_>>(),
vec![child.clone()]
);
assert_eq!(restored.relations().binding(&binding.id), Some(&binding));
}
#[test]
fn builder_rejects_duplicate_records_during_construction() {
let mut builder = CanvasDocument::builder();
builder
.add_node(CanvasNode::new(
"duplicate",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.unwrap();
let error = builder
.add_node(CanvasNode::new(
"duplicate",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.unwrap_err();
assert_eq!(
error,
DocumentError::DuplicateNode(NodeId::from("duplicate"))
);
}
#[test]
fn builder_rejects_invalid_edge_endpoints_during_construction() {
let mut builder = CanvasDocument::builder();
builder
.add_node(CanvasNode::new(
"source",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.unwrap();
let error = builder
.add_edge(CanvasEdge::new(
"edge",
CanvasEndpoint::new("source", None::<&str>),
CanvasEndpoint::new("missing", None::<&str>),
))
.unwrap_err();
assert_eq!(error, DocumentError::MissingNode(NodeId::from("missing")));
}
#[test]
fn builder_prunes_dangling_relations_at_build_time() {
let child = CanvasRecordId::Node(NodeId::from("child"));
let existing_group = CanvasRecordId::Shape(ShapeId::from("group"));
let missing_group = CanvasRecordId::Shape(ShapeId::from("missing"));
let mut relations = CanvasRecordRelations::default();
relations.set_parent(child.clone(), missing_group.clone());
relations.add_to_group(existing_group.clone(), child.clone());
relations.add_to_group(missing_group.clone(), child.clone());
relations.set_binding(CanvasRecordBindingRelation::new(
"binding",
child.clone(),
missing_group,
));
let mut builder = CanvasDocument::builder().with_relations(relations);
builder
.add_node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.unwrap();
builder
.add_shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.unwrap();
let document = builder.build().unwrap();
assert_eq!(document.relations().parent_of(&child), None);
assert_eq!(
document
.relations()
.members_of(&existing_group)
.cloned()
.collect::<Vec<_>>(),
vec![child]
);
assert!(document.relations().bindings().next().is_none());
}
#[test]
fn builder_rejects_duplicate_parent_relations_at_build_time() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
for id in ["frame-a", "frame-b"] {
document
.insert_shape(CanvasShape::new(
id,
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.unwrap();
}
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("frame-a")),
},
))
.unwrap();
let mut value = serde_json::to_value(document.to_snapshot()).unwrap();
let parents = value["relations"]["parents"].as_array_mut().unwrap();
let mut duplicate = parents[0].clone();
duplicate["parent"] =
serde_json::to_value(CanvasRecordId::Shape(ShapeId::from("frame-b"))).unwrap();
parents.push(duplicate);
let snapshot: CanvasSnapshot = serde_json::from_value(value).unwrap();
let mut builder = CanvasDocument::builder().with_relations(snapshot.relations);
for node in snapshot.nodes {
builder.add_node(node).unwrap();
}
for shape in snapshot.shapes {
builder.add_shape(shape).unwrap();
}
assert_eq!(
builder.build().unwrap_err(),
DocumentError::DuplicateParentRelation(CanvasRecordId::Node(NodeId::from("child")))
);
}
#[test]
fn from_snapshot_rejects_duplicate_parent_relations_for_one_child() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
for id in ["frame-a", "frame-b"] {
document
.insert_shape(CanvasShape::new(
id,
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.unwrap();
}
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("frame-a")),
},
))
.unwrap();
let mut value = serde_json::to_value(document.to_snapshot()).unwrap();
let parents = value["relations"]["parents"].as_array_mut().unwrap();
let mut duplicate = parents[0].clone();
duplicate["parent"] =
serde_json::to_value(CanvasRecordId::Shape(ShapeId::from("frame-b"))).unwrap();
parents.push(duplicate);
let snapshot = serde_json::from_value(value).unwrap();
assert_eq!(
CanvasDocument::from_snapshot(snapshot).unwrap_err(),
DocumentError::DuplicateParentRelation(CanvasRecordId::Node(NodeId::from("child")))
);
}
#[test]
fn from_snapshot_rejects_duplicate_group_relations() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
let group = CanvasRecordId::Shape(ShapeId::from("group"));
let member = CanvasRecordId::Node(NodeId::from("child"));
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::AddRecordToGroup {
group: group.clone(),
member: member.clone(),
},
))
.unwrap();
let mut value = serde_json::to_value(document.to_snapshot()).unwrap();
let groups = value["relations"]["groups"].as_array_mut().unwrap();
groups.push(groups[0].clone());
let snapshot = serde_json::from_value(value).unwrap();
assert_eq!(
CanvasDocument::from_snapshot(snapshot).unwrap_err(),
DocumentError::DuplicateGroupRelation { group, member }
);
}
#[test]
fn relation_commands_reject_parent_cycles() {
let mut document = document_fixture()
.shape(CanvasShape::new(
"frame-a",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.shape(CanvasShape::new(
"frame-b",
Bounds::new(point(px(20.0), px(20.0)), size(px(40.0), px(40.0))),
))
.build();
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Shape(ShapeId::from("frame-b")),
parent: CanvasRecordId::Shape(ShapeId::from("frame-a")),
},
))
.unwrap();
let err = document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Shape(ShapeId::from("frame-a")),
parent: CanvasRecordId::Shape(ShapeId::from("frame-b")),
},
))
.unwrap_err();
assert_eq!(
err,
DocumentError::CyclicRecordRelation(CanvasRecordId::Shape(ShapeId::from("frame-a")))
);
}
#[test]
fn relation_commands_reject_group_cycles() {
let mut document = document_fixture()
.shape(CanvasShape::new(
"group-a",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.shape(CanvasShape::new(
"group-b",
Bounds::new(point(px(20.0), px(20.0)), size(px(40.0), px(40.0))),
))
.build();
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::AddRecordToGroup {
group: CanvasRecordId::Shape(ShapeId::from("group-a")),
member: CanvasRecordId::Shape(ShapeId::from("group-b")),
},
))
.unwrap();
let err = document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::AddRecordToGroup {
group: CanvasRecordId::Shape(ShapeId::from("group-b")),
member: CanvasRecordId::Shape(ShapeId::from("group-a")),
},
))
.unwrap_err();
assert_eq!(
err,
DocumentError::CyclicRecordRelation(CanvasRecordId::Shape(ShapeId::from("group-a")))
);
}
#[test]
fn from_snapshot_rejects_relation_cycles() {
let mut document = document_fixture()
.shape(CanvasShape::new(
"frame-a",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.shape(CanvasShape::new(
"frame-b",
Bounds::new(point(px(20.0), px(20.0)), size(px(40.0), px(40.0))),
))
.build();
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Shape(ShapeId::from("frame-b")),
parent: CanvasRecordId::Shape(ShapeId::from("frame-a")),
},
))
.unwrap();
let mut value = serde_json::to_value(document.to_snapshot()).unwrap();
let parents = value["relations"]["parents"].as_array_mut().unwrap();
parents.push(
serde_json::to_value(CanvasRecordParentRelation::new(
CanvasRecordId::Shape(ShapeId::from("frame-a")),
CanvasRecordId::Shape(ShapeId::from("frame-b")),
))
.unwrap(),
);
let snapshot = serde_json::from_value(value).unwrap();
assert_eq!(
CanvasDocument::from_snapshot(snapshot).unwrap_err(),
DocumentError::CyclicRecordRelation(CanvasRecordId::Shape(ShapeId::from("frame-a")))
);
}
#[test]
fn from_snapshot_rejects_duplicate_binding_relations() {
let mut document = document_fixture()
.node(CanvasNode::new(
"source",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"target",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
let binding = CanvasRecordBindingRelation::new(
"binding",
CanvasRecordId::Node(NodeId::from("source")),
CanvasRecordId::Shape(ShapeId::from("target")),
);
document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordBinding(binding),
))
.unwrap();
let mut value = serde_json::to_value(document.to_snapshot()).unwrap();
let bindings = value["relations"]["bindings"].as_array_mut().unwrap();
bindings.push(bindings[0].clone());
let snapshot = serde_json::from_value(value).unwrap();
assert_eq!(
CanvasDocument::from_snapshot(snapshot).unwrap_err(),
DocumentError::DuplicateBindingRelation(BindingId::from("binding"))
);
}
#[test]
fn relation_commands_reject_self_binding() {
let mut document = document_fixture()
.node(CanvasNode::new(
"source",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let source = CanvasRecordId::Node(NodeId::from("source"));
let err = document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
"binding",
source.clone(),
source.clone(),
)),
))
.unwrap_err();
assert_eq!(err, DocumentError::SelfBindingRelation(source));
}
#[test]
fn current_snapshot_migration_is_noop() {
let mut snapshot = CanvasSnapshot::default();
snapshot.nodes.push(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
));
snapshot.metadata.insert("title".into(), "Canvas".into());
let migrated = snapshot.clone().migrate_to_current().unwrap();
assert_eq!(migrated, snapshot);
assert_eq!(migrated.format_version, CANVAS_DOCUMENT_FORMAT_VERSION);
}
#[test]
fn from_snapshot_accepts_current_snapshot_through_migration_boundary() {
let snapshot = CanvasSnapshot {
nodes: vec![CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
)],
..CanvasSnapshot::default()
};
let document = CanvasDocument::from_snapshot(snapshot).unwrap();
assert_eq!(document.format_version, CANVAS_DOCUMENT_FORMAT_VERSION);
assert_eq!(document.nodes.len(), 1);
}
#[test]
fn rejects_future_snapshot_version() {
let snapshot = CanvasSnapshot {
format_version: CANVAS_DOCUMENT_FORMAT_VERSION + 1,
..CanvasSnapshot::default()
};
assert_eq!(
CanvasDocument::from_snapshot(snapshot).unwrap_err(),
DocumentError::UnsupportedFormatVersion {
expected: CANVAS_DOCUMENT_FORMAT_VERSION,
found: CANVAS_DOCUMENT_FORMAT_VERSION + 1,
}
);
}
#[test]
fn rejects_snapshot_version_below_minimum_supported_version() {
let snapshot = CanvasSnapshot {
format_version: CANVAS_DOCUMENT_MIN_SUPPORTED_FORMAT_VERSION - 1,
..CanvasSnapshot::default()
};
assert_eq!(
migrate_canvas_snapshot(snapshot).unwrap_err(),
DocumentError::UnsupportedFormatVersion {
expected: CANVAS_DOCUMENT_FORMAT_VERSION,
found: CANVAS_DOCUMENT_MIN_SUPPORTED_FORMAT_VERSION - 1,
}
);
}
#[test]
fn snapshot_migration_table_is_monotonic() {
for migration in CANVAS_SNAPSHOT_MIGRATIONS {
assert!(migration.from_version < migration.to_version);
}
for migrations in CANVAS_SNAPSHOT_MIGRATIONS.windows(2) {
assert_eq!(migrations[0].to_version, migrations[1].from_version);
}
}
#[test]
fn removes_edges_when_node_is_removed() {
let mut document = connected_pair_fixture().build();
document.remove_node(&NodeId::from("a")).unwrap();
assert!(document.edges.is_empty());
}
#[test]
fn validates_edge_handles() {
let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
node.handles
.push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
let mut document = document_fixture()
.node(node)
.node(CanvasNode::new(
"b",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let err = document
.insert_edge(CanvasEdge::new(
"bad",
CanvasEndpoint::new("a", Some("missing")),
CanvasEndpoint::new("b", None::<&str>),
))
.unwrap_err();
assert_eq!(
err,
DocumentError::MissingHandle {
node_id: NodeId::from("a"),
handle_id: HandleId::from("missing")
}
);
}
#[test]
fn handle_connection_role_helpers_respect_roles_and_pickability() {
let mut source_only = CanvasHandle::new("out", point(px(10.0), px(5.0)));
source_only.role = HandleRole::Source;
assert!(source_only.accepts_connection_role(CanvasConnectionEndpointRole::Source));
assert!(!source_only.accepts_connection_role(CanvasConnectionEndpointRole::Target));
assert!(source_only.is_pickable_connection_endpoint(CanvasConnectionEndpointRole::Source));
source_only.hidden = true;
assert!(source_only.accepts_connection_role(CanvasConnectionEndpointRole::Source));
assert!(!source_only.is_pickable_connection_endpoint(CanvasConnectionEndpointRole::Source));
let mut target_only = CanvasHandle::new("in", point(px(0.0), px(5.0)));
target_only.role = HandleRole::Target;
target_only.connectable = false;
assert!(!target_only.accepts_connection_role(CanvasConnectionEndpointRole::Target));
assert!(!target_only.is_pickable_connection_endpoint(CanvasConnectionEndpointRole::Target));
}
#[test]
fn rejects_duplicate_handles_on_node_insert() {
let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
node.handles
.push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
node.handles
.push(CanvasHandle::new("out", point(px(0.0), px(5.0))));
let mut document = document_fixture().build();
let err = document.insert_node(node).unwrap_err();
assert_eq!(
err,
DocumentError::DuplicateHandle {
node_id: NodeId::from("a"),
handle_id: HandleId::from("out")
}
);
}
#[test]
fn validates_handle_roles_for_edges() {
let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
let mut target_only = CanvasHandle::new("in", point(px(10.0), px(5.0)));
target_only.role = HandleRole::Target;
source.handles.push(target_only);
let mut target = CanvasNode::new("b", point(px(20.0), px(0.0)), size(px(10.0), px(10.0)));
let mut source_only = CanvasHandle::new("out", point(px(0.0), px(5.0)));
source_only.role = HandleRole::Source;
target.handles.push(source_only);
let mut document = document_fixture().node(source).node(target).build();
let err = document
.insert_edge(CanvasEdge::new(
"bad",
CanvasEndpoint::new("a", Some("in")),
CanvasEndpoint::new("b", Some("out")),
))
.unwrap_err();
assert_eq!(
err,
DocumentError::InvalidSourceHandle {
node_id: NodeId::from("a"),
handle_id: HandleId::from("in")
}
);
}
#[test]
fn rejects_non_connectable_edge_handles() {
let mut source = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
let mut handle = CanvasHandle::new("out", point(px(10.0), px(5.0)));
handle.connectable = false;
source.handles.push(handle);
let mut document = document_fixture()
.node(source)
.node(CanvasNode::new(
"b",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let err = document
.insert_edge(CanvasEdge::new(
"bad",
CanvasEndpoint::new("a", Some("out")),
CanvasEndpoint::new("b", None::<&str>),
))
.unwrap_err();
assert_eq!(
err,
DocumentError::NonConnectableHandle {
node_id: NodeId::from("a"),
handle_id: HandleId::from("out")
}
);
}
#[test]
fn edge_route_defaults_keep_legacy_edges_readable() {
let edge: CanvasEdge = serde_json::from_str(
r#"{
"id": "a-b",
"source": { "node_id": "a" },
"target": { "node_id": "b" }
}"#,
)
.unwrap();
assert_eq!(
edge.route.kind,
CanvasEdgeRouteKind::from(CanvasEdgeRouteKind::STRAIGHT)
);
assert!(edge.route.waypoints.is_empty());
assert!(edge.route.control_points.is_empty());
assert_eq!(edge.route.interaction_width, px(12.0));
}
#[test]
fn edge_route_points_include_waypoints_between_endpoints() {
let document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.node(CanvasNode::new(
"b",
point(px(100.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let mut edge = CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
edge.route =
CanvasEdgeRoute::polyline([point(px(40.0), px(50.0)), point(px(80.0), px(50.0))]);
let route_points = document.edge_route_points(&edge).unwrap();
assert_eq!(
route_points,
vec![
point(px(5.0), px(5.0)),
point(px(40.0), px(50.0)),
point(px(80.0), px(50.0)),
point(px(105.0), px(5.0)),
]
);
}
#[test]
fn edge_bounds_include_route_points_and_interaction_width() {
let document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.node(CanvasNode::new(
"b",
point(px(100.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let mut edge = CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
edge.route = CanvasEdgeRoute::polyline([point(px(40.0), px(50.0))]);
edge.route.interaction_width = px(20.0);
let bounds = document.edge_bounds(&edge).unwrap();
assert_eq!(bounds.origin, point(px(-5.0), px(-5.0)));
assert_eq!(bounds.size, size(px(120.0), px(65.0)));
}
#[test]
fn edge_bounds_can_use_custom_router_path() {
struct OffsetRouter;
impl crate::CanvasEdgeRouter for OffsetRouter {
fn route_edge(&self, request: crate::CanvasRouteRequest<'_>) -> crate::CanvasRoutePath {
crate::CanvasRoutePath::polyline([
request.source,
point(px(50.0), px(120.0)),
request.target,
])
}
}
let document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.node(CanvasNode::new(
"b",
point(px(100.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let mut edge = CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
edge.route.interaction_width = px(10.0);
let path = document
.edge_route_path_with_router(&edge, &OffsetRouter)
.unwrap();
let bounds = document
.edge_bounds_with_router(&edge, &OffsetRouter)
.unwrap();
assert_eq!(
path.document_points(),
vec![
point(px(5.0), px(5.0)),
point(px(50.0), px(120.0)),
point(px(105.0), px(5.0)),
]
);
assert_eq!(bounds.origin, point(px(0.0), px(0.0)));
assert_eq!(bounds.size, size(px(110.0), px(125.0)));
}
#[test]
fn rejects_invalid_edge_route_metadata() {
let mut document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.node(CanvasNode::new(
"b",
point(px(100.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let mut empty_kind = CanvasEdge::new(
"empty-kind",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
empty_kind.route.kind = CanvasEdgeRouteKind::new("");
assert_eq!(
document.insert_edge(empty_kind).unwrap_err(),
DocumentError::EmptyEdgeRouteKind(EdgeId::from("empty-kind"))
);
let mut negative_width = CanvasEdge::new(
"negative-width",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
negative_width.route.interaction_width = px(-1.0);
assert_eq!(
document.insert_edge(negative_width).unwrap_err(),
DocumentError::InvalidEdgeInteractionWidth(EdgeId::from("negative-width"))
);
let mut invalid_point = CanvasEdge::new(
"invalid-point",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
);
invalid_point
.route
.waypoints
.push(point(px(f32::NAN), px(0.0)));
assert_eq!(
document.insert_edge(invalid_point).unwrap_err(),
DocumentError::InvalidEdgeRoutePoint(EdgeId::from("invalid-point"))
);
}
#[test]
fn node_update_cannot_break_existing_edge_endpoints() {
let mut node = CanvasNode::new("a", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
node.handles
.push(CanvasHandle::new("out", point(px(10.0), px(5.0))));
let mut document = document_fixture()
.node(node.clone())
.node(CanvasNode::new(
"b",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.edge(CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", Some("out")),
CanvasEndpoint::new("b", None::<&str>),
))
.build();
node.handles.clear();
let err = document
.apply_transaction_with_diff(CanvasTransaction::single(DocumentCommand::UpdateNode(
node,
)))
.unwrap_err();
assert_eq!(
err,
DocumentError::MissingHandle {
node_id: NodeId::from("a"),
handle_id: HandleId::from("out")
}
);
assert!(
document.nodes[&NodeId::from("a")]
.handle(Some(&HandleId::from("out")))
.is_some()
);
}
#[test]
fn applies_transaction_atomically() {
let mut document = document_fixture().build();
let transaction = CanvasTransaction::new([
DocumentCommand::InsertNode(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
)),
DocumentCommand::InsertEdge(CanvasEdge::new(
"bad",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("missing", None::<&str>),
)),
]);
let err = document.apply_transaction(transaction).unwrap_err();
assert_eq!(err, DocumentError::MissingNode(NodeId::from("missing")));
assert!(document.nodes.is_empty());
assert!(document.edges.is_empty());
}
#[test]
fn transaction_inverse_restores_document() {
let mut document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let before = document.clone();
let transaction = CanvasTransaction::new([
DocumentCommand::InsertNode(CanvasNode::new(
"b",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
)),
DocumentCommand::InsertEdge(CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
)),
]);
let inverse = document.invert_transaction(&transaction).unwrap();
document.apply_transaction(transaction).unwrap();
assert_ne!(document, before);
document.apply_transaction(inverse).unwrap();
assert_eq!(document, before);
}
#[test]
fn transaction_diff_tracks_record_changes() {
let mut document = document_fixture()
.node(CanvasNode::new(
"a",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let moved_a = CanvasNode::new("a", point(px(5.0), px(0.0)), size(px(10.0), px(10.0)));
let transaction = CanvasTransaction::new([
DocumentCommand::UpdateNode(moved_a),
DocumentCommand::InsertNode(CanvasNode::new(
"b",
point(px(20.0), px(0.0)),
size(px(10.0), px(10.0)),
)),
DocumentCommand::InsertEdge(CanvasEdge::new(
"a-b",
CanvasEndpoint::new("a", None::<&str>),
CanvasEndpoint::new("b", None::<&str>),
)),
]);
let diff = document.apply_transaction_with_diff(transaction).unwrap();
assert_eq!(
diff.updated.iter().cloned().collect::<Vec<_>>(),
vec![CanvasRecordId::Node(NodeId::from("a"))]
);
assert_eq!(
diff.inserted.iter().cloned().collect::<Vec<_>>(),
vec![
CanvasRecordId::Node(NodeId::from("b")),
CanvasRecordId::Edge(EdgeId::from("a-b")),
]
);
assert!(diff.removed.is_empty());
}
#[test]
fn transaction_diff_tracks_relation_changes() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
let diff = document
.apply_transaction_with_diff(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("group")),
},
))
.unwrap();
assert!(diff.relations_changed);
assert!(!diff.is_empty());
assert!(diff.inserted.is_empty());
assert!(diff.updated.is_empty());
assert!(diff.removed.is_empty());
}
#[test]
fn relation_commands_reject_dangling_records() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.build();
let err = document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("missing")),
},
))
.unwrap_err();
assert_eq!(
err,
DocumentError::MissingRelationRecord(CanvasRecordId::Shape(ShapeId::from("missing")))
);
assert!(document.relations().is_empty());
let err = document
.apply_transaction(CanvasTransaction::single(
DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
"binding",
CanvasRecordId::Node(NodeId::from("child")),
CanvasRecordId::Shape(ShapeId::from("missing")),
)),
))
.unwrap_err();
assert_eq!(
err,
DocumentError::MissingRelationRecord(CanvasRecordId::Shape(ShapeId::from("missing")))
);
}
#[test]
fn deleting_records_prunes_relations() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
document
.apply_transaction(CanvasTransaction::new([
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("group")),
},
DocumentCommand::AddRecordToGroup {
group: CanvasRecordId::Shape(ShapeId::from("group")),
member: CanvasRecordId::Node(NodeId::from("child")),
},
DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
"binding",
CanvasRecordId::Node(NodeId::from("child")),
CanvasRecordId::Shape(ShapeId::from("group")),
)),
]))
.unwrap();
let diff = document
.apply_transaction_with_diff(CanvasTransaction::single(DocumentCommand::RemoveNode(
NodeId::from("child"),
)))
.unwrap();
assert!(diff.relations_changed);
assert!(document.relations().is_empty());
}
#[test]
fn relation_inverse_restores_previous_relations() {
let mut document = document_fixture()
.node(CanvasNode::new(
"child",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
))
.shape(CanvasShape::new(
"group",
Bounds::new(point(px(0.0), px(0.0)), size(px(100.0), px(100.0))),
))
.build();
let before = document.clone();
let transaction = CanvasTransaction::new([
DocumentCommand::SetRecordParent {
child: CanvasRecordId::Node(NodeId::from("child")),
parent: CanvasRecordId::Shape(ShapeId::from("group")),
},
DocumentCommand::AddRecordToGroup {
group: CanvasRecordId::Shape(ShapeId::from("group")),
member: CanvasRecordId::Node(NodeId::from("child")),
},
DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
"binding",
CanvasRecordId::Node(NodeId::from("child")),
CanvasRecordId::Shape(ShapeId::from("group")),
)),
]);
let inverse = document.invert_transaction(&transaction).unwrap();
document.apply_transaction(transaction).unwrap();
assert!(!document.relations().is_empty());
document.apply_transaction(inverse).unwrap();
assert_eq!(document, before);
}
#[test]
fn transaction_diff_compacts_insert_then_remove() {
let mut document = document_fixture().build();
let transaction = CanvasTransaction::new([
DocumentCommand::InsertNode(CanvasNode::new(
"temp",
point(px(0.0), px(0.0)),
size(px(10.0), px(10.0)),
)),
DocumentCommand::RemoveNode(NodeId::from("temp")),
]);
let diff = document.apply_transaction_with_diff(transaction).unwrap();
assert!(diff.is_empty());
assert!(document.nodes.is_empty());
}
#[test]
fn transaction_diff_includes_edges_removed_with_node() {
let mut document = connected_pair_fixture().build();
let diff = document
.apply_transaction_with_diff(CanvasTransaction::single(DocumentCommand::RemoveNode(
NodeId::from("a"),
)))
.unwrap();
assert_eq!(
diff.removed.iter().cloned().collect::<Vec<_>>(),
vec![
CanvasRecordId::Node(NodeId::from("a")),
CanvasRecordId::Edge(EdgeId::from("a-b")),
]
);
assert!(document.edges.is_empty());
}
#[test]
fn document_diff_tracks_metadata_changes() {
let previous = document_fixture().build();
let mut document = previous.clone();
document
.metadata
.insert("title".to_string(), serde_json::json!("Canvas"));
let diff = document.diff_against(&previous);
assert!(diff.metadata_changed);
assert!(!diff.is_empty());
}
#[test]
fn randomized_transaction_batches_match_final_diff_and_inverse() {
let mut rng = TestRng::new(0x7a99_21c8_5f01_4d3b);
let mut generator = CanvasCommandGenerator::default();
let mut document = document_fixture().build();
for _ in 0..96 {
let before = document.clone();
let mut draft = before.clone();
let mut commands = Vec::new();
for _ in 0..(1 + rng.usize(6)) {
let command = generator.next_command(&draft, &mut rng);
draft.apply(command.clone()).unwrap();
commands.push(command);
}
let transaction = CanvasTransaction::new(commands);
let inverse = before.invert_transaction(&transaction).unwrap();
let diff = document
.apply_transaction_with_diff(transaction.clone())
.unwrap();
assert_eq!(document, draft);
assert_eq!(diff, document.diff_against(&before));
document.validate_integrity().unwrap();
document.apply_transaction(inverse).unwrap();
assert_eq!(document, before);
document.apply_transaction(transaction).unwrap();
assert_eq!(document, draft);
}
}
}