1use std::sync::Arc;
5
6use crate::{
7 block_model::{Area, Asset, Block, Image, Pin, Route, RouteLabel, Text},
8 commit::Commit,
9 entity::{Entity, entity},
10 hash::{AssetHash, AssetKind, HashedMap},
11 id::{
12 Allocator, AreaId, BlockId, EntityRef, Id, IdKind, ImageId, PinId, RouteId, RouteLabelId,
13 TextId,
14 },
15 opcode::{Crud, OpCodes},
16 rev::{DocStamp, Rev},
17};
18use ahash::{HashMap, HashSet};
19use serde::{Deserialize, Serialize, Serializer};
20use std::collections::{BTreeMap, BTreeSet};
21
22entity! {
23 pub struct TitleBlock(update TitleBlockUpdate, id ()) {
28 registers {
29 Name => name: String,
30 Top => top: BlockId,
35 }
36 namespaces {}
37 constants {}
38 }
39}
40
41pub const CURRENT_VERSION: u32 = 3;
49
50#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
51#[error("document format version {0} is newer than this build reads (up to {CURRENT_VERSION})")]
52pub struct UnsupportedVersion(u32);
53
54#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
59#[serde(try_from = "u32")]
60pub struct FormatVersion(u32);
61
62impl Default for FormatVersion {
63 fn default() -> Self {
64 FormatVersion(CURRENT_VERSION)
65 }
66}
67
68impl TryFrom<u32> for FormatVersion {
69 type Error = UnsupportedVersion;
70 fn try_from(version: u32) -> Result<Self, Self::Error> {
71 if version > CURRENT_VERSION {
72 return Err(UnsupportedVersion(version));
73 }
74 Ok(FormatVersion(version))
75 }
76}
77
78#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
87pub struct Content {
88 version: FormatVersion,
89 #[serde(flatten)]
90 title_block: TitleBlock,
91 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
92 blocks: BTreeMap<BlockId, Arc<Block>>,
93 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
94 pins: BTreeMap<PinId, Arc<Pin>>,
95 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
96 routes: BTreeMap<RouteId, Arc<Route>>,
97 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
98 route_labels: BTreeMap<RouteLabelId, Arc<RouteLabel>>,
99 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
100 texts: BTreeMap<TextId, Arc<Text>>,
101 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
102 areas: BTreeMap<AreaId, Arc<Area>>,
103 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
104 images: BTreeMap<ImageId, Arc<Image>>,
105 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
109 assets: HashedMap<AssetKind, Asset>,
110}
111
112#[derive(Clone, Debug, Deserialize)]
122#[serde(from = "Content")]
123pub struct Document {
124 rev: Rev,
128 stamp: DocStamp,
133 ids: Allocator,
141 content: Content,
142}
143
144impl Serialize for Document {
145 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
146 self.content.serialize(serializer)
147 }
148}
149
150impl PartialEq for Document {
154 fn eq(&self, other: &Self) -> bool {
155 self.content == other.content
156 }
157}
158
159impl From<Content> for Document {
160 fn from(content: Content) -> Self {
161 let mut document = Document {
162 rev: Rev::default(),
163 stamp: DocStamp::next(),
164 ids: Allocator::default(),
165 content,
166 };
167 document.observe_own_ids();
168 document
169 }
170}
171
172impl Default for Document {
173 fn default() -> Self {
174 Content::default().into()
175 }
176}
177
178impl Document {
179 pub fn rev(&self) -> Rev {
182 self.rev
183 }
184
185 pub fn stamp(&self) -> DocStamp {
188 self.stamp
189 }
190 pub fn ids(&self) -> Allocator {
192 self.ids
193 }
194 pub(crate) fn restored_at(&mut self, rev: Rev, reached: &Document) {
212 self.rev = rev;
213 self.stamp = DocStamp::next();
214 self.ids.raise_to(&reached.ids);
215 }
216
217 pub fn positioned_at(&mut self, rev: Rev) {
221 self.rev = rev;
222 }
223
224 pub fn mint<K: IdKind>(&mut self) -> Id<K> {
227 self.ids.mint()
228 }
229 pub fn title_block(&self) -> &TitleBlock {
230 &self.content.title_block
231 }
232 pub fn block(&self, id: &BlockId) -> Option<&Block> {
233 self.content.blocks.get(id).map(Arc::as_ref)
234 }
235 pub fn blocks(&self) -> impl Iterator<Item = (BlockId, &Block)> {
236 self.content
237 .blocks
238 .iter()
239 .map(|(id, block)| (*id, block.as_ref()))
240 }
241 pub fn pin(&self, id: &PinId) -> Option<&Pin> {
242 self.content.pins.get(id).map(Arc::as_ref)
243 }
244 pub fn pins(&self) -> impl Iterator<Item = (PinId, &Pin)> {
245 self.content
246 .pins
247 .iter()
248 .map(|(id, pin)| (*id, pin.as_ref()))
249 }
250 pub fn route(&self, id: &RouteId) -> Option<&Route> {
251 self.content.routes.get(id).map(Arc::as_ref)
252 }
253 pub fn routes(&self) -> impl Iterator<Item = (RouteId, &Route)> {
254 self.content
255 .routes
256 .iter()
257 .map(|(id, route)| (*id, route.as_ref()))
258 }
259 pub fn route_label(&self, id: &RouteLabelId) -> Option<&RouteLabel> {
260 self.content.route_labels.get(id).map(Arc::as_ref)
261 }
262 pub fn route_labels(&self) -> impl Iterator<Item = (RouteLabelId, &RouteLabel)> {
263 self.content
264 .route_labels
265 .iter()
266 .map(|(id, label)| (*id, label.as_ref()))
267 }
268 pub fn text(&self, id: &TextId) -> Option<&Text> {
269 self.content.texts.get(id).map(Arc::as_ref)
270 }
271 pub fn texts(&self) -> impl Iterator<Item = (TextId, &Text)> {
272 self.content
273 .texts
274 .iter()
275 .map(|(id, text)| (*id, text.as_ref()))
276 }
277 pub fn area(&self, id: &AreaId) -> Option<&Area> {
278 self.content.areas.get(id).map(Arc::as_ref)
279 }
280 pub fn areas(&self) -> impl Iterator<Item = (AreaId, &Area)> {
281 self.content
282 .areas
283 .iter()
284 .map(|(id, area)| (*id, area.as_ref()))
285 }
286 pub fn image(&self, id: &ImageId) -> Option<&Image> {
287 self.content.images.get(id).map(Arc::as_ref)
288 }
289 pub fn images(&self) -> impl Iterator<Item = (ImageId, &Image)> {
290 self.content
291 .images
292 .iter()
293 .map(|(id, image)| (*id, image.as_ref()))
294 }
295 pub fn asset(&self, hash: &AssetHash) -> Option<&Asset> {
299 self.content.assets.get(hash)
300 }
301 pub fn assets(&self) -> impl Iterator<Item = (AssetHash, &Asset)> {
304 self.content
305 .assets
306 .iter()
307 .map(|(hash, asset)| (*hash, asset))
308 }
309 pub fn try_apply(&self, commit: &Commit) -> Result<Document, FoldError> {
310 let mut new_doc = self.clone();
311 new_doc.rev = self.rev.next();
312 new_doc.stamp = DocStamp::next();
313 apply(&mut new_doc, commit)?;
314 validate(&new_doc, commit)?;
315 Ok(new_doc)
316 }
317 fn no_block_cycles(&self, id: BlockId) -> bool {
318 let mut current = id;
319 let mut seen = HashSet::default();
320 while let Some(block) = self.block(¤t) {
321 if !seen.insert(current) {
322 return false; }
324 if block.parent == BlockId::NULL {
325 break;
326 }
327 current = block.parent;
328 }
329 true
330 }
331 fn validate_block_owner(&self, id: BlockId) -> Result<(), FoldError> {
335 let Some(block) = self.block(&id) else {
336 return Ok(());
337 };
338 if block.parent != BlockId::NULL && !self.content.blocks.contains_key(&block.parent) {
339 return Err(FoldError::InvalidBlockParent(id, block.parent));
340 }
341 if !self.no_block_cycles(id) {
342 return Err(FoldError::BlockCycle(id));
343 }
344 Ok(())
345 }
346 fn holds_scope(&self, scope: BlockId) -> bool {
350 scope == BlockId::NULL || self.content.blocks.contains_key(&scope)
351 }
352 fn validate_pin_owner(&self, id: PinId) -> Result<(), FoldError> {
353 let Some(pin) = self.pin(&id) else {
354 return Ok(());
355 };
356 if !self.holds_scope(pin.owner) {
357 return Err(FoldError::InvalidPinOwner(id, pin.owner));
358 }
359 Ok(())
360 }
361 fn validate_route_owner_and_endpoints(&self, id: RouteId) -> Result<(), FoldError> {
362 let Some(route) = self.route(&id) else {
363 return Ok(());
364 };
365 if !self.holds_scope(route.owner) {
366 return Err(FoldError::InvalidRouteOwner(id, route.owner));
367 }
368 if !self.content.pins.contains_key(&route.from) {
369 return Err(FoldError::InvalidRouteFrom(id, route.from));
370 }
371 if !self.content.pins.contains_key(&route.to) {
372 return Err(FoldError::InvalidRouteTo(id, route.to));
373 }
374 Ok(())
375 }
376 fn validate_route_label_owner(&self, id: RouteLabelId) -> Result<(), FoldError> {
377 let Some(label) = self.route_label(&id) else {
378 return Ok(());
379 };
380 if !self.content.routes.contains_key(&label.owner) {
381 return Err(FoldError::InvalidRouteLabelOwner(id, label.owner));
382 }
383 Ok(())
384 }
385 fn validate_text_owner(&self, id: TextId) -> Result<(), FoldError> {
386 let Some(text) = self.text(&id) else {
387 return Ok(());
388 };
389 if !self.holds_scope(text.owner) {
390 return Err(FoldError::InvalidTextOwner(id, text.owner));
391 }
392 Ok(())
393 }
394 fn validate_area_owner(&self, id: AreaId) -> Result<(), FoldError> {
395 let Some(area) = self.area(&id) else {
396 return Ok(());
397 };
398 if !self.holds_scope(area.owner) {
399 return Err(FoldError::InvalidAreaOwner(id, area.owner));
400 }
401 Ok(())
402 }
403 fn validate_image_owner(&self, id: ImageId) -> Result<(), FoldError> {
404 let Some(image) = self.image(&id) else {
405 return Ok(());
406 };
407 if !self.holds_scope(image.owner) {
408 return Err(FoldError::InvalidImageOwner(id, image.owner));
409 }
410 Ok(())
411 }
412
413 fn observe_own_ids(&mut self) {
417 for id in self.content.blocks.keys() {
418 self.ids.observe(EntityRef::Block(*id));
419 }
420 for id in self.content.pins.keys() {
421 self.ids.observe(EntityRef::Pin(*id));
422 }
423 for id in self.content.routes.keys() {
424 self.ids.observe(EntityRef::Route(*id));
425 }
426 for id in self.content.route_labels.keys() {
427 self.ids.observe(EntityRef::RouteLabel(*id));
428 }
429 for id in self.content.texts.keys() {
430 self.ids.observe(EntityRef::Text(*id));
431 }
432 for id in self.content.areas.keys() {
433 self.ids.observe(EntityRef::Area(*id));
434 }
435 for id in self.content.images.keys() {
436 self.ids.observe(EntityRef::Image(*id));
437 }
438 }
439
440 pub fn creating_commit(&self, label: &str) -> Option<Commit> {
447 fn creates<'a, K, E>(
448 table: &'a BTreeMap<Id<K>, Arc<E>>,
449 op: impl Fn(Id<K>, Crud<E, E::Update>) -> OpCodes + 'a,
450 ) -> impl Iterator<Item = OpCodes> + 'a
451 where
452 K: IdKind,
453 E: Entity + Clone + 'a,
454 {
455 table
456 .iter()
457 .map(move |(id, entity)| op(*id, Crud::Create(E::clone(entity))))
458 }
459 let mut builder = crate::commit::CommitBuilder::new(label);
460 builder.extend(
461 self.content
462 .assets
463 .iter()
464 .map(|(hash, asset)| OpCodes::Asset(*hash, asset.clone())),
465 );
466 builder.extend(creates(&self.content.blocks, OpCodes::Block));
467 builder.extend(creates(&self.content.pins, OpCodes::Pin));
468 builder.extend(creates(&self.content.routes, OpCodes::Route));
469 builder.extend(creates(&self.content.route_labels, OpCodes::RouteLabel));
470 builder.extend(creates(&self.content.texts, OpCodes::Text));
471 builder.extend(creates(&self.content.areas, OpCodes::Area));
472 builder.extend(creates(&self.content.images, OpCodes::Image));
473 let title = &self.content.title_block;
474 if !title.name.is_empty() {
475 builder.push(OpCodes::Document(TitleBlockUpdate::Name(
476 title.name.clone(),
477 )));
478 }
479 if title.top != BlockId::NULL {
480 builder.push(OpCodes::Document(TitleBlockUpdate::Top(title.top)));
481 }
482 builder.seal()
483 }
484
485 pub fn referenced_assets(&self) -> BTreeSet<AssetHash> {
490 let icons = self
491 .content
492 .blocks
493 .values()
494 .map(|block| block.icon.asset)
495 .filter(|hash| *hash != AssetHash::default());
496 let images = self.content.images.values().map(|image| image.asset);
497 icons.chain(images).collect()
498 }
499
500 #[must_use]
506 pub fn without_assets(&self) -> Document {
507 let mut stripped = self.clone();
508 stripped.content.assets.clear();
509 stripped
510 }
511
512 pub fn attach_assets<E>(
520 &mut self,
521 mut payload: impl FnMut(AssetHash) -> Result<Asset, E>,
522 ) -> Result<(), E> {
523 for hash in self.referenced_assets() {
524 if self.content.assets.contains_key(&hash) {
525 continue;
526 }
527 self.content.assets.insert(hash, payload(hash)?);
528 }
529 Ok(())
530 }
531}
532
533#[non_exhaustive]
550#[derive(Clone, Copy)]
551pub struct IndexedDocument<'a> {
552 pub doc: &'a Document,
553 pub index: &'a DocIndex,
554}
555
556#[derive(Default)]
566pub struct DocIndex {
567 built_from: Option<DocStamp>,
570 pub blocks: HashMap<BlockId, BlockIndex>,
576 pub routes: HashMap<RouteId, RouteIndex>,
577 pub routes_by_endpoint: HashMap<PinId, HashSet<RouteId>>,
580}
581
582impl DocIndex {
583 pub fn of(doc: &Document) -> Self {
587 let mut index = Self {
588 built_from: Some(doc.stamp),
589 blocks: doc
590 .blocks()
591 .map(|(id, _)| (id, BlockIndex::default()))
592 .chain(std::iter::once((BlockId::NULL, BlockIndex::default())))
593 .collect(),
594 routes: doc
595 .routes()
596 .map(|(id, _)| (id, RouteIndex::default()))
597 .collect(),
598 routes_by_endpoint: HashMap::default(),
599 };
600 for (id, block) in doc.blocks() {
601 if let Some(entry) = index.blocks.get_mut(&block.parent) {
602 entry.children.insert(id);
603 }
604 }
605 for (id, pin) in doc.pins() {
606 if let Some(entry) = index.blocks.get_mut(&pin.owner) {
607 entry.pins.insert(id);
608 }
609 }
610 for (id, route) in doc.routes() {
611 if let Some(entry) = index.blocks.get_mut(&route.owner) {
612 entry.routes.insert(id);
613 }
614 for endpoint in [route.from, route.to] {
615 index
616 .routes_by_endpoint
617 .entry(endpoint)
618 .or_default()
619 .insert(id);
620 }
621 }
622 for (id, label) in doc.route_labels() {
623 if let Some(entry) = index.routes.get_mut(&label.owner) {
624 entry.labels.insert(id);
625 }
626 }
627 for (id, text) in doc.texts() {
628 if let Some(entry) = index.blocks.get_mut(&text.owner) {
629 entry.texts.insert(id);
630 }
631 }
632 for (id, area) in doc.areas() {
633 if let Some(entry) = index.blocks.get_mut(&area.owner) {
634 entry.areas.insert(id);
635 }
636 }
637 for (id, image) in doc.images() {
638 if let Some(entry) = index.blocks.get_mut(&image.owner) {
639 entry.images.insert(id);
640 }
641 }
642 index
643 }
644
645 pub fn scope(&self, id: BlockId) -> Option<&BlockIndex> {
649 self.blocks.get(&id)
650 }
651
652 pub fn holds_block(&self, id: BlockId) -> bool {
657 id != BlockId::NULL && self.blocks.contains_key(&id)
658 }
659
660 pub fn view<'a>(&'a mut self, doc: &'a Document) -> IndexedDocument<'a> {
663 if self.built_from != Some(doc.stamp) {
664 *self = Self::of(doc);
665 }
666 IndexedDocument { doc, index: self }
667 }
668
669 pub fn view_of<'a>(&'a self, doc: &'a Document) -> Option<IndexedDocument<'a>> {
675 (self.built_from == Some(doc.stamp)).then_some(IndexedDocument { doc, index: self })
676 }
677}
678
679#[derive(Debug, Default, PartialEq, Eq)]
680pub struct BlockIndex {
681 pub pins: HashSet<PinId>,
682 pub routes: HashSet<RouteId>,
683 pub texts: HashSet<TextId>,
684 pub areas: HashSet<AreaId>,
685 pub images: HashSet<ImageId>,
686 pub children: HashSet<BlockId>,
687}
688
689#[derive(Debug, Default, PartialEq, Eq)]
690pub struct RouteIndex {
691 pub labels: HashSet<RouteLabelId>,
692}
693
694pub fn chronological<'a, K, T>(entries: impl Iterator<Item = (Id<K>, &'a T)>) -> Vec<Id<K>>
700where
701 K: crate::id::IdKind,
702 T: 'a,
703{
704 let mut ids: Vec<Id<K>> = entries.map(|(id, _)| id).collect();
705 ids.sort();
706 ids
707}
708
709#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
710pub enum FoldError {
711 #[error("invalid block id {0}")]
712 InvalidBlockId(BlockId),
713 #[error("invalid pin id {0}")]
714 InvalidPinId(PinId),
715 #[error("invalid route id {0}")]
716 InvalidRouteId(RouteId),
717 #[error("invalid route label id {0}")]
718 InvalidRouteLabelId(RouteLabelId),
719 #[error("invalid text id {0}")]
720 InvalidTextId(TextId),
721 #[error("invalid area id {0}")]
722 InvalidAreaId(AreaId),
723 #[error("invalid image id {0}")]
724 InvalidImageId(ImageId),
725 #[error("Block {0} parent {1} is not a block in the document")]
726 InvalidBlockParent(BlockId, BlockId),
727 #[error("Pin {0} owner {1} is not a block in the document")]
728 InvalidPinOwner(PinId, BlockId),
729 #[error("Route {0} owner {1} is not a block in the document")]
730 InvalidRouteOwner(RouteId, BlockId),
731 #[error("Route {0} from pin {1} is not a pin in the document")]
732 InvalidRouteFrom(RouteId, PinId),
733 #[error("Route {0} to pin {1} is not a pin in the document")]
734 InvalidRouteTo(RouteId, PinId),
735 #[error("RouteLabel {0} owner {1} is not a route in the document")]
736 InvalidRouteLabelOwner(RouteLabelId, RouteId),
737 #[error("Text {0} owner {1} is not a block in the document")]
738 InvalidTextOwner(TextId, BlockId),
739 #[error("Area {0} owner {1} is not a block in the document")]
740 InvalidAreaOwner(AreaId, BlockId),
741 #[error("top {0} is not a block in the document")]
742 InvalidTop(BlockId),
743 #[error("Image {0} owner {1} is not a block in the document")]
744 InvalidImageOwner(ImageId, BlockId),
745 #[error("Block {0} is in a cycle of parent links")]
746 BlockCycle(BlockId),
747 #[error("the payload filed under asset {0} does not hash to it")]
748 AssetHashMismatch(AssetHash),
749 #[error("asset {0} is {1} bytes, over the {limit}-byte limit", limit = crate::block_model::ASSET_LIMIT)]
750 AssetTooLarge(AssetHash, usize),
751}
752
753fn apply(doc: &mut Document, commit: &Commit) -> Result<(), FoldError> {
754 for op in commit.ops() {
755 doc.ids.observe(op.target());
756 match op {
757 OpCodes::Document(update) => {
758 doc.content.title_block.apply(update);
759 }
760 OpCodes::Block(id, crud) => {
761 apply_crud_to_entity(&mut doc.content.blocks, *id, crud)
762 .ok_or(FoldError::InvalidBlockId(*id))?;
763 }
764 OpCodes::Pin(id, crud) => {
765 apply_crud_to_entity(&mut doc.content.pins, *id, crud)
766 .ok_or(FoldError::InvalidPinId(*id))?;
767 }
768 OpCodes::Route(id, crud) => {
769 apply_crud_to_entity(&mut doc.content.routes, *id, crud)
770 .ok_or(FoldError::InvalidRouteId(*id))?;
771 }
772 OpCodes::RouteLabel(id, crud) => {
773 apply_crud_to_entity(&mut doc.content.route_labels, *id, crud)
774 .ok_or(FoldError::InvalidRouteLabelId(*id))?;
775 }
776 OpCodes::Text(id, crud) => {
777 apply_crud_to_entity(&mut doc.content.texts, *id, crud)
778 .ok_or(FoldError::InvalidTextId(*id))?;
779 }
780 OpCodes::Area(id, crud) => {
781 apply_crud_to_entity(&mut doc.content.areas, *id, crud)
782 .ok_or(FoldError::InvalidAreaId(*id))?;
783 }
784 OpCodes::Image(id, crud) => {
785 apply_crud_to_entity(&mut doc.content.images, *id, crud)
786 .ok_or(FoldError::InvalidImageId(*id))?;
787 }
788 OpCodes::Asset(hash, asset) => {
789 doc.content
790 .assets
791 .entry(*hash)
792 .or_insert_with(|| asset.clone());
793 }
794 }
795 }
796 Ok(())
797}
798
799fn validate(doc: &Document, commit: &Commit) -> Result<(), FoldError> {
803 dangling_endpoints(doc, commit)?;
804 for op in commit.ops() {
805 match op {
806 OpCodes::Block(id, _) => {
807 doc.validate_block_owner(*id)?;
808 }
809 OpCodes::Pin(id, _) => {
810 doc.validate_pin_owner(*id)?;
811 }
812 OpCodes::Route(id, _) => {
813 doc.validate_route_owner_and_endpoints(*id)?;
814 }
815 OpCodes::RouteLabel(id, _) => {
816 doc.validate_route_label_owner(*id)?;
817 }
818 OpCodes::Text(id, _) => {
819 doc.validate_text_owner(*id)?;
820 }
821 OpCodes::Area(id, _) => {
822 doc.validate_area_owner(*id)?;
823 }
824 OpCodes::Image(id, _) => {
825 doc.validate_image_owner(*id)?;
826 }
827 OpCodes::Asset(hash, asset) => {
828 if asset.hash() != *hash {
833 return Err(FoldError::AssetHashMismatch(*hash));
834 }
835 if !asset.within_limit() {
839 return Err(FoldError::AssetTooLarge(*hash, asset.bytes().len()));
840 }
841 }
842 OpCodes::Document(_) => {
843 let top = doc.title_block().top;
848 if top != BlockId::NULL && !doc.content.blocks.contains_key(&top) {
849 return Err(FoldError::InvalidTop(top));
850 }
851 }
852 }
853 }
854 Ok(())
855}
856
857fn dangling_endpoints(doc: &Document, commit: &Commit) -> Result<(), FoldError> {
864 let removed: HashSet<PinId> = commit
865 .ops()
866 .iter()
867 .filter_map(|op| match op {
868 OpCodes::Pin(id, Crud::Delete) => Some(*id),
869 _ => None,
870 })
871 .collect();
872 if removed.is_empty() {
873 return Ok(());
874 }
875 let mut dangling: Vec<(RouteId, &Route)> = doc
876 .routes()
877 .filter(|(_, route)| removed.contains(&route.from) || removed.contains(&route.to))
878 .collect();
879 dangling.sort_by_key(|(id, _)| *id);
880 match dangling.first() {
881 None => Ok(()),
882 Some((id, route)) if removed.contains(&route.from) => {
883 Err(FoldError::InvalidRouteFrom(*id, route.from))
884 }
885 Some((id, route)) => Err(FoldError::InvalidRouteTo(*id, route.to)),
886 }
887}
888
889fn apply_crud_to_entity<E: Entity + Clone>(
894 map: &mut BTreeMap<E::Id, Arc<E>>,
895 id: E::Id,
896 crud: &Crud<E, E::Update>,
897) -> Option<()> {
898 match crud {
899 Crud::Create(entity) => {
900 if map.contains_key(&id) {
901 return None;
902 }
903 map.insert(id, Arc::new(entity.clone()));
904 Some(())
905 }
906 Crud::Update(update) => {
907 Arc::make_mut(map.get_mut(&id)?).apply(update);
908 Some(())
909 }
910 Crud::Delete => map.remove(&id).map(|_| ()),
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use crate::fixtures::{area_id, block_id, image_id, pin_id, route_id, route_label_id, text_id};
918 use crate::{
919 block_model::{
920 Area, Block, BlockUpdate, Icon, Image, Label, Pin, PinUpdate, Route, RouteLabel, Text,
921 },
922 geometry::{FracVal, GridPoint, GridRect, GridSize, PinSlot, ScreenRect},
923 values::{LabelSide, PinDir, PinSide, Role},
924 };
925
926 fn rename(name: &str) -> Commit {
927 Commit::new(
928 "Renamed the document".into(),
929 vec![OpCodes::Document(TitleBlockUpdate::Name(name.into()))],
930 )
931 }
932
933 fn label_init(name: String) -> Label {
934 Label {
935 name,
936 side: LabelSide::default(),
937 offset: FracVal::default(),
938 hidden: false,
939 }
940 }
941
942 fn block_create(n: u32) -> OpCodes {
943 OpCodes::Block(
944 block_id(n),
945 Crud::Create(Block {
946 parent: BlockId::NULL,
947 rect: GridRect::default(),
948 locked: false,
949 role: Role::default(),
950 title: label_init(format!("b{n}")),
951 type_label: label_init(String::new()),
952 icon: Icon::default(),
953 }),
954 )
955 }
956
957 fn route_create(n: u32, owner: u32, from: u32, to: u32) -> OpCodes {
958 OpCodes::Route(
959 route_id(n),
960 Crud::Create(Route {
961 owner: block_id(owner),
962 name: format!("r{n}"),
963 from: pin_id(from),
964 to: pin_id(to),
965 role: Role::default(),
966 waypoints: Vec::new(),
967 }),
968 )
969 }
970
971 fn wired_document() -> Document {
975 Document::default()
976 .try_apply(&Commit::new(
977 "Wired a document".into(),
978 vec![
979 block_create(1),
980 block_create(2),
981 reparent(2, 1),
982 pin_create(3, block_id(1)),
983 pin_create(4, block_id(1)),
984 route_create(5, 1, 3, 4),
985 OpCodes::RouteLabel(
986 route_label_id(6),
987 Crud::Create(RouteLabel {
988 owner: route_id(5),
989 pos: FracVal::default(),
990 }),
991 ),
992 OpCodes::Text(
993 text_id(7),
994 Crud::Create(Text {
995 owner: block_id(1),
996 text: "note".into(),
997 pos: GridPoint::default(),
998 role: Role::default(),
999 width: None,
1000 }),
1001 ),
1002 OpCodes::Area(
1003 area_id(8),
1004 Crud::Create(Area {
1005 owner: block_id(1),
1006 rect: GridRect::default(),
1007 role: Role::default(),
1008 title: label_init("c8".into()),
1009 }),
1010 ),
1011 OpCodes::Image(
1012 image_id(9),
1013 Crud::Create(Image {
1014 owner: block_id(1),
1015 asset: AssetHash::default(),
1016 rect: ScreenRect::default(),
1017 }),
1018 ),
1019 ],
1020 ))
1021 .expect("the fold succeeds")
1022 }
1023
1024 fn reparent(child: u32, parent: u32) -> OpCodes {
1025 OpCodes::Block(
1026 block_id(child),
1027 Crud::Update(BlockUpdate::Parent(block_id(parent))),
1028 )
1029 }
1030
1031 fn rect(x: i32, y: i32) -> GridRect {
1032 GridRect {
1033 top_left: GridPoint { x, y },
1034 size: GridSize { w: 4, h: 4 },
1035 }
1036 }
1037
1038 fn resize(block: u32, to: GridRect) -> OpCodes {
1039 OpCodes::Block(block_id(block), Crud::Update(BlockUpdate::Rect(to)))
1040 }
1041
1042 fn rename_pin(n: u32, name: &str) -> OpCodes {
1043 OpCodes::Pin(pin_id(n), Crud::Update(PinUpdate::Name(name.into())))
1044 }
1045
1046 fn pin_delete(n: u32) -> OpCodes {
1047 OpCodes::Pin(pin_id(n), Crud::Delete)
1048 }
1049
1050 fn block_with_pin() -> Document {
1051 Document::default()
1052 .try_apply(&Commit::new(
1053 "Added a block with a pin".into(),
1054 vec![block_create(1), pin_create(2, block_id(1))],
1055 ))
1056 .expect("the fold succeeds")
1057 }
1058
1059 fn pin_create(n: u32, owner: BlockId) -> OpCodes {
1060 OpCodes::Pin(
1061 pin_id(n),
1062 Crud::Create(Pin {
1063 owner,
1064 name: format!("p{n}"),
1065 type_name: String::new(),
1066 tag: String::new(),
1067 tag_hidden: false,
1068 rect: GridRect::default(),
1069 slot: PinSlot::default(),
1070 dir: PinDir::default(),
1071 port_accent: Role::default(),
1072 flip_lr: false,
1073 }),
1074 )
1075 }
1076
1077 #[test]
1078 fn a_name_update_folds_into_a_new_document_and_the_source_stays_frozen() {
1079 let doc = Document::default();
1080
1081 let next = doc
1082 .try_apply(&rename("drawing"))
1083 .expect("the fold succeeds");
1084 assert_eq!(next.rev(), Rev::new(1), "the fold mints the successor rev");
1085 assert_eq!(next.title_block().name.as_str(), "drawing");
1086 assert_eq!(doc.rev(), Rev::ZERO, "the source document is frozen");
1087 assert_eq!(
1088 doc.title_block().name.as_str(),
1089 "",
1090 "the source document is frozen"
1091 );
1092 }
1093
1094 #[test]
1097 fn a_block_role_folds_and_the_later_write_wins() {
1098 let doc = Document::default()
1099 .try_apply(&Commit::new("Added b1".into(), vec![block_create(1)]))
1100 .expect("the create folds");
1101
1102 let recolor = |role| {
1103 Commit::new(
1104 "Recolored".into(),
1105 vec![OpCodes::Block(
1106 block_id(1),
1107 Crud::Update(BlockUpdate::Role(role)),
1108 )],
1109 )
1110 };
1111 let next = doc
1112 .try_apply(&recolor(Role::Accent3))
1113 .expect("the first recolor folds")
1114 .try_apply(&recolor(Role::Accent5))
1115 .expect("the second recolor folds");
1116
1117 let block = next.block(&block_id(1)).expect("the block is held");
1118 assert_eq!(block.role, Role::Accent5);
1119 }
1120
1121 #[test]
1125 fn wrap_top_folds_and_the_later_top_wins() {
1126 let set_top = |n| OpCodes::Document(TitleBlockUpdate::Top(block_id(n)));
1127 let doc = Document::default()
1128 .try_apply(&Commit::new(
1129 "Added b1".into(),
1130 vec![block_create(1), set_top(1)],
1131 ))
1132 .expect("the create folds")
1133 .try_apply(&Commit::new(
1134 "Wrapped the top".into(),
1135 vec![block_create(2), reparent(1, 2), set_top(2)],
1136 ))
1137 .expect("the wrap folds");
1138
1139 assert_eq!(doc.title_block().top, block_id(2));
1140 assert_eq!(
1141 doc.block(&block_id(1)).expect("the old top is held").parent,
1142 block_id(2),
1143 );
1144 }
1145
1146 #[test]
1150 fn a_top_naming_an_unknown_block_is_refused() {
1151 let doc = Document::default();
1152 let unknown = block_id(7);
1153
1154 let refused = doc.try_apply(&Commit::new(
1155 "Pointed top nowhere".into(),
1156 vec![OpCodes::Document(TitleBlockUpdate::Top(unknown))],
1157 ));
1158 assert_eq!(refused.err(), Some(FoldError::InvalidTop(unknown)));
1159
1160 doc.try_apply(&Commit::new(
1161 "Cleared the top".into(),
1162 vec![OpCodes::Document(TitleBlockUpdate::Top(BlockId::NULL))],
1163 ))
1164 .expect("NULL is the meaningful zero, not a reference");
1165 }
1166
1167 #[test]
1171 fn concurrent_slot_writes_race_as_whole_pairs() {
1172 let doc = block_with_pin();
1173
1174 let move_to = |slot| {
1175 Commit::new(
1176 "Moved a pin".into(),
1177 vec![OpCodes::Pin(pin_id(2), Crud::Update(PinUpdate::Slot(slot)))],
1178 )
1179 };
1180 let east_3 = PinSlot {
1181 side: PinSide::East,
1182 offset: 3,
1183 };
1184 let west_9 = PinSlot {
1185 side: PinSide::West,
1186 offset: 9,
1187 };
1188 let next = doc
1189 .try_apply(&move_to(east_3))
1190 .expect("the first move folds")
1191 .try_apply(&move_to(west_9))
1192 .expect("the second move folds");
1193
1194 let pin = next.pin(&pin_id(2)).expect("the pin is held");
1195 assert_eq!(pin.slot, west_9, "the later pair wins whole");
1196 }
1197
1198 #[test]
1202 fn a_bad_op_refuses_the_whole_commit() {
1203 let doc = Document::default();
1204 let unknown = block_id(7);
1205 let commit = Commit::new(
1206 "Deleted a block".into(),
1207 vec![
1208 OpCodes::Document(TitleBlockUpdate::Name("drawing".into())),
1209 OpCodes::Block(unknown, Crud::Delete),
1210 ],
1211 );
1212
1213 let Err(error) = doc.try_apply(&commit) else {
1214 panic!("a delete of an unknown id must refuse the commit");
1215 };
1216 assert_eq!(error, FoldError::InvalidBlockId(unknown));
1217 }
1218
1219 #[test]
1222 fn a_refused_commit_mints_no_rev() {
1223 let doc = Document::default()
1224 .try_apply(&rename("drawing"))
1225 .expect("the fold succeeds");
1226 assert_eq!(doc.rev(), Rev::new(1));
1227
1228 let unknown = block_id(7);
1229 let bad = Commit::new(
1230 "Deleted a block".into(),
1231 vec![OpCodes::Block(unknown, Crud::Delete)],
1232 );
1233 assert!(doc.try_apply(&bad).is_err());
1234
1235 let next = doc
1236 .try_apply(&rename("schematic"))
1237 .expect("the fold succeeds");
1238 assert_eq!(next.rev(), Rev::new(2));
1239 }
1240
1241 #[test]
1246 fn the_tables_iterate_in_id_order_whatever_order_they_were_written_in() {
1247 let written: Vec<u32> = vec![12, 3, 7, 1, 9];
1248 let doc = Document::default()
1249 .try_apply(&Commit::new(
1250 "Created five blocks".into(),
1251 written.iter().copied().map(block_create).collect(),
1252 ))
1253 .expect("the fold succeeds");
1254 assert_ne!(
1255 written,
1256 {
1257 let mut sorted = written.clone();
1258 sorted.sort_unstable();
1259 sorted
1260 },
1261 "the fixture must write out of order or the ordering is untested"
1262 );
1263 assert_eq!(
1264 doc.blocks().map(|(id, _)| id).collect::<Vec<_>>(),
1265 [1, 3, 7, 9, 12].map(block_id),
1266 );
1267 }
1268
1269 #[test]
1274 fn stripping_and_re_attaching_payloads_is_a_round_trip() {
1275 let held = png(b"artwork");
1276 let orphan = png(b"nobody points at this");
1277 let document = Document::default()
1278 .try_apply(&Commit::new(
1279 "Placed it".into(),
1280 vec![payload(&held), payload(&orphan), image_create(1, 0, &held)],
1281 ))
1282 .expect("the fold succeeds");
1283 assert_eq!(
1284 document.assets().count(),
1285 2,
1286 "precondition: the table holds a payload nothing references",
1287 );
1288 assert_eq!(
1289 document.referenced_assets(),
1290 std::collections::BTreeSet::from([held.hash()]),
1291 );
1292
1293 let stripped = document.without_assets();
1294 assert_eq!(stripped.assets().count(), 0, "the rev file carries bytes");
1295 assert_ne!(stripped, document);
1296
1297 let mut attached = stripped;
1298 let store = [(held.hash(), held.clone()), (orphan.hash(), orphan)];
1299 attached
1300 .attach_assets(|hash| {
1301 store
1302 .iter()
1303 .find(|(named, _)| *named == hash)
1304 .map(|(_, asset)| asset.clone())
1305 .ok_or(hash)
1306 })
1307 .expect("the store holds what the document references");
1308 assert_eq!(attached.asset(&held.hash()), Some(&held));
1309 assert_eq!(
1310 attached.assets().count(),
1311 1,
1312 "the unreferenced payload is not read back: nothing names it",
1313 );
1314 }
1315
1316 #[test]
1319 fn attaching_reports_the_payload_the_store_will_not_hand_back() {
1320 let held = png(b"artwork");
1321 let mut document = Document::default()
1322 .try_apply(&Commit::new(
1323 "Placed it".into(),
1324 vec![payload(&held), image_create(1, 0, &held)],
1325 ))
1326 .expect("the fold succeeds")
1327 .without_assets();
1328 assert_eq!(document.attach_assets(Err::<Asset, _>), Err(held.hash()),);
1329 }
1330
1331 #[test]
1335 fn a_pin_needs_its_owner_block() {
1336 let owner = block_id(1);
1337 let good = Commit::new(
1338 "Added a block with a pin".into(),
1339 vec![pin_create(2, owner), block_create(1)],
1340 );
1341 let doc = Document::default()
1342 .try_apply(&good)
1343 .expect("an owner created anywhere in the commit resolves");
1344 assert!(doc.pin(&pin_id(2)).is_some());
1345
1346 let stranger = block_id(9);
1347 let bad = Commit::new("Added an orphan pin".into(), vec![pin_create(3, stranger)]);
1348 let Err(error) = doc.try_apply(&bad) else {
1349 panic!("a pin with an unknown owner must refuse the commit");
1350 };
1351 assert_eq!(error, FoldError::InvalidPinOwner(pin_id(3), stranger));
1352 assert!(
1353 doc.pin(&pin_id(3)).is_none(),
1354 "the refused create must leave no trace"
1355 );
1356 }
1357
1358 #[test]
1362 fn an_owner_update_to_an_unknown_block_is_refused() {
1363 let owner = block_id(1);
1364 let doc = Document::default()
1365 .try_apply(&Commit::new(
1366 "Added a block with a pin".into(),
1367 vec![block_create(1), pin_create(2, owner)],
1368 ))
1369 .expect("the fold succeeds");
1370
1371 let stranger = block_id(9);
1372 let reown = Commit::new(
1373 "Re-owned the pin".into(),
1374 vec![OpCodes::Pin(
1375 pin_id(2),
1376 Crud::Update(PinUpdate::Owner(stranger)),
1377 )],
1378 );
1379 let Err(error) = doc.try_apply(&reown) else {
1380 panic!("re-owning to an unknown block must refuse the commit");
1381 };
1382 assert_eq!(error, FoldError::InvalidPinOwner(pin_id(2), stranger));
1383 assert_eq!(
1384 doc.pin(&pin_id(2)).map(|pin| pin.owner),
1385 Some(owner),
1386 "the source document still holds the valid owner"
1387 );
1388 }
1389
1390 #[test]
1393 fn a_parent_cycle_is_refused() {
1394 let doc = Document::default()
1395 .try_apply(&Commit::new(
1396 "Added two blocks".into(),
1397 vec![block_create(1), block_create(2)],
1398 ))
1399 .expect("the fold succeeds");
1400
1401 let nested = doc
1402 .try_apply(&Commit::new(
1403 "Nested 1 under 2".into(),
1404 vec![reparent(1, 2)],
1405 ))
1406 .expect("a loop-free reparent lands");
1407 assert_eq!(
1408 nested.block(&block_id(1)).map(|block| block.parent),
1409 Some(block_id(2)),
1410 "the fixture must nest or the loop below tests nothing"
1411 );
1412
1413 let Err(error) =
1414 nested.try_apply(&Commit::new("Closed the loop".into(), vec![reparent(2, 1)]))
1415 else {
1416 panic!("closing a parent loop must refuse the commit");
1417 };
1418 assert_eq!(error, FoldError::BlockCycle(block_id(2)));
1419
1420 let Err(error) = doc.try_apply(&Commit::new(
1421 "Swapped parents".into(),
1422 vec![reparent(1, 2), reparent(2, 1)],
1423 )) else {
1424 panic!("a loop closed within one commit must refuse it");
1425 };
1426 assert_eq!(error, FoldError::BlockCycle(block_id(1)));
1427
1428 let Err(error) = doc.try_apply(&Commit::new("Self parent".into(), vec![reparent(1, 1)]))
1429 else {
1430 panic!("a self-parent must refuse the commit");
1431 };
1432 assert_eq!(error, FoldError::BlockCycle(block_id(1)));
1433 }
1434
1435 #[test]
1439 fn a_commit_writing_one_register_twice_ends_on_its_last_write() {
1440 let commit = Commit::new(
1441 "Created and nudged a block".into(),
1442 vec![
1443 block_create(1),
1444 resize(1, rect(1, 1)),
1445 resize(1, rect(2, 2)),
1446 ],
1447 );
1448 let doc = Document::default()
1449 .try_apply(&commit)
1450 .expect("the fold succeeds");
1451
1452 let block = doc.block(&block_id(1)).expect("created");
1453 assert_eq!(block.rect, rect(2, 2));
1454 }
1455
1456 #[test]
1461 fn a_later_commits_write_displaces_an_earlier_ones() {
1462 let doc = Document::default()
1463 .try_apply(&Commit::new("Added a block".into(), vec![block_create(1)]))
1464 .expect("the fold succeeds")
1465 .try_apply(&Commit::new("Moved it".into(), vec![resize(1, rect(1, 1))]))
1466 .expect("the fold succeeds")
1467 .try_apply(&Commit::new(
1468 "Moved it again".into(),
1469 vec![resize(1, rect(2, 2))],
1470 ))
1471 .expect("the fold succeeds");
1472
1473 let block = doc.block(&block_id(1)).expect("created");
1474 assert_eq!(block.rect, rect(2, 2));
1475 assert!(!block.locked, "untouched fields keep their created value");
1476 }
1477
1478 #[test]
1482 fn deleting_removes_an_entity_and_a_second_delete_refuses() {
1483 let deleted = block_with_pin()
1484 .try_apply(&Commit::new("Deleted the pin".into(), vec![pin_delete(2)]))
1485 .expect("the fold succeeds");
1486 assert!(deleted.pin(&pin_id(2)).is_none());
1487
1488 let Err(error) = deleted.try_apply(&Commit::new(
1489 "Deleted the pin again".into(),
1490 vec![pin_delete(2)],
1491 )) else {
1492 panic!("a delete at an absent target must refuse");
1493 };
1494 assert_eq!(error, FoldError::InvalidPinId(pin_id(2)));
1495 }
1496
1497 #[test]
1502 fn an_edit_after_a_delete_refuses_the_commit() {
1503 let deleted = block_with_pin()
1504 .try_apply(&Commit::new("Deleted the pin".into(), vec![pin_delete(2)]))
1505 .expect("the fold succeeds");
1506 let Err(error) = deleted.try_apply(&Commit::new(
1507 "Renamed the pin".into(),
1508 vec![rename_pin(2, "clk")],
1509 )) else {
1510 panic!("an update at an absent target must refuse");
1511 };
1512 assert_eq!(error, FoldError::InvalidPinId(pin_id(2)));
1513 }
1514
1515 #[test]
1519 fn a_deleted_id_is_never_reissued_but_can_be_re_created() {
1520 let mut deleted = block_with_pin()
1521 .try_apply(&Commit::new("Deleted the pin".into(), vec![pin_delete(2)]))
1522 .expect("the fold succeeds");
1523 assert_ne!(deleted.mint::<crate::id::PinKind>(), pin_id(2));
1524
1525 let back = deleted
1526 .try_apply(&Commit::new(
1527 "Put the pin back".into(),
1528 vec![pin_create(2, block_id(1))],
1529 ))
1530 .expect("a create under a departed id folds");
1531 assert!(back.pin(&pin_id(2)).is_some());
1532 }
1533
1534 #[test]
1538 fn replay_is_a_function_of_the_log_alone() {
1539 let log = [
1540 Commit::new(
1541 "Added a block with a pin".into(),
1542 vec![block_create(1), pin_create(2, block_id(1))],
1543 ),
1544 Commit::new("Renamed the pin".into(), vec![rename_pin(2, "clk")]),
1545 Commit::new("Deleted the pin".into(), vec![pin_delete(2)]),
1546 ];
1547 let replay = || {
1548 log.iter()
1549 .try_fold(Document::default(), |doc, commit| doc.try_apply(commit))
1550 };
1551
1552 let one = replay().expect("the fold succeeds");
1553 let other = replay().expect("the fold succeeds");
1554 assert_eq!(
1555 one.rev(),
1556 Rev::new(3),
1557 "the fixture must fold the whole log"
1558 );
1559 assert_eq!(one, other);
1560 }
1561
1562 #[test]
1563 fn the_index_covers_every_kind_and_the_containment_tree() {
1564 let doc = wired_document();
1565 let index = DocIndex::of(&doc);
1566
1567 assert_eq!(
1568 index.blocks[&BlockId::NULL].children,
1569 [block_id(1)].into_iter().collect()
1570 );
1571 let entry = &index.blocks[&block_id(1)];
1572 assert_eq!(entry.children, [block_id(2)].into_iter().collect());
1573 assert_eq!(entry.pins, [pin_id(3), pin_id(4)].into_iter().collect());
1574 assert_eq!(entry.routes, [route_id(5)].into_iter().collect());
1575 assert_eq!(entry.texts, [text_id(7)].into_iter().collect());
1576 assert_eq!(entry.areas, [area_id(8)].into_iter().collect());
1577 assert_eq!(entry.images, [image_id(9)].into_iter().collect());
1578 assert_eq!(
1579 index.blocks[&block_id(2)],
1580 BlockIndex::default(),
1581 "the nested block owns nothing"
1582 );
1583 assert_eq!(
1584 index.routes[&route_id(5)].labels,
1585 [route_label_id(6)].into_iter().collect()
1586 );
1587 }
1588
1589 #[test]
1592 fn a_born_document_value_mints_a_stamp_and_a_clone_carries_it() {
1593 let empty = Document::default();
1594 assert_ne!(
1595 empty.stamp,
1596 Document::default().stamp,
1597 "two empty documents are two values"
1598 );
1599 assert_eq!(empty.stamp, empty.clone().stamp);
1600
1601 let folded = empty
1602 .try_apply(&rename("drawing"))
1603 .expect("the fold succeeds");
1604 assert_ne!(empty.stamp, folded.stamp, "a fold mints");
1605 }
1606
1607 #[test]
1611 fn view_rebuilds_for_a_new_document_value_and_not_otherwise() {
1612 let doc = wired_document();
1613 let mut index = DocIndex::of(&doc);
1614 let sentinel = block_id(200);
1615 let root = |index: &DocIndex| index.blocks[&BlockId::NULL].children.clone();
1616 assert!(
1617 !root(&index).contains(&sentinel),
1618 "the sentinel must not be a real entry or the probe proves nothing"
1619 );
1620 let plant = |index: &mut DocIndex| {
1621 index
1622 .blocks
1623 .entry(BlockId::NULL)
1624 .or_default()
1625 .children
1626 .insert(sentinel);
1627 };
1628 plant(&mut index);
1629
1630 assert!(
1631 root(index.view(&doc).index).contains(&sentinel),
1632 "one document value is indexed once"
1633 );
1634 assert!(
1635 root(index.view(&doc.clone()).index).contains(&sentinel),
1636 "a clone is the same value"
1637 );
1638
1639 plant(&mut index);
1640 let folded = doc
1641 .try_apply(&Commit::new("Added b20".into(), vec![block_create(20)]))
1642 .expect("the fold succeeds");
1643 let view = index.view(&folded);
1644 assert!(
1645 !root(view.index).contains(&sentinel),
1646 "a folded document is a new value"
1647 );
1648 assert!(root(view.index).contains(&block_id(20)));
1649 }
1650
1651 #[test]
1656 fn the_root_is_a_scope_row_but_not_a_block() {
1657 let doc = Document::default()
1658 .try_apply(&Commit::new(
1659 "Authored at the root".into(),
1660 vec![
1661 block_create(1),
1662 pin_create(3, BlockId::NULL),
1663 pin_create(4, BlockId::NULL),
1664 OpCodes::Route(
1665 route_id(5),
1666 Crud::Create(Route {
1667 owner: BlockId::NULL,
1668 name: "r5".into(),
1669 from: pin_id(3),
1670 to: pin_id(4),
1671 role: Role::default(),
1672 waypoints: Vec::new(),
1673 }),
1674 ),
1675 OpCodes::Text(
1676 text_id(7),
1677 Crud::Create(Text {
1678 owner: BlockId::NULL,
1679 text: "note".into(),
1680 pos: GridPoint::default(),
1681 role: Role::default(),
1682 width: None,
1683 }),
1684 ),
1685 OpCodes::Area(
1686 area_id(8),
1687 Crud::Create(Area {
1688 owner: BlockId::NULL,
1689 rect: GridRect::default(),
1690 role: Role::default(),
1691 title: label_init("c8".into()),
1692 }),
1693 ),
1694 OpCodes::Image(
1695 image_id(9),
1696 Crud::Create(Image {
1697 owner: BlockId::NULL,
1698 asset: AssetHash::default(),
1699 rect: ScreenRect::default(),
1700 }),
1701 ),
1702 ],
1703 ))
1704 .expect("the root accepts scoped entities");
1705 let index = DocIndex::of(&doc);
1706
1707 let root = index.scope(BlockId::NULL).expect("the root is a scope");
1708 assert_eq!(root.children, [block_id(1)].into_iter().collect());
1709 assert_eq!(root.pins, [pin_id(3), pin_id(4)].into_iter().collect());
1710 assert_eq!(root.routes, [route_id(5)].into_iter().collect());
1711 assert_eq!(root.texts, [text_id(7)].into_iter().collect());
1712 assert_eq!(root.areas, [area_id(8)].into_iter().collect());
1713 assert_eq!(root.images, [image_id(9)].into_iter().collect());
1714
1715 assert!(index.holds_block(block_id(1)));
1716 assert!(
1717 !index.holds_block(BlockId::NULL),
1718 "the root has no entity to hold"
1719 );
1720 assert_eq!(
1721 index.scope(block_id(1)),
1722 Some(&BlockIndex::default()),
1723 "the block owns nothing — the root does"
1724 );
1725 }
1726
1727 #[test]
1731 fn the_fold_accepts_the_root_as_an_owner_and_refuses_a_stranger() {
1732 let stranger = block_id(9);
1733 let doc = Document::default();
1734
1735 doc.try_apply(&Commit::new(
1736 "Pinned the root".into(),
1737 vec![pin_create(3, BlockId::NULL)],
1738 ))
1739 .expect("the root owns pins");
1740
1741 let Err(error) = doc.try_apply(&Commit::new(
1742 "Pinned a stranger".into(),
1743 vec![pin_create(3, stranger)],
1744 )) else {
1745 panic!("an owner that is neither a block nor the root must refuse");
1746 };
1747 assert_eq!(error, FoldError::InvalidPinOwner(pin_id(3), stranger));
1748 }
1749
1750 #[test]
1753 fn a_deleted_child_leaves_its_parents_sets() {
1754 let doc = wired_document()
1755 .try_apply(&Commit::new(
1756 "Deleted a pin and the nested block".into(),
1757 vec![
1758 OpCodes::Route(route_id(5), Crud::Delete),
1759 OpCodes::RouteLabel(route_label_id(6), Crud::Delete),
1760 pin_delete(4),
1761 OpCodes::Block(block_id(2), Crud::Delete),
1762 ],
1763 ))
1764 .expect("the fold succeeds");
1765 let index = DocIndex::of(&doc);
1766
1767 let entry = &index.blocks[&block_id(1)];
1768 assert_eq!(
1769 entry.pins,
1770 [pin_id(3)].into_iter().collect(),
1771 "a deleted pin leaves the owner's set"
1772 );
1773 assert!(
1774 entry.children.is_empty(),
1775 "a deleted child leaves the parent's list"
1776 );
1777 assert!(
1778 !index.blocks.contains_key(&block_id(2)),
1779 "no entries for deleted elements"
1780 );
1781 }
1782
1783 #[test]
1790 fn deleting_an_endpoint_without_its_route_refuses_the_commit() {
1791 let doc = wired_document();
1792 assert!(
1793 doc.route(&route_id(5)).is_some_and(|r| r.from == pin_id(3)),
1794 "precondition: the fixture's route hangs off the pin being deleted"
1795 );
1796
1797 let Err(error) = doc.try_apply(&Commit::new(
1798 "Deleted an endpoint".into(),
1799 vec![pin_delete(3)],
1800 )) else {
1801 panic!("a dangling endpoint must refuse the commit");
1802 };
1803 assert_eq!(error, FoldError::InvalidRouteFrom(route_id(5), pin_id(3)));
1804
1805 doc.try_apply(&Commit::new(
1806 "Deleted an endpoint and its route".into(),
1807 vec![
1808 OpCodes::RouteLabel(route_label_id(6), Crud::Delete),
1809 OpCodes::Route(route_id(5), Crud::Delete),
1810 pin_delete(3),
1811 ],
1812 ))
1813 .expect("the cascade folds");
1814 }
1815
1816 #[test]
1820 fn routes_by_endpoint_agrees_with_a_brute_force_scan() {
1821 let doc = wired_document()
1822 .try_apply(&Commit::new(
1823 "Added a second route and killed the first".into(),
1824 vec![
1825 route_create(10, 1, 3, 4),
1826 OpCodes::RouteLabel(route_label_id(6), Crud::Delete),
1827 OpCodes::Route(route_id(5), Crud::Delete),
1828 ],
1829 ))
1830 .expect("the fold succeeds");
1831 let index = DocIndex::of(&doc);
1832
1833 let mut oracle: HashMap<PinId, HashSet<RouteId>> = HashMap::default();
1834 for (id, route) in doc.routes() {
1835 for endpoint in [route.from, route.to] {
1836 oracle.entry(endpoint).or_default().insert(id);
1837 }
1838 }
1839 assert_eq!(index.routes_by_endpoint, oracle);
1840
1841 assert!(
1842 index.routes_by_endpoint[&pin_id(3)].contains(&route_id(10)),
1843 "a route is indexed under both its endpoints"
1844 );
1845 assert!(
1846 !index
1847 .routes_by_endpoint
1848 .values()
1849 .any(|routes| routes.contains(&route_id(5))),
1850 "a deleted route leaves the index"
1851 );
1852 }
1853
1854 #[test]
1859 fn chronological_orders_by_id_and_a_touch_does_not_move_it() {
1860 let mut doc = Document::default();
1861 for n in [1, 2, 3] {
1862 doc = doc
1863 .try_apply(&Commit::new(format!("Added b{n}"), vec![block_create(n)]))
1864 .expect("the create folds");
1865 }
1866 doc = doc
1867 .try_apply(&Commit::new("Moved b1".into(), vec![resize(1, rect(9, 9))]))
1868 .expect("the move folds");
1869 assert_eq!(
1870 chronological(doc.blocks()),
1871 vec![block_id(1), block_id(2), block_id(3)],
1872 "the touched block stays where its id puts it"
1873 );
1874 }
1875
1876 fn png(bytes: &[u8]) -> Asset {
1877 Asset::Png(bytes.into())
1878 }
1879
1880 fn payload(asset: &Asset) -> OpCodes {
1881 OpCodes::Asset(asset.hash(), asset.clone())
1882 }
1883
1884 fn image_create(n: u32, owner: u32, asset: &Asset) -> OpCodes {
1885 OpCodes::Image(
1886 image_id(n),
1887 Crud::Create(Image {
1888 owner: block_id(owner),
1889 asset: asset.hash(),
1890 rect: ScreenRect::default(),
1891 }),
1892 )
1893 }
1894
1895 #[test]
1898 fn an_asset_payload_folds_into_the_table_under_its_hash() {
1899 let asset = png(b"artwork");
1900 let doc = Document::default()
1901 .try_apply(&Commit::new(
1902 "Placed an image".into(),
1903 vec![payload(&asset), block_create(1), image_create(9, 1, &asset)],
1904 ))
1905 .expect("the fold succeeds");
1906
1907 assert_eq!(doc.asset(&asset.hash()), Some(&asset));
1908 assert_eq!(
1909 doc.asset(&png(b"other artwork").hash()),
1910 None,
1911 "an unheld hash reads as absent, not as some other payload"
1912 );
1913 assert_eq!(
1914 doc.image(&image_id(9)).expect("the image is held").asset,
1915 asset.hash(),
1916 "the entity carries the reference, the table carries the bytes"
1917 );
1918 }
1919
1920 #[test]
1924 fn a_duplicate_payload_is_a_no_op_not_a_refusal() {
1925 let asset = png(b"artwork");
1926 let once = Document::default()
1927 .try_apply(&Commit::new("Placed it".into(), vec![payload(&asset)]))
1928 .expect("the fold succeeds");
1929 let twice = once
1930 .try_apply(&Commit::new(
1931 "Placed it again".into(),
1932 vec![payload(&asset)],
1933 ))
1934 .expect("a duplicate payload folds rather than refusing");
1935
1936 assert_eq!(twice.asset(&asset.hash()), Some(&asset));
1937 assert_eq!(
1938 twice.assets().count(),
1939 1,
1940 "the table holds one entry per hash, however often it arrives"
1941 );
1942 }
1943
1944 #[test]
1948 fn a_payload_that_does_not_hash_to_its_key_is_refused() {
1949 let asset = png(b"artwork");
1950 let forged = png(b"different artwork").hash();
1951 assert_ne!(
1952 forged,
1953 asset.hash(),
1954 "precondition: the key must name other bytes or nothing is tested"
1955 );
1956
1957 let doc = Document::default();
1958 let Err(error) = doc.try_apply(&Commit::new(
1959 "Forged a payload".into(),
1960 vec![OpCodes::Asset(forged, asset)],
1961 )) else {
1962 panic!("a payload that does not hash to its key must refuse the commit");
1963 };
1964 assert_eq!(error, FoldError::AssetHashMismatch(forged));
1965 }
1966
1967 #[test]
1973 fn a_document_round_trips_through_json_and_the_marks_come_back() {
1974 let doc = wired_document();
1975 let text = serde_json::to_string(&doc).expect("a document serializes");
1976 let read: Document = serde_json::from_str(&text).expect("and parses back");
1977
1978 assert_eq!(read, doc);
1979 assert_ne!(read.stamp(), doc.stamp(), "a parsed value is its own");
1980 assert_eq!(read.rev(), Rev::ZERO, "a file names no log position");
1981 assert_eq!(
1982 read.ids().mint::<crate::id::BlockKind>(),
1983 doc.clone().ids().mint::<crate::id::BlockKind>(),
1984 "the marks are derived from what the file holds",
1985 );
1986 }
1987
1988 #[test]
1991 fn a_version_from_the_future_is_refused() {
1992 let text = serde_json::to_string(&Document::default()).expect("it serializes");
1993 assert!(text.contains(&format!(r#""version":{CURRENT_VERSION}"#)));
1994 let ahead = text.replace(
1995 &format!(r#""version":{CURRENT_VERSION}"#),
1996 &format!(r#""version":{}"#, CURRENT_VERSION + 1),
1997 );
1998 let refusal = serde_json::from_str::<Document>(&ahead).expect_err("it is refused");
1999 assert!(
2000 refusal.to_string().contains("newer than this build reads"),
2001 "{refusal}"
2002 );
2003 }
2004
2005 #[test]
2006 fn a_payload_over_the_limit_is_refused() {
2007 let oversized = png(&vec![0u8; crate::block_model::ASSET_LIMIT + 1]);
2008 let hash = oversized.hash();
2009 let doc = Document::default();
2010
2011 let Err(error) = doc.try_apply(&Commit::new(
2012 "Committed a photograph".into(),
2013 vec![OpCodes::Asset(hash, oversized)],
2014 )) else {
2015 panic!("a payload over the limit must refuse the commit");
2016 };
2017 assert_eq!(
2018 error,
2019 FoldError::AssetTooLarge(hash, crate::block_model::ASSET_LIMIT + 1)
2020 );
2021
2022 let exact = png(&vec![0u8; crate::block_model::ASSET_LIMIT]);
2025 assert!(
2026 doc.try_apply(&Commit::new(
2027 "Committed the largest allowed artwork".into(),
2028 vec![OpCodes::Asset(exact.hash(), exact)],
2029 ))
2030 .is_ok(),
2031 "a payload exactly at the limit must fold",
2032 );
2033 }
2034
2035 #[test]
2042 fn the_next_id_is_one_past_the_highest_the_fold_saw() {
2043 let mut document = wired_document();
2044 assert!(
2045 document.block(&block_id(2)).is_some() && document.block(&block_id(3)).is_none(),
2046 "precondition: the fixture's blocks run out at b2",
2047 );
2048 assert_eq!(document.mint::<crate::id::BlockKind>(), block_id(3));
2049 assert_eq!(document.mint::<crate::id::PinKind>(), pin_id(5));
2050 assert_eq!(
2051 document.mint::<crate::id::TextKind>(),
2052 text_id(8),
2053 "each kind counts in its own space",
2054 );
2055 }
2056
2057 #[test]
2060 fn a_deleted_entity_does_not_free_its_id() {
2061 let document = wired_document()
2062 .try_apply(&Commit::new(
2063 "Deleted a block".into(),
2064 vec![OpCodes::Block(block_id(2), Crud::Delete)],
2065 ))
2066 .expect("the delete folds");
2067 assert!(
2068 document.block(&block_id(2)).is_none(),
2069 "precondition: b2 is gone from the document",
2070 );
2071 let mut ids = document.ids();
2072 assert_eq!(ids.mint::<crate::id::BlockKind>(), block_id(3));
2073 }
2074
2075 #[test]
2078 fn a_document_is_its_asset_table_too() {
2079 let fold_payload = |asset: &Asset| {
2080 Document::default()
2081 .try_apply(&Commit::new("Placed it".into(), vec![payload(asset)]))
2082 .expect("the fold succeeds")
2083 };
2084 let one = fold_payload(&png(b"artwork"));
2085 let other = fold_payload(&png(b"other artwork"));
2086
2087 assert_eq!(one.rev(), other.rev(), "precondition: same log position");
2088 assert_ne!(one, other);
2089 }
2090
2091 #[test]
2095 fn chronological_orders_by_id_alone() {
2096 let block = Block {
2097 title: label_init("tied".into()),
2098 ..Block::default()
2099 };
2100 let touched = Block {
2101 rect: rect(9, 9),
2102 ..block.clone()
2103 };
2104 assert_eq!(
2105 chronological([(block_id(2), &block), (block_id(1), &touched)].into_iter()),
2106 vec![block_id(1), block_id(2)],
2107 );
2108 }
2109}