1use serde::{Deserialize, Serialize};
6
7use crate::{
8 block_model::{
9 Area, AreaUpdate, Asset, Block, BlockUpdate, Image, ImageUpdate, Pin, PinUpdate, Route,
10 RouteLabel, RouteLabelUpdate, RouteUpdate, Text, TextUpdate,
11 },
12 document::TitleBlockUpdate,
13 hash::AssetHash,
14 id::{AreaId, BlockId, ImageId, PinId, RouteId, RouteLabelId, TextId},
15};
16
17#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
20pub enum Crud<E, U> {
21 Create(E),
22 Update(U),
23 Delete,
24}
25
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27pub enum OpCodes {
28 Document(TitleBlockUpdate),
30 Block(BlockId, Crud<Block, BlockUpdate>),
31 Pin(PinId, Crud<Pin, PinUpdate>),
32 Route(RouteId, Crud<Route, RouteUpdate>),
33 RouteLabel(RouteLabelId, Crud<RouteLabel, RouteLabelUpdate>),
34 Text(TextId, Crud<Text, TextUpdate>),
35 Area(AreaId, Crud<Area, AreaUpdate>),
36 Image(ImageId, Crud<Image, ImageUpdate>),
37 Asset(AssetHash, Asset),
44}
45
46impl OpCodes {
47 #[must_use]
52 pub fn narrate(&self) -> String {
53 format!("{} {}", self.target(), self.act())
54 }
55
56 pub fn target(&self) -> crate::id::EntityRef {
60 use crate::id::EntityRef;
61 match self {
62 OpCodes::Document(_) => EntityRef::Document,
63 OpCodes::Block(id, _) => EntityRef::Block(*id),
64 OpCodes::Pin(id, _) => EntityRef::Pin(*id),
65 OpCodes::Route(id, _) => EntityRef::Route(*id),
66 OpCodes::RouteLabel(id, _) => EntityRef::RouteLabel(*id),
67 OpCodes::Text(id, _) => EntityRef::Text(*id),
68 OpCodes::Area(id, _) => EntityRef::Area(*id),
69 OpCodes::Image(id, _) => EntityRef::Image(*id),
70 OpCodes::Asset(hash, _) => EntityRef::Asset(*hash),
71 }
72 }
73
74 fn act(&self) -> &'static str {
78 fn crud<E, U>(op: &Crud<E, U>) -> &'static str {
79 match op {
80 Crud::Create(_) => "create",
81 Crud::Update(_) => "update",
82 Crud::Delete => "delete",
83 }
84 }
85 match self {
86 OpCodes::Document(_) => "update",
87 OpCodes::Asset(..) => "create",
88 OpCodes::Block(_, op) => crud(op),
89 OpCodes::Pin(_, op) => crud(op),
90 OpCodes::Route(_, op) => crud(op),
91 OpCodes::RouteLabel(_, op) => crud(op),
92 OpCodes::Text(_, op) => crud(op),
93 OpCodes::Area(_, op) => crud(op),
94 OpCodes::Image(_, op) => crud(op),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
107 fn narration_names_the_target_without_its_payload() {
108 use crate::block_model::TextUpdate;
109 use crate::fixtures::{block_id, text_id};
110 let secret = "the user's private note".to_owned();
111 let op = OpCodes::Text(text_id(9), Crud::Update(TextUpdate::Text(secret.clone())));
112 let line = op.narrate();
113 assert!(line.contains("text"), "the kind is named: {line}");
114 assert!(line.contains("update"), "the lifecycle is named: {line}");
115 assert!(
116 !line.contains(&secret),
117 "narration must not carry the payload: {line}"
118 );
119 assert!(
120 OpCodes::Block(block_id(1), Crud::Delete)
121 .narrate()
122 .contains("delete")
123 );
124 }
125}