strut_core/replica/
lifetime_id.rs

1use std::any::type_name;
2use std::fmt::{Debug, Display, Formatter};
3use std::hash::{DefaultHasher, Hash, Hasher};
4use std::time::SystemTime;
5
6/// Ten pseudo-random lower-case ASCII letters with a few visually digestible
7/// string representations. **Not** cryptographically secure.
8#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct LifetimeId {
10    bytes: [u8; 10],
11}
12
13impl LifetimeId {
14    /// Generates a pseudo-randomized, non-secure [`LifetimeId`].
15    pub fn random() -> Self {
16        // Prepare storage for the pseudo-random bytes
17        let mut bytes = [0u8; 10];
18
19        // Prepare the hash holder variable
20        let mut hash = 0u64;
21
22        // Static set for picking random characters
23        static CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
24
25        // Repeat for each random character in lifetime ID
26        for i in 0..bytes.len() {
27            // Refresh the hash when necessary
28            if hash == 0 {
29                hash = Self::make_hash();
30            }
31
32            // Pick a pseudo-random index
33            let idx = (hash % CHARSET.len() as u64) as usize;
34
35            // Push the character at the pseudo-random index
36            bytes[i] = CHARSET[idx];
37
38            // Shift a few bits to get new data
39            hash >>= 5;
40        }
41
42        Self { bytes }
43    }
44
45    /// Returns a [`Hyphenated`] version of this [`LifetimeId`].
46    pub fn hyphenated(&self) -> Hyphenated<'_> {
47        Hyphenated(self)
48    }
49
50    /// Returns an [`Underscored`] version of this [`LifetimeId`].
51    pub fn underscored(&self) -> Underscored<'_> {
52        Underscored(self)
53    }
54
55    /// Returns an [`Dotted`] version of this [`LifetimeId`].
56    pub fn dotted(&self) -> Dotted<'_> {
57        Dotted(self)
58    }
59
60    /// Returns a [`Glued`] version of this [`LifetimeId`].
61    pub fn glued(&self) -> Glued<'_> {
62        Glued(self)
63    }
64}
65
66impl LifetimeId {
67    /// Generates a pseudo-random `u64` that is **not** cryptographically secure.
68    fn make_hash() -> u64 {
69        // Make a seed such as the current time in nanoseconds
70        let seed = SystemTime::now()
71            .duration_since(SystemTime::UNIX_EPOCH)
72            .expect("current time should always be after UNIX epoch")
73            .as_nanos(); // nanoseconds provide more entropy
74
75        // Hash the nanoseconds
76        let mut hasher = DefaultHasher::new();
77        seed.hash(&mut hasher);
78
79        hasher.finish()
80    }
81
82    /// Exposes an immutable view of the internally held bytes.
83    pub fn view_bytes(&self) -> &[u8; 10] {
84        &self.bytes
85    }
86
87    /// Exposes an immutable view of the internally held bytes as a single
88    /// ten-character string reference.
89    pub fn view_glued(&self) -> &str {
90        std::str::from_utf8(&self.bytes).expect(concat!(
91            "it should be possible to view the internal buffer as a &str because",
92            " the constructor of this struct always interprets the input string",
93            " as a sequence of valid UTF-8 characters",
94        ))
95    }
96
97    /// Exposes an immutable view of the internally held bytes as three string
98    /// references: three, four, and three character long.
99    pub fn view_chunks(&self) -> (&str, &str, &str) {
100        let chunk_a = std::str::from_utf8(&self.bytes[0..3]).expect(concat!(
101            "it should be possible to view the internal buffer as a &str because",
102            " the constructor of this struct always interprets the input string",
103            " as a sequence of valid UTF-8 characters",
104        ));
105        let chunk_b = std::str::from_utf8(&self.bytes[3..7]).expect(concat!(
106            "it should be possible to view the internal buffer as a &str because",
107            " the constructor of this struct always interprets the input string",
108            " as a sequence of valid UTF-8 characters",
109        ));
110        let chunk_c = std::str::from_utf8(&self.bytes[7..10]).expect(concat!(
111            "it should be possible to view the internal buffer as a &str because",
112            " the constructor of this struct always interprets the input string",
113            " as a sequence of valid UTF-8 characters",
114        ));
115
116        (chunk_a, chunk_b, chunk_c)
117    }
118}
119
120impl Display for LifetimeId {
121    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122        Display::fmt(&self.hyphenated(), f)
123    }
124}
125
126impl Debug for LifetimeId {
127    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct(type_name::<Self>())
129            .field("bytes", &self.hyphenated())
130            .finish()
131    }
132}
133
134/// A wrapped [`LifetimeId`] that implements [`Display`] by writing the ID in
135/// three chunks, separated by the hyphen characters. Writes exactly twelve
136/// ASCII characters.
137pub struct Hyphenated<'a>(&'a LifetimeId);
138
139impl<'a> Display for Hyphenated<'a> {
140    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
141        let (chunk_a, chunk_b, chunk_c) = self.0.view_chunks();
142
143        write!(f, "{}-{}-{}", chunk_a, chunk_b, chunk_c)
144    }
145}
146
147impl<'a> Debug for Hyphenated<'a> {
148    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
149        Display::fmt(self, f)
150    }
151}
152
153/// A wrapped [`LifetimeId`] that implements [`Display`] by writing the ID in
154/// three chunks, separated by the underscore characters. Writes exactly twelve
155/// ASCII characters.
156pub struct Underscored<'a>(&'a LifetimeId);
157
158impl<'a> Display for Underscored<'a> {
159    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
160        let (chunk_a, chunk_b, chunk_c) = self.0.view_chunks();
161
162        write!(f, "{}_{}_{}", chunk_a, chunk_b, chunk_c)
163    }
164}
165
166impl<'a> Debug for Underscored<'a> {
167    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168        Display::fmt(self, f)
169    }
170}
171
172/// A wrapped [`LifetimeId`] that implements [`Display`] by writing the ID in
173/// three chunks, separated by the dot (full stop) characters. Writes exactly
174/// twelve ASCII characters.
175pub struct Dotted<'a>(&'a LifetimeId);
176
177impl<'a> Display for Dotted<'a> {
178    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
179        let (chunk_a, chunk_b, chunk_c) = self.0.view_chunks();
180
181        write!(f, "{}.{}.{}", chunk_a, chunk_b, chunk_c)
182    }
183}
184
185impl<'a> Debug for Dotted<'a> {
186    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
187        Display::fmt(self, f)
188    }
189}
190
191/// A wrapped [`LifetimeId`] that implements [`Display`] by writing the ID in an
192/// unbroken chunk. Writes exactly twelve ASCII characters.
193pub struct Glued<'a>(&'a LifetimeId);
194
195impl<'a> Display for Glued<'a> {
196    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
197        write!(f, "{}", self.0.view_glued())
198    }
199}
200
201impl<'a> Debug for Glued<'a> {
202    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
203        Display::fmt(self, f)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use pretty_assertions::{assert_eq, assert_ne};
210
211    #[test]
212    fn generate_lifetime_id() {
213        // When
214        let lifetime_id = super::LifetimeId::random().to_string();
215
216        // Then
217        assert_eq!(lifetime_id.len(), 12);
218        assert!(
219            lifetime_id
220                .chars()
221                .all(|c| c.is_ascii_alphabetic() || c == '-')
222        );
223        assert!(
224            lifetime_id
225                .chars()
226                .all(|c| c.is_ascii_lowercase() || c == '-')
227        );
228        assert!(
229            matches!(lifetime_id.chars().nth(0), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
230        );
231        assert!(
232            matches!(lifetime_id.chars().nth(1), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
233        );
234        assert!(
235            matches!(lifetime_id.chars().nth(2), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
236        );
237        assert!(matches!(lifetime_id.chars().nth(3), Some(c) if c == '-'));
238        assert!(
239            matches!(lifetime_id.chars().nth(4), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
240        );
241        assert!(
242            matches!(lifetime_id.chars().nth(5), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
243        );
244        assert!(
245            matches!(lifetime_id.chars().nth(6), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
246        );
247        assert!(
248            matches!(lifetime_id.chars().nth(7), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
249        );
250        assert!(matches!(lifetime_id.chars().nth(8), Some(c) if c == '-'));
251        assert!(
252            matches!(lifetime_id.chars().nth(9), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
253        );
254        assert!(
255            matches!(lifetime_id.chars().nth(10), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
256        );
257        assert!(
258            matches!(lifetime_id.chars().nth(11), Some(c) if c.is_ascii_lowercase() && c.is_ascii_lowercase())
259        );
260    }
261
262    #[test]
263    fn generate_lifetime_ids() {
264        // When
265        let lifetime_id_a = super::LifetimeId::random().to_string();
266        let lifetime_id_b = super::LifetimeId::random().to_string();
267
268        // Then
269        assert_ne!(lifetime_id_a, lifetime_id_b);
270    }
271}