monospace_core/identity.rs
1//! A shape's identity: the ordinal a figure is drawn under and a buffer records beside the cells it
2//! decided. See _Stamping_ in [`docs/model.md`](../../../docs/model.md).
3//!
4//! # Design notes
5//!
6//! **The type is an ordinal and holds no rule about a diagram.** Who issues an identity, whether two
7//! may share one and what may be done with it are all questions about a diagram rather than about a
8//! number, and they stay with the diagram — see _Identity_ in
9//! [`docs/diagram-model.md`](../../../docs/diagram-model.md). What is left here is the one thing
10//! the core has to hold: a number it can compare and write down.
11//!
12//! **The one rule it does hold is about the value rather than about any diagram.** Zero is not an
13//! identity, so the ordinal is a [`NonZeroU16`] and [`ShapeId::new`] is the way in. That is a
14//! narrower claim than the issuing and uniqueness questions above, and the distinction is what puts
15//! it here: no diagram ever issued a `0`, and a diagram seeded at `1` says so.
16//!
17//! **Two bytes that are `Copy` rather than a string.** A buffer records one identity per stamped
18//! position, so the width is paid for once per stamped cell and once per figure a diagram holds:
19//! `u16` is 65 535 figures, and an ordinal is compared, copied and stored without an allocation; a
20//! borrowed `&ShapeId` per position would put a lifetime in front of every caller to save nothing.
21//!
22//! **A module of its own rather than a line in `geometry`.** `Pos`, `Size`, `Direction` and
23//! `Orientation` are places and amounts on the plane, and an identity is neither — putting the two
24//! in one file would give the identity a neighbor it has nothing to say to. [`crate::stroke`] is
25//! the precedent: one type, one module.
26
27use std::fmt;
28use std::num::NonZeroU16;
29
30/// A shape's identity: the ordinal a figure is drawn under, and what a buffer records beside each
31/// cell that figure decided.
32///
33/// Built from its ordinal by [`ShapeId::new`] and written back out as the bare number, `1` and `2`
34/// and so on. It is `Copy`, and it carries one rule of its own: zero is not an identity.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct ShapeId(NonZeroU16);
37
38impl ShapeId {
39 /// Builds an identity from its ordinal — `1`, `2`, and so on.
40 ///
41 /// The [`NonZeroU16`] is the rule that `0` is not an identity, so a caller cannot ask for one:
42 /// what a plain number would have to be checked for is instead a type a zero cannot have.
43 #[must_use]
44 pub fn new(ordinal: NonZeroU16) -> Self {
45 Self(ordinal)
46 }
47}
48
49impl fmt::Display for ShapeId {
50 /// Writes the ordinal and nothing else — `1`, `2`, and so on.
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 self.0.fmt(f)
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use std::num::NonZeroU16;
59
60 use super::ShapeId;
61
62 /// An identity of the ordinal `ordinal`, which every test below builds one with.
63 fn id(ordinal: u16) -> ShapeId {
64 ShapeId::new(NonZeroU16::new(ordinal).expect("no test asks for zero"))
65 }
66
67 /// `0` is not an identity, and there is no way to ask for one.
68 ///
69 /// **The type half is the absence itself**: `ShapeId::new` takes a [`NonZeroU16`] and there is
70 /// no other constructor, so the value this test shows `0` cannot produce is the value the whole
71 /// crate has no way to hold. Asserting what `NonZeroU16::new(0)` answers is therefore not a
72 /// claim about the standard library — it is the observation that the door is closed, and it is
73 /// the only thing about a rule this absolute that a test can observe at all.
74 ///
75 /// **The lowest ordinal there is writes `1`,** which is what the second half says: the first
76 /// identity any diagram issues is the one after zero rather than zero itself.
77 ///
78 /// The reader is the other half of the name, and it lives in the crate that reads a file:
79 /// `a_next_id_of_zero_is_refused_by_name_on_stderr_and_fails` and
80 /// `an_identity_written_as_a_string_is_refused_by_name_on_stderr_and_fails` in
81 /// `crates/monospace-cli/tests/cli.rs`, where a description is something that could have asked
82 /// for a `0` and is refused.
83 #[test]
84 fn an_identity_cannot_be_zero_and_nothing_in_the_reader_or_the_type_allows_it_to_be_asked_for()
85 {
86 assert!(
87 NonZeroU16::new(0).is_none(),
88 "the ordinal an identity is built from cannot be zero, which is what keeps `0` \
89 unrepresentable rather than merely unused"
90 );
91 assert_eq!(
92 id(1).to_string(),
93 "1",
94 "the lowest ordinal there is writes itself, so a diagram issues `1` first"
95 );
96 }
97
98 /// `Display` writes the ordinal alone.
99 ///
100 /// **The absence of the `#` is the claim**, and a table of rendered strings is what shows it:
101 /// each of these is the whole of what a reader of the message sees, and none of them carries a
102 /// character the type does not hold. The largest ordinal is there because it is where a `u16`
103 /// stops being a short string, and the second because a multi-digit ordinal is where a
104 /// hand-written `#` in a caption would have been most tempting.
105 #[test]
106 fn an_identity_writes_its_ordinal_and_nothing_else() {
107 for (ordinal, written) in [
108 (1, "1"),
109 (2, "2"),
110 (7, "7"),
111 (10, "10"),
112 (u16::MAX, "65535"),
113 ] {
114 assert_eq!(
115 id(ordinal).to_string(),
116 written,
117 "an identity writes its ordinal and nothing else"
118 );
119 }
120 }
121}