Skip to main content

blockworx_doc/
id.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
2use std::marker::PhantomData;
3use std::str::FromStr;
4
5/// One entity's identity: a per-kind, document-global counter.
6///
7/// The wire form is the [`Display`](std::fmt::Display) spelling — `b7` —
8/// which is both what a human carries in their head and a legal JSON map
9/// key, so the entity tables can be objects keyed by id.
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct Id<K: IdKind>(u32, PhantomData<K>);
12
13impl<K: IdKind> std::fmt::Display for Id<K> {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "{}{}", K::MNEMONIC, self.0)
16    }
17}
18
19/// The kind is a compile-time tag, so the derive would print it as
20/// `PhantomData<…>` in every `?id` the editor's mutation log carries.
21impl<K: IdKind> std::fmt::Debug for Id<K> {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "{self}")
24    }
25}
26
27impl<K: IdKind> Id<K> {
28    pub const NULL: Id<K> = Id::<K>(0, PhantomData::<K>);
29
30    /// An id by its number, for fixtures and for a decoder that has already
31    /// checked the mnemonic. Everything else takes ids from an
32    /// [`Allocator`], which is what keeps two live entities from sharing
33    /// one.
34    pub(crate) const fn from_raw(n: u32) -> Self {
35        Id(n, PhantomData)
36    }
37}
38
39/// `NULL`, never a minted id: the default is the meaningful zero (the
40/// document root / "none"), and minting stays an explicit client act.
41impl<K: IdKind> Default for Id<K> {
42    fn default() -> Self {
43        Self::NULL
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
48#[error("{0:?} does not name a {1} id")]
49pub struct BadId(String, &'static str);
50
51#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
52#[error("{0:?} does not name anything a commit can touch")]
53pub struct BadEntityRef(String);
54
55impl<K: IdKind> FromStr for Id<K> {
56    type Err = BadId;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        s.strip_prefix(K::MNEMONIC)
60            .and_then(|n| n.parse().ok())
61            .map(Id::from_raw)
62            .ok_or_else(|| BadId(s.to_owned(), K::NOUN))
63    }
64}
65
66impl<K: IdKind> Serialize for Id<K> {
67    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
68        serializer.collect_str(self)
69    }
70}
71
72impl<'de, K: IdKind> Deserialize<'de> for Id<K> {
73    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
74        deserializer.deserialize_str(IdVisitor::<K>(PhantomData))
75    }
76}
77
78struct IdVisitor<K: IdKind>(PhantomData<K>);
79
80impl<K: IdKind> de::Visitor<'_> for IdVisitor<K> {
81    type Value = Id<K>;
82
83    fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "a {} id, spelled \"{}7\"", K::NOUN, K::MNEMONIC)
85    }
86
87    fn visit_str<E: de::Error>(self, text: &str) -> Result<Self::Value, E> {
88        text.parse().map_err(E::custom)
89    }
90}
91
92/// The wire form is the narration spelling, so a `touched` list reads as
93/// the names a bug report and a `grep` both carry.
94impl Serialize for EntityRef {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        serializer.collect_str(self)
97    }
98}
99
100impl<'de> Deserialize<'de> for EntityRef {
101    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
102        let text = String::deserialize(deserializer)?;
103        text.parse().map_err(de::Error::custom)
104    }
105}
106
107/// The supertraits are the tag's, not the id's: `Id<K>` derives `Copy`,
108/// `Ord` and `Hash`, and without them every generic bound would have to
109/// restate what a compile-time tag trivially satisfies.
110pub trait IdKind: Copy + Ord + std::hash::Hash {
111    /// The lower-case letter every id of this kind is spelled with.
112    const MNEMONIC: char;
113    /// What this kind is called in a message a human reads.
114    const NOUN: &'static str;
115    /// This kind's high-water mark inside an [`Allocator`].
116    fn mark(ids: &mut Allocator) -> &mut u32;
117}
118
119/// The kinds, declared once: the tag type, its alias, its spellings, and
120/// its slot in the allocator and the id union. A macro because these five
121/// must agree per kind and nothing weaker than generation makes them
122/// unable to drift.
123macro_rules! id_kinds {
124    ($( $variant:ident => $kind:ident, $alias:ident, $mark:ident, $mnemonic:literal, $noun:literal; )*) => {
125        $(
126            #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
127            pub struct $kind;
128
129            impl IdKind for $kind {
130                const MNEMONIC: char = $mnemonic;
131                const NOUN: &'static str = $noun;
132                fn mark(ids: &mut Allocator) -> &mut u32 {
133                    &mut ids.$mark
134                }
135            }
136
137            pub type $alias = Id<$kind>;
138        )*
139
140        /// One reference to anything an op can touch — the id union that a
141        /// note's anchor, the spotlight's footprints, and every "what did
142        /// this op target" question share, so the entity kinds are never
143        /// enumerated twice.
144        ///
145        /// Displays in the log's narration spelling (`block <id>`,
146        /// `document`, `asset <hash>`), which is what makes
147        /// [`OpCodes::narrate`](crate::opcode::OpCodes::narrate) a
148        /// composition over this type instead of a second list of the kinds.
149        /// That spelling is also the wire form: a manifest row's `touched`
150        /// list is the same names a reader greps for.
151        #[derive(Clone, Copy, PartialEq, Eq, Debug)]
152        pub enum EntityRef {
153            Document,
154            $( $variant($alias), )*
155            Asset(crate::hash::AssetHash),
156        }
157
158        impl std::fmt::Display for EntityRef {
159            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160                match self {
161                    EntityRef::Document => f.write_str("document"),
162                    $( EntityRef::$variant(id) => write!(f, "{} {id}", $noun), )*
163                    EntityRef::Asset(hash) => write!(f, "asset {hash}"),
164                }
165            }
166        }
167
168        impl std::str::FromStr for EntityRef {
169            type Err = BadEntityRef;
170
171            fn from_str(text: &str) -> Result<Self, Self::Err> {
172                let unknown = || BadEntityRef(text.to_owned());
173                if text == "document" {
174                    return Ok(EntityRef::Document);
175                }
176                let (noun, name) = text.split_once(' ').ok_or_else(unknown)?;
177                match noun {
178                    $( $noun => name.parse().map(EntityRef::$variant).map_err(|_| unknown()), )*
179                    "asset" => crate::hash::AssetHash::from_hex(name)
180                        .map(EntityRef::Asset)
181                        .ok_or_else(unknown),
182                    _ => Err(unknown()),
183                }
184            }
185        }
186
187        /// Per-kind high-water marks: the largest id of each kind the
188        /// document has ever held, minted or folded. Never stored — a
189        /// loaded document derives them from its own content — so there is
190        /// no durable counter to run backwards and no invariant for the
191        /// fold to police.
192        #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
193        pub struct Allocator {
194            $( $mark: u32, )*
195        }
196
197        impl Allocator {
198            /// Raise every mark to `other`'s. Marks only ever rise, so
199            /// merging two views of the same document cannot hand out an
200            /// id either of them has already given.
201            pub fn raise_to(&mut self, other: &Allocator) {
202                $( self.$mark = self.$mark.max(other.$mark); )*
203            }
204
205            /// Record that `target` exists, so it is never minted again.
206            pub fn observe(&mut self, target: EntityRef) {
207                match target {
208                    $( EntityRef::$variant(id) => self.raise(id), )*
209                    EntityRef::Document | EntityRef::Asset(_) => {}
210                }
211            }
212        }
213    };
214}
215
216id_kinds! {
217    Block => BlockKind, BlockId, blocks, 'b', "block";
218    Pin => PinKind, PinId, pins, 'p', "pin";
219    Route => RouteKind, RouteId, routes, 'r', "route";
220    RouteLabel => RouteLabelKind, RouteLabelId, route_labels, 'x', "route-label";
221    Text => TextKind, TextId, texts, 't', "text";
222    Area => AreaKind, AreaId, areas, 'a', "area";
223    Image => ImageKind, ImageId, images, 'i', "image";
224}
225
226impl Allocator {
227    /// The next id of its kind, counting from 1 — [`Id::NULL`] is the
228    /// document root and must stay distinguishable from a minted id.
229    pub fn mint<K: IdKind>(&mut self) -> Id<K> {
230        let mark = K::mark(self);
231        *mark += 1;
232        Id::from_raw(*mark)
233    }
234
235    fn raise<K: IdKind>(&mut self, id: Id<K>) {
236        let mark = K::mark(self);
237        *mark = (*mark).max(id.0);
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn debug_is_the_display_spelling() {
247        let id: BlockId = Allocator::default().mint();
248        assert_eq!(format!("{id:?}"), format!("{id}"));
249        assert!(!format!("{id:?}").contains("PhantomData"));
250    }
251
252    /// The greppable spelling is the wire form: what a bug report carries
253    /// is what a `grep` over the file finds.
254    #[test]
255    fn an_id_is_its_display_spelling_on_the_wire() {
256        let id: BlockId = Id::from_raw(7);
257        assert_eq!(id.to_string(), "b7");
258        assert_eq!(
259            serde_json::to_string(&id).expect("an id serializes"),
260            "\"b7\""
261        );
262        assert_eq!("b7".parse::<BlockId>(), Ok(id));
263        assert_eq!(BlockId::NULL.to_string(), "b0");
264        assert_eq!("b0".parse::<BlockId>(), Ok(BlockId::NULL));
265    }
266
267    /// The mnemonic is part of the id, not decoration: a pin's spelling
268    /// does not read back as a block.
269    #[test]
270    fn a_foreign_mnemonic_is_refused() {
271        assert!("p7".parse::<BlockId>().is_err());
272        assert!("7".parse::<BlockId>().is_err());
273        assert!("b".parse::<BlockId>().is_err());
274        assert!("b-1".parse::<BlockId>().is_err());
275    }
276
277    /// Per-kind spaces are independent, and every kind has its own mark —
278    /// which is also what catches two kinds sharing one slot.
279    #[test]
280    fn the_kinds_count_independently() {
281        let mut ids = Allocator::default();
282        let spelled = [
283            ids.mint::<BlockKind>().to_string(),
284            ids.mint::<PinKind>().to_string(),
285            ids.mint::<RouteKind>().to_string(),
286            ids.mint::<RouteLabelKind>().to_string(),
287            ids.mint::<TextKind>().to_string(),
288            ids.mint::<AreaKind>().to_string(),
289            ids.mint::<ImageKind>().to_string(),
290        ];
291        assert_eq!(spelled, ["b1", "p1", "r1", "x1", "t1", "a1", "i1"]);
292    }
293
294    /// Counting from 1 keeps the root distinguishable from the first block.
295    #[test]
296    fn minting_never_yields_the_null_id() {
297        let mut ids = Allocator::default();
298        assert_ne!(ids.mint::<BlockKind>(), BlockId::NULL);
299    }
300
301    /// An observed id is never handed out again, whichever kind it names.
302    #[test]
303    fn observing_raises_the_mark_of_that_kind_alone() {
304        let mut ids = Allocator::default();
305        ids.observe(EntityRef::Block(Id::from_raw(41)));
306        assert_eq!(ids.mint::<BlockKind>(), Id::from_raw(42));
307        assert_eq!(ids.mint::<PinKind>(), Id::from_raw(1));
308        // Marks only rise: a lower id observed later cannot rewind them.
309        ids.observe(EntityRef::Block(Id::from_raw(3)));
310        assert_eq!(ids.mint::<BlockKind>(), Id::from_raw(43));
311    }
312
313    #[test]
314    fn entity_refs_keep_the_narration_spelling() {
315        assert_eq!(
316            EntityRef::RouteLabel(Id::from_raw(2)).to_string(),
317            "route-label x2"
318        );
319        assert_eq!(EntityRef::Block(Id::from_raw(1)).to_string(), "block b1");
320        assert_eq!(EntityRef::Document.to_string(), "document");
321    }
322
323    /// A manifest row's `touched` list is these names, so the spelling has
324    /// to survive the round trip a rev pick reads it back through.
325    #[test]
326    fn every_entity_ref_round_trips_through_its_narration_spelling() {
327        let asset = crate::hash::AssetHash::of(b"<svg/>");
328        for reference in [
329            EntityRef::Document,
330            EntityRef::Block(Id::from_raw(7)),
331            EntityRef::Pin(Id::from_raw(7)),
332            EntityRef::Route(Id::from_raw(1)),
333            EntityRef::RouteLabel(Id::from_raw(2)),
334            EntityRef::Text(Id::from_raw(3)),
335            EntityRef::Area(Id::from_raw(4)),
336            EntityRef::Image(Id::from_raw(5)),
337            EntityRef::Asset(asset),
338        ] {
339            let spelled = reference.to_string();
340            assert_eq!(spelled.parse::<EntityRef>(), Ok(reference), "{spelled}");
341            assert_eq!(
342                serde_json::to_string(&reference).expect("it serializes"),
343                format!("\"{spelled}\""),
344            );
345            assert_eq!(
346                serde_json::from_str::<EntityRef>(&format!("\"{spelled}\"")).expect("it parses"),
347                reference,
348            );
349        }
350        assert_eq!(
351            EntityRef::Block(Id::from_raw(7)).to_string(),
352            "block b7",
353            "the spelling §10.1 names",
354        );
355        for bad in ["", "block", "block p7", "blocks b7", "asset zz", "b7"] {
356            assert!(bad.parse::<EntityRef>().is_err(), "{bad:?} parsed");
357        }
358    }
359}