1use std::sync::Arc;
5
6use crate::doc_ng::{
7 block_model::{Block, Comment, Image, Live, Pin, Route, RouteLabel, Text},
8 commit::Commit,
9 entity::{Entity, entity},
10 hash::{DocHash, DocKind, Hasher},
11 id::{BlockId, CommentId, ImageId, PinId, RouteId, RouteLabelId, TextId},
12 opcode::{Crud, OpCodes},
13 register::Applied,
14 rev::{Confirmed, Provisional, Rev, RevKind},
15 write_order::{Seq, WriteOrder},
16};
17use ahash::{HashMap, HashSet};
18
19entity! {
20 #[derive(Default)]
26 pub struct TitleBlock(init TitleBlockInit, update TitleBlockUpdate, id ()) {
27 registers {
28 Name => name: String,
29 }
30 namespaces {}
31 constants {}
32 }
33}
34
35#[derive(Default, Clone)]
47pub struct Document<R: RevKind> {
48 rev: R,
52 title_block: TitleBlock,
53 blocks: HashMap<BlockId, Arc<Live<Block>>>,
54 pins: HashMap<PinId, Arc<Live<Pin>>>,
55 routes: HashMap<RouteId, Arc<Live<Route>>>,
56 route_labels: HashMap<RouteLabelId, Arc<Live<RouteLabel>>>,
57 texts: HashMap<TextId, Arc<Live<Text>>>,
58 comments: HashMap<CommentId, Arc<Live<Comment>>>,
59 images: HashMap<ImageId, Arc<Live<Image>>>,
60}
61
62impl Document<Confirmed> {
63 pub fn rev(&self) -> Rev {
66 self.rev.get()
67 }
68
69 pub fn predict(&self) -> Document<Provisional> {
73 Document {
74 rev: Provisional::new(self.rev.get()),
75 title_block: self.title_block.clone(),
76 blocks: self.blocks.clone(),
77 pins: self.pins.clone(),
78 routes: self.routes.clone(),
79 route_labels: self.route_labels.clone(),
80 texts: self.texts.clone(),
81 comments: self.comments.clone(),
82 images: self.images.clone(),
83 }
84 }
85}
86
87impl<R: RevKind> Document<R> {
88 pub fn title_block(&self) -> &TitleBlock {
89 &self.title_block
90 }
91 pub fn block(&self, id: &BlockId) -> Option<&Live<Block>> {
92 self.blocks.get(id).map(Arc::as_ref)
93 }
94 pub fn blocks(&self) -> impl Iterator<Item = (BlockId, &Live<Block>)> {
95 self.blocks.iter().map(|(id, block)| (*id, block.as_ref()))
96 }
97 pub fn pin(&self, id: &PinId) -> Option<&Live<Pin>> {
98 self.pins.get(id).map(Arc::as_ref)
99 }
100 pub fn pins(&self) -> impl Iterator<Item = (PinId, &Live<Pin>)> {
101 self.pins.iter().map(|(id, pin)| (*id, pin.as_ref()))
102 }
103 pub fn route(&self, id: &RouteId) -> Option<&Live<Route>> {
104 self.routes.get(id).map(Arc::as_ref)
105 }
106 pub fn routes(&self) -> impl Iterator<Item = (RouteId, &Live<Route>)> {
107 self.routes.iter().map(|(id, route)| (*id, route.as_ref()))
108 }
109 pub fn route_label(&self, id: &RouteLabelId) -> Option<&Live<RouteLabel>> {
110 self.route_labels.get(id).map(Arc::as_ref)
111 }
112 pub fn route_labels(&self) -> impl Iterator<Item = (RouteLabelId, &Live<RouteLabel>)> {
113 self.route_labels
114 .iter()
115 .map(|(id, label)| (*id, label.as_ref()))
116 }
117 pub fn text(&self, id: &TextId) -> Option<&Live<Text>> {
118 self.texts.get(id).map(Arc::as_ref)
119 }
120 pub fn texts(&self) -> impl Iterator<Item = (TextId, &Live<Text>)> {
121 self.texts.iter().map(|(id, text)| (*id, text.as_ref()))
122 }
123 pub fn comment(&self, id: &CommentId) -> Option<&Live<Comment>> {
124 self.comments.get(id).map(Arc::as_ref)
125 }
126 pub fn comments(&self) -> impl Iterator<Item = (CommentId, &Live<Comment>)> {
127 self.comments
128 .iter()
129 .map(|(id, comment)| (*id, comment.as_ref()))
130 }
131 pub fn image(&self, id: &ImageId) -> Option<&Live<Image>> {
132 self.images.get(id).map(Arc::as_ref)
133 }
134 pub fn images(&self) -> impl Iterator<Item = (ImageId, &Live<Image>)> {
135 self.images.iter().map(|(id, image)| (*id, image.as_ref()))
136 }
137 pub fn try_apply(&self, commit: &Commit) -> Result<Document<R>, FoldError> {
138 let mut new_doc = self.clone();
139 new_doc.rev = self.rev.next();
140 apply(&mut new_doc, commit)?;
141 validate(&new_doc, commit)?;
142 Ok(new_doc)
143 }
144 fn no_block_cycles(&self, id: BlockId) -> bool {
145 let mut current = id;
146 let mut seen = HashSet::default();
147 while let Some(block) = self.block(¤t) {
148 if !seen.insert(current) {
149 return false; }
151 let parent = block.as_ref().parent.as_ref();
152 if parent == &BlockId::NULL {
153 break;
154 }
155 current = *parent;
156 }
157 true
158 }
159 fn validate_block_owner(&self, id: BlockId) -> Result<(), FoldError> {
160 let block = self
161 .block(&id)
162 .ok_or(FoldError::InvalidBlockId(id))?
163 .as_ref();
164 let block_parent = block.parent.as_ref();
165 if block_parent != &BlockId::NULL && !self.blocks.contains_key(block_parent) {
166 return Err(FoldError::InvalidBlockParent(id, *block_parent));
167 }
168 if !self.no_block_cycles(id) {
169 return Err(FoldError::BlockCycle(id));
170 }
171 Ok(())
172 }
173 fn validate_pin_owner(&self, id: PinId) -> Result<(), FoldError> {
174 let pin: &Pin = self.pin(&id).ok_or(FoldError::InvalidPinId(id))?.as_ref();
175 let owner = pin.owner.as_ref();
176 if !self.blocks.contains_key(owner) {
177 return Err(FoldError::InvalidPinOwner(id, *owner));
178 }
179 Ok(())
180 }
181 fn validate_route_owner_and_endpoints(&self, id: RouteId) -> Result<(), FoldError> {
182 let route: &Route = self
183 .route(&id)
184 .ok_or(FoldError::InvalidRouteId(id))?
185 .as_ref();
186 let owner = route.owner.as_ref();
187 if !self.blocks.contains_key(owner) {
188 return Err(FoldError::InvalidRouteOwner(id, *owner));
189 }
190 if !self.pins.contains_key(&route.from) {
191 return Err(FoldError::InvalidRouteFrom(id, route.from));
192 }
193 if !self.pins.contains_key(&route.to) {
194 return Err(FoldError::InvalidRouteTo(id, route.to));
195 }
196 Ok(())
197 }
198 fn validate_route_label_owner(&self, id: RouteLabelId) -> Result<(), FoldError> {
199 let label: &RouteLabel = self
200 .route_label(&id)
201 .ok_or(FoldError::InvalidRouteLabelId(id))?
202 .as_ref();
203 let owner = label.owner.as_ref();
204 if !self.routes.contains_key(owner) {
205 return Err(FoldError::InvalidRouteLabelOwner(id, *owner));
206 }
207 Ok(())
208 }
209 fn validate_text_owner(&self, id: TextId) -> Result<(), FoldError> {
210 let text: &Text = self.text(&id).ok_or(FoldError::InvalidTextId(id))?.as_ref();
211 let owner = text.owner.as_ref();
212 if !self.blocks.contains_key(owner) {
213 return Err(FoldError::InvalidTextOwner(id, *owner));
214 }
215 Ok(())
216 }
217 fn validate_comment_owner(&self, id: CommentId) -> Result<(), FoldError> {
218 let comment: &Comment = self
219 .comment(&id)
220 .ok_or(FoldError::InvalidCommentId(id))?
221 .as_ref();
222 let owner = comment.owner.as_ref();
223 if !self.blocks.contains_key(owner) {
224 return Err(FoldError::InvalidCommentOwner(id, *owner));
225 }
226 Ok(())
227 }
228 fn validate_image_owner(&self, id: ImageId) -> Result<(), FoldError> {
229 let image: &Image = self
230 .image(&id)
231 .ok_or(FoldError::InvalidImageId(id))?
232 .as_ref();
233 let owner = image.owner.as_ref();
234 if !self.blocks.contains_key(owner) {
235 return Err(FoldError::InvalidImageOwner(id, *owner));
236 }
237 Ok(())
238 }
239
240 #[expect(clippy::expect_used, clippy::missing_panics_doc)]
247 pub fn content_hash(&self) -> DocHash {
248 fn sorted<I: Ord + Copy, T>(map: &HashMap<I, Arc<Live<T>>>) -> Vec<(I, &Live<T>)> {
249 let mut entries: Vec<_> = map.iter().map(|(id, live)| (*id, live.as_ref())).collect();
250 entries.sort_by_key(|(id, _)| *id);
251 entries
252 }
253 let mut hasher = Hasher::<DocKind>::new();
254 ciborium::into_writer(
255 &(
256 self.rev.minting(),
259 &self.title_block,
260 sorted(&self.blocks),
261 sorted(&self.pins),
262 sorted(&self.routes),
263 sorted(&self.route_labels),
264 sorted(&self.texts),
265 sorted(&self.comments),
266 sorted(&self.images),
267 ),
268 &mut hasher,
269 )
270 .expect("the document serializes infallibly");
271 hasher.finalize()
272 }
273 pub fn cache(&self) -> DocumentCache<'_, R> {
276 let mut cache = DocumentCache {
277 doc: self,
278 top_level: HashSet::default(),
279 blocks: self
280 .blocks()
281 .filter(|(_, block)| block.is_alive())
282 .map(|(id, _)| (id, BlockIndex::default()))
283 .collect(),
284 routes: self
285 .routes()
286 .filter(|(_, route)| route.is_alive())
287 .map(|(id, _)| (id, RouteIndex::default()))
288 .collect(),
289 suppressed: HashSet::default(),
290 };
291 for (id, block) in self.blocks().filter(|(_, block)| block.is_alive()) {
292 let parent = block.as_ref().parent.as_ref();
293 if parent == &BlockId::NULL {
294 cache.top_level.insert(id);
295 } else if let Some(index) = cache.blocks.get_mut(parent) {
296 index.children.insert(id);
297 }
298 }
299 for (id, pin) in self.pins().filter(|(_, pin)| pin.is_alive()) {
300 if let Some(index) = cache.blocks.get_mut(pin.as_ref().owner.as_ref()) {
301 index.pins.insert(id);
302 }
303 }
304 for (id, route) in self.routes().filter(|(_, route)| route.is_alive()) {
305 let inner = route.as_ref();
306 if let Some(index) = cache.blocks.get_mut(inner.owner.as_ref()) {
307 index.routes.insert(id);
308 }
309 let endpoints_alive = self.pin(&inner.from).is_some_and(Live::is_alive)
310 && self.pin(&inner.to).is_some_and(Live::is_alive);
311 if !endpoints_alive {
312 cache.suppressed.insert(id);
313 }
314 }
315 for (id, label) in self.route_labels().filter(|(_, label)| label.is_alive()) {
316 if let Some(index) = cache.routes.get_mut(label.as_ref().owner.as_ref()) {
317 index.labels.insert(id);
318 }
319 }
320 for (id, text) in self.texts().filter(|(_, text)| text.is_alive()) {
321 if let Some(index) = cache.blocks.get_mut(text.as_ref().owner.as_ref()) {
322 index.texts.insert(id);
323 }
324 }
325 for (id, comment) in self.comments().filter(|(_, comment)| comment.is_alive()) {
326 if let Some(index) = cache.blocks.get_mut(comment.as_ref().owner.as_ref()) {
327 index.comments.insert(id);
328 }
329 }
330 for (id, image) in self.images().filter(|(_, image)| image.is_alive()) {
331 if let Some(index) = cache.blocks.get_mut(image.as_ref().owner.as_ref()) {
332 index.images.insert(id);
333 }
334 }
335 cache
336 }
337}
338
339pub struct DocumentCache<'a, R: RevKind> {
347 pub doc: &'a Document<R>,
348 pub top_level: HashSet<BlockId>,
350 pub blocks: HashMap<BlockId, BlockIndex>,
351 pub routes: HashMap<RouteId, RouteIndex>,
352 pub suppressed: HashSet<RouteId>,
356}
357
358#[derive(Debug, Default, PartialEq, Eq)]
359pub struct BlockIndex {
360 pub pins: HashSet<PinId>,
361 pub routes: HashSet<RouteId>,
362 pub texts: HashSet<TextId>,
363 pub comments: HashSet<CommentId>,
364 pub images: HashSet<ImageId>,
365 pub children: HashSet<BlockId>,
366}
367
368#[derive(Debug, Default, PartialEq, Eq)]
369pub struct RouteIndex {
370 pub labels: HashSet<RouteLabelId>,
371}
372
373#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
374pub enum FoldError {
375 #[error("invalid block id {0}")]
376 InvalidBlockId(BlockId),
377 #[error("invalid pin id {0}")]
378 InvalidPinId(PinId),
379 #[error("invalid route id {0}")]
380 InvalidRouteId(RouteId),
381 #[error("invalid route label id {0}")]
382 InvalidRouteLabelId(RouteLabelId),
383 #[error("invalid text id {0}")]
384 InvalidTextId(TextId),
385 #[error("invalid comment id {0}")]
386 InvalidCommentId(CommentId),
387 #[error("invalid image id {0}")]
388 InvalidImageId(ImageId),
389 #[error("Block {0} parent {1} is not a block in the document")]
390 InvalidBlockParent(BlockId, BlockId),
391 #[error("Pin {0} owner {1} is not a block in the document")]
392 InvalidPinOwner(PinId, BlockId),
393 #[error("Route {0} owner {1} is not a block in the document")]
394 InvalidRouteOwner(RouteId, BlockId),
395 #[error("Route {0} from pin {1} is not a pin in the document")]
396 InvalidRouteFrom(RouteId, PinId),
397 #[error("Route {0} to pin {1} is not a pin in the document")]
398 InvalidRouteTo(RouteId, PinId),
399 #[error("RouteLabel {0} owner {1} is not a route in the document")]
400 InvalidRouteLabelOwner(RouteLabelId, RouteId),
401 #[error("Text {0} owner {1} is not a block in the document")]
402 InvalidTextOwner(TextId, BlockId),
403 #[error("Comment {0} owner {1} is not a block in the document")]
404 InvalidCommentOwner(CommentId, BlockId),
405 #[error("Image {0} owner {1} is not a block in the document")]
406 InvalidImageOwner(ImageId, BlockId),
407 #[error("Block {0} is in a cycle of parent links")]
408 BlockCycle(BlockId),
409}
410
411fn apply<R: RevKind>(doc: &mut Document<R>, commit: &Commit) -> Result<(), FoldError> {
412 for (ndx, op) in commit.ops().iter().enumerate() {
413 let order = WriteOrder::new(doc.rev.minting(), Seq::new(ndx));
414 match op {
415 OpCodes::Document(update) => {
416 doc.title_block.apply(update, order);
417 }
418 OpCodes::Block(id, crud) => {
419 apply_crud_to_entity(&mut doc.blocks, *id, crud, order)
420 .ok_or(FoldError::InvalidBlockId(*id))?;
421 }
422 OpCodes::Pin(id, crud) => {
423 apply_crud_to_entity(&mut doc.pins, *id, crud, order)
424 .ok_or(FoldError::InvalidPinId(*id))?;
425 }
426 OpCodes::Route(id, crud) => {
427 apply_crud_to_entity(&mut doc.routes, *id, crud, order)
428 .ok_or(FoldError::InvalidRouteId(*id))?;
429 }
430 OpCodes::RouteLabel(id, crud) => {
431 apply_crud_to_entity(&mut doc.route_labels, *id, crud, order)
432 .ok_or(FoldError::InvalidRouteLabelId(*id))?;
433 }
434 OpCodes::Text(id, crud) => {
435 apply_crud_to_entity(&mut doc.texts, *id, crud, order)
436 .ok_or(FoldError::InvalidTextId(*id))?;
437 }
438 OpCodes::Comment(id, crud) => {
439 apply_crud_to_entity(&mut doc.comments, *id, crud, order)
440 .ok_or(FoldError::InvalidCommentId(*id))?;
441 }
442 OpCodes::Image(id, crud) => {
443 apply_crud_to_entity(&mut doc.images, *id, crud, order)
444 .ok_or(FoldError::InvalidImageId(*id))?;
445 }
446 }
447 }
448 Ok(())
449}
450
451fn validate<R: RevKind>(doc: &Document<R>, commit: &Commit) -> Result<(), FoldError> {
452 for op in commit.ops() {
453 match op {
454 OpCodes::Block(id, _) => {
455 doc.validate_block_owner(*id)?;
456 }
457 OpCodes::Pin(id, _) => {
458 doc.validate_pin_owner(*id)?;
459 }
460 OpCodes::Route(id, _) => {
461 doc.validate_route_owner_and_endpoints(*id)?;
462 }
463 OpCodes::RouteLabel(id, _) => {
464 doc.validate_route_label_owner(*id)?;
465 }
466 OpCodes::Text(id, _) => {
467 doc.validate_text_owner(*id)?;
468 }
469 OpCodes::Comment(id, _) => {
470 doc.validate_comment_owner(*id)?;
471 }
472 OpCodes::Image(id, _) => {
473 doc.validate_image_owner(*id)?;
474 }
475 OpCodes::Document(_) => {}
476 }
477 }
478 Ok(())
479}
480
481fn apply_crud_to_entity<E: Entity + Clone>(
485 map: &mut HashMap<E::Id, Arc<Live<E>>>,
486 id: E::Id,
487 crud: &Crud<E::Init, E::Update>,
488 order: WriteOrder,
489) -> Option<Applied> {
490 match crud {
491 Crud::Create(init) => {
492 if map.contains_key(&id) {
493 return None;
494 }
495 let live = Live::new(E::from_init(init, order), order);
496 map.insert(id, Arc::new(live));
497 Some(Applied::Won)
498 }
499 Crud::Restore => Some(Arc::make_mut(map.get_mut(&id)?).restore(order)),
500 Crud::Update(update) => Some(Arc::make_mut(map.get_mut(&id)?).apply_update(update, order)),
501 Crud::Delete => Some(Arc::make_mut(map.get_mut(&id)?).delete(order)),
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::doc_ng::fixtures::{
509 block_id, comment_id, image_id, pin_id, route_id, route_label_id, text_id,
510 };
511 use crate::doc_ng::{
512 block_model::{
513 BlockInit, BlockUpdate, CommentInit, Icon, ImageInit, LabelInit, PinInit, PinUpdate,
514 RouteInit, RouteLabelInit, TextInit,
515 },
516 geometry::{FracVal, GridPoint, GridRect, GridSize, ScreenRect},
517 hash::AssetHash,
518 values::{LabelSide, PinDir, Role},
519 };
520
521 fn at(rev: u64) -> WriteOrder {
522 WriteOrder::new(Rev::new(rev), Seq::new(0))
523 }
524
525 fn rename(name: &str) -> Commit {
526 Commit::new(
527 "Renamed the document".into(),
528 vec![OpCodes::Document(TitleBlockUpdate::Name(name.into()))],
529 )
530 }
531
532 fn label_init(name: String) -> LabelInit {
533 LabelInit {
534 name,
535 side: LabelSide::default(),
536 offset: FracVal::default(),
537 hidden: false,
538 }
539 }
540
541 fn block_create(byte: u8) -> OpCodes {
542 OpCodes::Block(
543 block_id(byte),
544 Crud::Create(BlockInit {
545 parent: BlockId::NULL,
546 rect: GridRect::default(),
547 locked: false,
548 title: label_init(format!("b{byte}")),
549 type_label: label_init(String::new()),
550 icon: Icon::default(),
551 }),
552 )
553 }
554
555 fn route_create(byte: u8, owner: u8, from: u8, to: u8) -> OpCodes {
556 OpCodes::Route(
557 route_id(byte),
558 Crud::Create(RouteInit {
559 owner: block_id(owner),
560 name: format!("r{byte}"),
561 from: pin_id(from),
562 to: pin_id(to),
563 role: Role::default(),
564 waypoints: Vec::new(),
565 }),
566 )
567 }
568
569 fn wired_document() -> Document<Confirmed> {
573 Document::<Confirmed>::default()
574 .try_apply(&Commit::new(
575 "Wired a document".into(),
576 vec![
577 block_create(1),
578 block_create(2),
579 reparent(2, 1),
580 pin_create(3, block_id(1)),
581 pin_create(4, block_id(1)),
582 route_create(5, 1, 3, 4),
583 OpCodes::RouteLabel(
584 route_label_id(6),
585 Crud::Create(RouteLabelInit {
586 owner: route_id(5),
587 pos: FracVal::default(),
588 }),
589 ),
590 OpCodes::Text(
591 text_id(7),
592 Crud::Create(TextInit {
593 owner: block_id(1),
594 text: "note".into(),
595 pos: GridPoint::default(),
596 role: Role::default(),
597 }),
598 ),
599 OpCodes::Comment(
600 comment_id(8),
601 Crud::Create(CommentInit {
602 owner: block_id(1),
603 rect: GridRect::default(),
604 role: Role::default(),
605 title: label_init("c8".into()),
606 }),
607 ),
608 OpCodes::Image(
609 image_id(9),
610 Crud::Create(ImageInit {
611 owner: block_id(1),
612 asset: AssetHash::default(),
613 rect: ScreenRect::default(),
614 }),
615 ),
616 ],
617 ))
618 .expect("the fold succeeds")
619 }
620
621 fn reparent(child: u8, parent: u8) -> OpCodes {
622 OpCodes::Block(
623 block_id(child),
624 Crud::Update(BlockUpdate::Parent(block_id(parent))),
625 )
626 }
627
628 fn rect(x: i32, y: i32) -> GridRect {
629 GridRect {
630 top_left: GridPoint { x, y },
631 size: GridSize { w: 4, h: 4 },
632 }
633 }
634
635 fn resize(block: u8, to: GridRect) -> OpCodes {
636 OpCodes::Block(block_id(block), Crud::Update(BlockUpdate::Rect(to)))
637 }
638
639 fn rename_pin(byte: u8, name: &str) -> OpCodes {
640 OpCodes::Pin(pin_id(byte), Crud::Update(PinUpdate::Name(name.into())))
641 }
642
643 fn pin_delete(byte: u8) -> OpCodes {
644 OpCodes::Pin(pin_id(byte), Crud::Delete)
645 }
646
647 fn pin_restore(byte: u8) -> OpCodes {
648 OpCodes::Pin(pin_id(byte), Crud::Restore)
649 }
650
651 fn block_with_pin() -> Document<Confirmed> {
652 Document::<Confirmed>::default()
653 .try_apply(&Commit::new(
654 "Added a block with a pin".into(),
655 vec![block_create(1), pin_create(2, block_id(1))],
656 ))
657 .expect("the fold succeeds")
658 }
659
660 fn pin_create(byte: u8, owner: BlockId) -> OpCodes {
661 OpCodes::Pin(
662 pin_id(byte),
663 Crud::Create(PinInit {
664 owner,
665 name: format!("p{byte}"),
666 type_name: String::new(),
667 tag: String::new(),
668 tag_hidden: false,
669 rect: GridRect::default(),
670 dir: PinDir::default(),
671 pin_accent: Role::default(),
672 port_accent: Role::default(),
673 port_pin_accent: Role::default(),
674 flip_lr: false,
675 }),
676 )
677 }
678
679 #[test]
680 fn a_name_update_folds_into_a_new_document_and_the_source_stays_frozen() {
681 let doc = Document::<Confirmed>::default();
682
683 let next = doc
684 .try_apply(&rename("drawing"))
685 .expect("the fold succeeds");
686 assert_eq!(next.rev(), Rev::new(1), "the fold mints the successor rev");
687 assert_eq!(next.title_block().name.as_ref().as_str(), "drawing");
688 assert_eq!(next.title_block().name.order(), at(1));
689 assert_eq!(doc.rev(), Rev::ZERO, "the source document is frozen");
690 assert_eq!(
691 doc.title_block().name.as_ref().as_str(),
692 "",
693 "the source document is frozen"
694 );
695 }
696
697 #[test]
701 fn a_bad_op_refuses_the_whole_commit() {
702 let doc = Document::<Confirmed>::default();
703 let unknown = block_id(7);
704 let commit = Commit::new(
705 "Deleted a block".into(),
706 vec![
707 OpCodes::Document(TitleBlockUpdate::Name("drawing".into())),
708 OpCodes::Block(unknown, Crud::Delete),
709 ],
710 );
711
712 let Err(error) = doc.try_apply(&commit) else {
713 panic!("a delete of an unknown id must refuse the commit");
714 };
715 assert_eq!(error, FoldError::InvalidBlockId(unknown));
716 }
717
718 #[test]
721 fn a_refused_commit_mints_no_rev() {
722 let doc = Document::<Confirmed>::default()
723 .try_apply(&rename("drawing"))
724 .expect("the fold succeeds");
725 assert_eq!(doc.rev(), Rev::new(1));
726
727 let unknown = block_id(7);
728 let bad = Commit::new(
729 "Deleted a block".into(),
730 vec![OpCodes::Block(unknown, Crud::Delete)],
731 );
732 assert!(doc.try_apply(&bad).is_err());
733
734 let next = doc
735 .try_apply(&rename("schematic"))
736 .expect("the fold succeeds");
737 assert_eq!(next.rev(), Rev::new(2));
738 }
739
740 #[test]
743 fn the_content_hash_is_canonical_over_map_iteration_order() {
744 let commit = Commit::new(
745 "Created twelve blocks".into(),
746 (1..=12u8).map(block_create).collect(),
747 );
748 let a = Document::<Confirmed>::default()
749 .try_apply(&commit)
750 .expect("the fold succeeds");
751 let b = Document::<Confirmed>::default()
752 .try_apply(&commit)
753 .expect("the fold succeeds");
754
755 assert_ne!(
756 a.blocks.keys().copied().collect::<Vec<_>>(),
757 b.blocks.keys().copied().collect::<Vec<_>>(),
758 "the fixture must iterate differently or canonicalization is untested"
759 );
760 assert_eq!(a.content_hash(), b.content_hash());
761 }
762
763 #[test]
764 fn the_content_hash_is_deterministic_and_detects_divergence() {
765 let doc = Document::<Confirmed>::default()
766 .try_apply(&rename("drawing"))
767 .expect("the fold succeeds");
768 assert_eq!(doc.content_hash(), doc.content_hash());
769
770 let diverged = doc
771 .try_apply(&rename("schematic"))
772 .expect("the fold succeeds");
773 assert_ne!(doc.content_hash(), diverged.content_hash());
774 }
775
776 #[test]
780 fn a_pin_needs_its_owner_block() {
781 let owner = block_id(1);
782 let good = Commit::new(
783 "Added a block with a pin".into(),
784 vec![pin_create(2, owner), block_create(1)],
785 );
786 let doc = Document::<Confirmed>::default()
787 .try_apply(&good)
788 .expect("an owner created anywhere in the commit resolves");
789 assert!(doc.pin(&pin_id(2)).is_some());
790
791 let stranger = block_id(9);
792 let bad = Commit::new("Added an orphan pin".into(), vec![pin_create(3, stranger)]);
793 let Err(error) = doc.try_apply(&bad) else {
794 panic!("a pin with an unknown owner must refuse the commit");
795 };
796 assert_eq!(error, FoldError::InvalidPinOwner(pin_id(3), stranger));
797 assert!(
798 doc.pin(&pin_id(3)).is_none(),
799 "the refused create must leave no trace"
800 );
801 }
802
803 #[test]
807 fn an_owner_update_to_an_unknown_block_is_refused() {
808 let owner = block_id(1);
809 let doc = Document::<Confirmed>::default()
810 .try_apply(&Commit::new(
811 "Added a block with a pin".into(),
812 vec![block_create(1), pin_create(2, owner)],
813 ))
814 .expect("the fold succeeds");
815
816 let stranger = block_id(9);
817 let reown = Commit::new(
818 "Re-owned the pin".into(),
819 vec![OpCodes::Pin(
820 pin_id(2),
821 Crud::Update(PinUpdate::Owner(stranger)),
822 )],
823 );
824 let Err(error) = doc.try_apply(&reown) else {
825 panic!("re-owning to an unknown block must refuse the commit");
826 };
827 assert_eq!(error, FoldError::InvalidPinOwner(pin_id(2), stranger));
828 assert_eq!(
829 doc.pin(&pin_id(2)).map(|pin| *pin.as_ref().owner.as_ref()),
830 Some(owner),
831 "the source document still holds the valid owner"
832 );
833 }
834
835 #[test]
838 fn a_parent_cycle_is_refused() {
839 let doc = Document::<Confirmed>::default()
840 .try_apply(&Commit::new(
841 "Added two blocks".into(),
842 vec![block_create(1), block_create(2)],
843 ))
844 .expect("the fold succeeds");
845
846 let nested = doc
847 .try_apply(&Commit::new(
848 "Nested 1 under 2".into(),
849 vec![reparent(1, 2)],
850 ))
851 .expect("a loop-free reparent lands");
852 assert_eq!(
853 nested
854 .block(&block_id(1))
855 .map(|block| *block.as_ref().parent.as_ref()),
856 Some(block_id(2)),
857 "the fixture must nest or the loop below tests nothing"
858 );
859
860 let Err(error) =
861 nested.try_apply(&Commit::new("Closed the loop".into(), vec![reparent(2, 1)]))
862 else {
863 panic!("closing a parent loop must refuse the commit");
864 };
865 assert_eq!(error, FoldError::BlockCycle(block_id(2)));
866
867 let Err(error) = doc.try_apply(&Commit::new(
868 "Swapped parents".into(),
869 vec![reparent(1, 2), reparent(2, 1)],
870 )) else {
871 panic!("a loop closed within one commit must refuse it");
872 };
873 assert_eq!(error, FoldError::BlockCycle(block_id(1)));
874
875 let Err(error) = doc.try_apply(&Commit::new("Self parent".into(), vec![reparent(1, 1)]))
876 else {
877 panic!("a self-parent must refuse the commit");
878 };
879 assert_eq!(error, FoldError::BlockCycle(block_id(1)));
880 }
881
882 #[test]
886 fn a_commit_writing_one_register_twice_ends_on_its_last_write() {
887 let commit = Commit::new(
888 "Created and nudged a block".into(),
889 vec![
890 block_create(1),
891 resize(1, rect(1, 1)),
892 resize(1, rect(2, 2)),
893 ],
894 );
895 let doc = Document::<Confirmed>::default()
896 .try_apply(&commit)
897 .expect("the fold succeeds");
898
899 let block = doc.block(&block_id(1)).expect("created").as_ref();
900 assert_eq!(block.rect.as_ref(), &rect(2, 2));
901 assert_eq!(
902 block.rect.order(),
903 WriteOrder::new(Rev::new(1), Seq::new(2))
904 );
905 }
906
907 #[test]
913 fn a_later_commits_write_displaces_an_earlier_ones() {
914 let doc = Document::<Confirmed>::default()
915 .try_apply(&Commit::new("Added a block".into(), vec![block_create(1)]))
916 .expect("the fold succeeds")
917 .try_apply(&Commit::new("Moved it".into(), vec![resize(1, rect(1, 1))]))
918 .expect("the fold succeeds")
919 .try_apply(&Commit::new(
920 "Moved it again".into(),
921 vec![resize(1, rect(2, 2))],
922 ))
923 .expect("the fold succeeds");
924
925 let block = doc.block(&block_id(1)).expect("created").as_ref();
926 assert_eq!(block.rect.as_ref(), &rect(2, 2));
927 assert_eq!(
928 block.rect.order(),
929 WriteOrder::new(Rev::new(3), Seq::new(0))
930 );
931 assert_eq!(
932 block.locked.order(),
933 WriteOrder::new(Rev::new(1), Seq::new(0)),
934 "untouched registers keep their create order"
935 );
936 }
937
938 #[test]
942 fn deleting_tombstones_an_entity_but_keeps_it_restorable() {
943 let deleted = block_with_pin()
944 .try_apply(&Commit::new("Deleted the pin".into(), vec![pin_delete(2)]))
945 .expect("the fold succeeds");
946 let pin = deleted
947 .pin(&pin_id(2))
948 .expect("the tombstone keeps the identity");
949 assert!(!pin.is_alive());
950 assert_eq!(pin.as_ref().name.as_ref(), "p2", "the inner is retained");
951
952 let restored = deleted
953 .try_apply(&Commit::new(
954 "Restored the pin".into(),
955 vec![pin_restore(2)],
956 ))
957 .expect("restoring a tombstone");
958 let pin = restored.pin(&pin_id(2)).expect("restored");
959 assert!(pin.is_alive());
960 assert_eq!(
961 pin.as_ref().name.order(),
962 WriteOrder::new(Rev::new(1), Seq::new(1)),
963 "delete and restore never touch the inner registers"
964 );
965 }
966
967 #[test]
972 fn an_edit_after_a_delete_lands_in_the_inner_and_surfaces_on_restore() {
973 let edited = block_with_pin()
974 .try_apply(&Commit::new("Deleted the pin".into(), vec![pin_delete(2)]))
975 .expect("the fold succeeds")
976 .try_apply(&Commit::new(
977 "Renamed the pin".into(),
978 vec![rename_pin(2, "clk")],
979 ))
980 .expect("an edit to a tombstone folds — absorb, not reject");
981
982 let pin = edited.pin(&pin_id(2)).expect("tombstone");
983 assert!(
984 !pin.is_alive(),
985 "no edit at any order resurrects a tombstone"
986 );
987 assert_eq!(
988 pin.as_ref().name.as_ref(),
989 "clk",
990 "the edit landed in the retained inner"
991 );
992
993 let restored = edited
994 .try_apply(&Commit::new(
995 "Restored the pin".into(),
996 vec![pin_restore(2)],
997 ))
998 .expect("the fold succeeds");
999 let pin = restored.pin(&pin_id(2)).expect("restored");
1000 assert!(pin.is_alive());
1001 assert_eq!(
1002 pin.as_ref().name.as_ref(),
1003 "clk",
1004 "the absorbed edit surfaces on restore"
1005 );
1006 }
1007
1008 #[test]
1012 fn replay_is_a_function_of_the_log_alone() {
1013 let log = [
1014 Commit::new(
1015 "Added a block with a pin".into(),
1016 vec![block_create(1), pin_create(2, block_id(1))],
1017 ),
1018 Commit::new("Renamed the pin".into(), vec![rename_pin(2, "clk")]),
1019 Commit::new("Deleted the pin".into(), vec![pin_delete(2)]),
1020 Commit::new("Restored the pin".into(), vec![pin_restore(2)]),
1021 ];
1022 let replay = || {
1023 log.iter()
1024 .try_fold(Document::<Confirmed>::default(), |doc, commit| {
1025 doc.try_apply(commit)
1026 })
1027 };
1028
1029 let one = replay().expect("the fold succeeds");
1030 let other = replay().expect("the fold succeeds");
1031 assert_eq!(
1032 one.rev(),
1033 Rev::new(4),
1034 "the fixture must fold the whole log"
1035 );
1036 assert_eq!(one.content_hash(), other.content_hash());
1037 }
1038
1039 #[test]
1040 fn the_cache_indexes_every_kind_and_the_containment_tree() {
1041 let doc = wired_document();
1042 let cache = doc.cache();
1043
1044 assert_eq!(cache.top_level, [block_id(1)].into_iter().collect());
1045 let index = &cache.blocks[&block_id(1)];
1046 assert_eq!(index.children, [block_id(2)].into_iter().collect());
1047 assert_eq!(index.pins, [pin_id(3), pin_id(4)].into_iter().collect());
1048 assert_eq!(index.routes, [route_id(5)].into_iter().collect());
1049 assert_eq!(index.texts, [text_id(7)].into_iter().collect());
1050 assert_eq!(index.comments, [comment_id(8)].into_iter().collect());
1051 assert_eq!(index.images, [image_id(9)].into_iter().collect());
1052 assert_eq!(
1053 cache.blocks[&block_id(2)],
1054 BlockIndex::default(),
1055 "the nested block owns nothing"
1056 );
1057 assert_eq!(
1058 cache.routes[&route_id(5)].labels,
1059 [route_label_id(6)].into_iter().collect()
1060 );
1061 assert!(cache.suppressed.is_empty());
1062 }
1063
1064 #[test]
1067 fn a_tombstoned_child_leaves_its_parents_sets() {
1068 let doc = wired_document()
1069 .try_apply(&Commit::new(
1070 "Deleted a pin and the nested block".into(),
1071 vec![pin_delete(4), OpCodes::Block(block_id(2), Crud::Delete)],
1072 ))
1073 .expect("the fold succeeds");
1074 let cache = doc.cache();
1075
1076 let index = &cache.blocks[&block_id(1)];
1077 assert_eq!(
1078 index.pins,
1079 [pin_id(3)].into_iter().collect(),
1080 "a tombstoned pin leaves the owner's set"
1081 );
1082 assert!(
1083 index.children.is_empty(),
1084 "a tombstoned child leaves the parent's list"
1085 );
1086 assert!(
1087 !cache.blocks.contains_key(&block_id(2)),
1088 "no entries for tombstoned elements"
1089 );
1090 }
1091
1092 #[test]
1096 fn a_route_with_a_tombstoned_endpoint_is_suppressed_and_revived() {
1097 let deleted = wired_document()
1098 .try_apply(&Commit::new(
1099 "Deleted an endpoint".into(),
1100 vec![pin_delete(3)],
1101 ))
1102 .expect("the fold succeeds");
1103 assert!(
1104 deleted.route(&route_id(5)).expect("route").is_alive(),
1105 "the fixture's route must stay authored-alive"
1106 );
1107 assert!(
1108 deleted.pin(&pin_id(3)).is_some(),
1109 "the tombstoned endpoint must remain present or suppression tests nothing"
1110 );
1111
1112 let cache = deleted.cache();
1113 assert_eq!(cache.suppressed, [route_id(5)].into_iter().collect());
1114 assert!(
1115 cache.blocks[&block_id(1)].routes.contains(&route_id(5)),
1116 "a suppressed route stays indexed; suppression is a flag, not removal"
1117 );
1118
1119 let restored = deleted
1120 .try_apply(&Commit::new(
1121 "Restored the endpoint".into(),
1122 vec![pin_restore(3)],
1123 ))
1124 .expect("the fold succeeds");
1125 assert!(
1126 restored.cache().suppressed.is_empty(),
1127 "restoring the endpoint revives the route"
1128 );
1129 }
1130}