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;
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 AsRef<str>) -> Result<Self, NameError> {
61        let s = s.as_ref();
62        validate_name(s, "event type")?;
63        Ok(EventType(Box::from(s)))
64    }
65
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69}
70
71impl AsRef<str> for EventType {
72    fn as_ref(&self) -> &str {
73        &self.0
74    }
75}
76
77impl fmt::Display for EventType {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        self.0.fmt(f)
80    }
81}
82
83/// A tag, e.g. `course:c1`. An arbitrary opaque, non-empty string (the spec does not
84/// split it into key/value), stored as an exact-sized `Box<str>`. Like [`EventType`],
85/// it has no `Deref`; use [`as_str`](Self::as_str), [`AsRef<str>`], or `Display`.
86#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
87pub struct Tag(Box<str>);
88
89impl Tag {
90    /// Constructs a tag, rejecting empty or over-long input.
91    pub fn new(s: impl AsRef<str>) -> Result<Self, NameError> {
92        let s = s.as_ref();
93        validate_name(s, "tag")?;
94        Ok(Tag(Box::from(s)))
95    }
96
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100}
101
102impl AsRef<str> for Tag {
103    fn as_ref(&self) -> &str {
104        &self.0
105    }
106}
107
108impl fmt::Display for Tag {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        self.0.fmt(f)
111    }
112}
113
114// ---------------------------------------------------------------------------
115// Tags
116// ---------------------------------------------------------------------------
117
118/// Error constructing [`Tags`].
119#[derive(Debug, Error, PartialEq, Eq)]
120pub enum TagsError {
121    #[error("duplicate tag '{tag}'")]
122    Duplicate { tag: Tag },
123}
124
125/// A sorted, duplicate-free set of tags.
126///
127/// Sortedness is load-bearing: it makes the encoded form canonical (identical sets
128/// produce identical bytes) and makes AND-matching a linear merge over two sorted
129/// slices. Duplicates are rejected rather than silently deduped, because a duplicate
130/// is a caller bug and swallowing it would make an event round-trip to something the
131/// caller did not submit.
132///
133/// A `SmallVec<[Tag; 4]>` rather than a `BTreeSet`: tag sets are 1 to 4 entries, so an
134/// inline sorted vec beats node allocation and pointer chasing, and the encoder needs
135/// a slice anyway.
136#[derive(Clone, Debug, Default, PartialEq, Eq)]
137pub struct Tags(SmallVec<[Tag; 4]>);
138
139impl Tags {
140    /// Constructs a tag set, sorting the input and rejecting any duplicate.
141    ///
142    /// Accepts anything iterable over [`Tag`]: an array literal (`[a, b]`), a `Vec`, or another
143    /// iterator. The `impl IntoIterator` bound is what lets an array of any length be passed
144    /// directly (unlike `Into<SmallVec<..>>`, which only converts an array of the exact inline
145    /// size); collection into the backing `SmallVec` uses its `FromIterator` impl.
146    pub fn new(tags: impl IntoIterator<Item = Tag>) -> Result<Self, TagsError> {
147        let mut tags: SmallVec<[Tag; 4]> = tags.into_iter().collect();
148        tags.sort_unstable();
149        // Scan for the first adjacent duplicate. `find` short-circuits, and on the
150        // error path the vec is discarded, so move the offender out with an O(1)
151        // `swap_remove` rather than cloning it.
152        if let Some(i) = (1..tags.len()).find(|&i| tags[i] == tags[i - 1]) {
153            return Err(TagsError::Duplicate {
154                tag: tags.swap_remove(i),
155            });
156        }
157        Ok(Tags(tags))
158    }
159
160    /// The empty tag set.
161    pub fn empty() -> Self {
162        Tags(SmallVec::new())
163    }
164
165    pub fn as_slice(&self) -> &[Tag] {
166        &self.0
167    }
168
169    pub fn len(&self) -> usize {
170        self.0.len()
171    }
172
173    pub fn is_empty(&self) -> bool {
174        self.0.is_empty()
175    }
176
177    pub fn iter(&self) -> std::slice::Iter<'_, Tag> {
178        self.0.iter()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn tag(s: &str) -> Tag {
187        Tag::new(s).unwrap()
188    }
189
190    fn tags(items: &[&str]) -> Tags {
191        Tags::new(items.iter().map(|s| tag(s)).collect::<SmallVec<[Tag; 4]>>()).unwrap()
192    }
193
194    #[test]
195    fn name_rejects_empty() {
196        assert_eq!(
197            EventType::new(""),
198            Err(NameError::Empty { what: "event type" })
199        );
200        assert_eq!(Tag::new(""), Err(NameError::Empty { what: "tag" }));
201    }
202
203    #[test]
204    fn name_rejects_over_long() {
205        let big = "x".repeat(MAX_NAME_LEN + 1);
206        assert_eq!(
207            EventType::new(&big),
208            Err(NameError::TooLong {
209                what: "event type",
210                len: MAX_NAME_LEN + 1,
211                max: MAX_NAME_LEN,
212            })
213        );
214        // Exactly at the maximum is accepted.
215        assert!(Tag::new("y".repeat(MAX_NAME_LEN)).is_ok());
216    }
217
218    #[test]
219    fn name_accessors_and_ordering() {
220        assert_eq!(EventType::new("Registered").unwrap().as_str(), "Registered");
221        assert_eq!(<Tag as AsRef<str>>::as_ref(&tag("course:c1")), "course:c1");
222        assert!(tag("course:a") < tag("course:b"));
223        assert_eq!(format!("{}", tag("student:s1")), "student:s1");
224    }
225
226    #[test]
227    fn tags_sorts_input() {
228        let t = tags(&["course:c1", "student:s1", "admin:a1"]);
229        let got: Vec<&str> = t.iter().map(|t| t.as_str()).collect();
230        assert_eq!(got, ["admin:a1", "course:c1", "student:s1"]);
231    }
232
233    #[test]
234    fn tags_rejects_duplicates() {
235        let input: SmallVec<[Tag; 4]> = [tag("course:c1"), tag("course:c1")].into_iter().collect();
236        assert_eq!(
237            Tags::new(input),
238            Err(TagsError::Duplicate {
239                tag: tag("course:c1")
240            })
241        );
242    }
243
244    #[test]
245    fn tags_empty_is_empty() {
246        assert!(Tags::empty().is_empty());
247        assert_eq!(Tags::empty().len(), 0);
248    }
249}