Skip to main content

tephra_types/
name.rs

1//! Event type and tag names, and the sorted [`Tags`] set.
2//!
3//! An [`EventType`] and a [`Tag`] are arbitrary opaque, non-empty strings (the spec never
4//! parses them), each stored as an exact-sized `Box<str>`. [`Tags`] is a sorted,
5//! duplicate-free set of tags. These are the addressable surface of an event: they drive
6//! queries and the append condition, and they become index keys in the engine.
7
8use std::{fmt, slice};
9
10use smallvec::SmallVec;
11use thiserror::Error;
12
13/// Maximum length, in bytes, of an [`EventType`] or [`Tag`]. Each is stored with a
14/// fixed-width `u16` length in the engine's encoded header, so the field capacity is the
15/// limit.
16pub const MAX_NAME_LEN: usize = u16::MAX as usize;
17
18// ---------------------------------------------------------------------------
19// EventType and Tag
20// ---------------------------------------------------------------------------
21
22/// Error constructing an [`EventType`] or [`Tag`].
23#[derive(Debug, Error, PartialEq, Eq)]
24pub enum NameError {
25    #[error("{what} must not be empty")]
26    Empty { what: &'static str },
27    #[error("{what} is {len} bytes, exceeding the {max}-byte maximum")]
28    TooLong {
29        what: &'static str,
30        len: usize,
31        max: usize,
32    },
33}
34
35fn validate_name(s: &str, what: &'static str) -> Result<(), NameError> {
36    if s.is_empty() {
37        return Err(NameError::Empty { what });
38    }
39    if s.len() > MAX_NAME_LEN {
40        return Err(NameError::TooLong {
41            what,
42            len: s.len(),
43            max: MAX_NAME_LEN,
44        });
45    }
46    Ok(())
47}
48
49/// An event type. An arbitrary opaque, non-empty string (the spec never parses it),
50/// stored as an exact-sized `Box<str>`.
51///
52/// There is deliberately no `Deref<Target = str>`: reaching through a newtype makes
53/// method resolution ambiguous (`ty.len()` would silently mean the string length).
54/// Use [`as_str`](Self::as_str), [`AsRef<str>`], or [`Display`](fmt::Display).
55#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
56pub struct EventType(Box<str>);
57
58impl EventType {
59    /// Constructs an event type, rejecting empty or over-long input.
60    pub fn new(s: impl Into<Box<str>>) -> Result<Self, NameError> {
61        let s = s.into();
62        validate_name(&s, "event type")?;
63        Ok(EventType(s))
64    }
65
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69
70    pub fn into_inner(self) -> Box<str> {
71        self.0
72    }
73}
74
75impl AsRef<str> for EventType {
76    fn as_ref(&self) -> &str {
77        &self.0
78    }
79}
80
81impl fmt::Display for EventType {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        self.0.fmt(f)
84    }
85}
86
87/// A tag, e.g. `course:c1`. An arbitrary opaque, non-empty string (the spec does not
88/// split it into key/value), stored as an exact-sized `Box<str>`. Like [`EventType`],
89/// it has no `Deref`; use [`as_str`](Self::as_str), [`AsRef<str>`], or `Display`.
90#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
91pub struct Tag(Box<str>);
92
93impl Tag {
94    /// Constructs a tag, rejecting empty or over-long input.
95    pub fn new(s: impl Into<Box<str>>) -> Result<Self, NameError> {
96        let s = s.into();
97        validate_name(&s, "tag")?;
98        Ok(Tag(s))
99    }
100
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104
105    pub fn into_inner(self) -> Box<str> {
106        self.0
107    }
108}
109
110impl AsRef<str> for Tag {
111    fn as_ref(&self) -> &str {
112        &self.0
113    }
114}
115
116impl fmt::Display for Tag {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        self.0.fmt(f)
119    }
120}
121
122// ---------------------------------------------------------------------------
123// Tags
124// ---------------------------------------------------------------------------
125
126/// Error constructing [`Tags`].
127#[derive(Debug, Error, PartialEq, Eq)]
128pub enum TagsError {
129    #[error("duplicate tag '{tag}'")]
130    Duplicate { tag: Tag },
131}
132
133/// A sorted, duplicate-free set of tags.
134///
135/// Sortedness is load-bearing: it makes the encoded form canonical (identical sets
136/// produce identical bytes) and makes AND-matching a linear merge over two sorted
137/// slices. Duplicates are rejected rather than silently deduped, because a duplicate
138/// is a caller bug and swallowing it would make an event round-trip to something the
139/// caller did not submit.
140///
141/// A `SmallVec<[Tag; 4]>` rather than a `BTreeSet`: tag sets are 1 to 4 entries, so an
142/// inline sorted vec beats node allocation and pointer chasing, and the encoder needs
143/// a slice anyway.
144#[derive(Clone, Debug, Default, PartialEq, Eq)]
145pub struct Tags(SmallVec<[Tag; 4]>);
146
147impl Tags {
148    /// Constructs a tag set, sorting the input and rejecting any duplicate.
149    ///
150    /// Accepts anything iterable over [`Tag`]: an array literal (`[a, b]`), a `Vec`, or another
151    /// iterator. The `impl IntoIterator` bound is what lets an array of any length be passed
152    /// directly (unlike `Into<SmallVec<..>>`, which only converts an array of the exact inline
153    /// size); collection into the backing `SmallVec` uses its `FromIterator` impl.
154    pub fn new(tags: impl IntoIterator<Item = Tag>) -> Result<Self, TagsError> {
155        let mut tags: SmallVec<[Tag; 4]> = tags.into_iter().collect();
156        tags.sort_unstable();
157        // Scan for the first adjacent duplicate. `find` short-circuits, and on the
158        // error path the vec is discarded, so move the offender out with an O(1)
159        // `swap_remove` rather than cloning it.
160        if let Some(i) = (1..tags.len()).find(|&i| tags[i] == tags[i - 1]) {
161            return Err(TagsError::Duplicate {
162                tag: tags.swap_remove(i),
163            });
164        }
165        Ok(Tags(tags))
166    }
167
168    /// The empty tag set.
169    pub fn empty() -> Self {
170        Tags(SmallVec::new())
171    }
172
173    pub fn into_inner(self) -> SmallVec<[Tag; 4]> {
174        self.0
175    }
176
177    pub fn as_slice(&self) -> &[Tag] {
178        &self.0
179    }
180
181    pub fn len(&self) -> usize {
182        self.0.len()
183    }
184
185    pub fn is_empty(&self) -> bool {
186        self.0.is_empty()
187    }
188
189    pub fn iter(&self) -> slice::Iter<'_, Tag> {
190        self.0.iter()
191    }
192}
193
194impl IntoIterator for Tags {
195    type Item = Tag;
196    type IntoIter = smallvec::IntoIter<[Tag; 4]>;
197
198    fn into_iter(self) -> Self::IntoIter {
199        self.0.into_iter()
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    fn tag(s: &str) -> Tag {
208        Tag::new(s).unwrap()
209    }
210
211    fn tags(items: &[&str]) -> Tags {
212        Tags::new(items.iter().map(|s| tag(s)).collect::<SmallVec<[Tag; 4]>>()).unwrap()
213    }
214
215    #[test]
216    fn name_rejects_empty() {
217        assert_eq!(
218            EventType::new(""),
219            Err(NameError::Empty { what: "event type" })
220        );
221        assert_eq!(Tag::new(""), Err(NameError::Empty { what: "tag" }));
222    }
223
224    #[test]
225    fn name_rejects_over_long() {
226        let big = "x".repeat(MAX_NAME_LEN + 1);
227        assert_eq!(
228            EventType::new(big),
229            Err(NameError::TooLong {
230                what: "event type",
231                len: MAX_NAME_LEN + 1,
232                max: MAX_NAME_LEN,
233            })
234        );
235        // Exactly at the maximum is accepted.
236        assert!(Tag::new("y".repeat(MAX_NAME_LEN)).is_ok());
237    }
238
239    #[test]
240    fn name_accessors_and_ordering() {
241        assert_eq!(EventType::new("Registered").unwrap().as_str(), "Registered");
242        assert_eq!(<Tag as AsRef<str>>::as_ref(&tag("course:c1")), "course:c1");
243        assert!(tag("course:a") < tag("course:b"));
244        assert_eq!(format!("{}", tag("student:s1")), "student:s1");
245    }
246
247    #[test]
248    fn tags_sorts_input() {
249        let t = tags(&["course:c1", "student:s1", "admin:a1"]);
250        let got: Vec<&str> = t.iter().map(|t| t.as_str()).collect();
251        assert_eq!(got, ["admin:a1", "course:c1", "student:s1"]);
252    }
253
254    #[test]
255    fn tags_rejects_duplicates() {
256        let input: SmallVec<[Tag; 4]> = [tag("course:c1"), tag("course:c1")].into_iter().collect();
257        assert_eq!(
258            Tags::new(input),
259            Err(TagsError::Duplicate {
260                tag: tag("course:c1")
261            })
262        );
263    }
264
265    #[test]
266    fn tags_empty_is_empty() {
267        assert!(Tags::empty().is_empty());
268        assert_eq!(Tags::empty().len(), 0);
269    }
270}