Skip to main content

tempest_core/
tempest_str.rs

1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4
5use crate::utils::contains_null;
6
7/// Error returned when constructing a [`TempestStr`] from invalid input.
8#[derive(Debug, Display, Error)]
9pub enum TempestStrError {
10    /// The input string contains a null byte (`\x00`), which is reserved
11    /// for use as a key component terminator in the lexical encoding scheme.
12    InputContainsNullByte,
13}
14
15/// A validated, potentially borrowed string for use as a Tempest identifier
16/// (database name, table name, etc.).
17///
18/// Null bytes are rejected at construction time, since they are reserved as
19/// terminators in the lexical key encoding scheme. This keeps the encoding
20/// layer simple - identifier components can be written as raw bytes with no
21/// escaping required.
22///
23/// Borrows the input when possible (`Cow::Borrowed`) to avoid unnecessary
24/// allocations. Use [`into_owned`] to promote to a `'static` lifetime.
25///
26/// [`into_owned`]: TempestStr::into_owned
27#[derive(
28    Debug, Display, Clone, Deref, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
29)]
30pub struct TempestStr<'a>(Cow<'a, str>);
31
32impl<'a> TempestStr<'a> {
33    /// Constructs a [`TempestStr`] borrowing from `s`.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`TempestStrError::InputContainsNullByte`] if `s` contains a null byte.
38    pub fn from_borrowed(s: &'a str) -> Result<TempestStr<'a>, TempestStrError> {
39        if contains_null(s) {
40            return Err(TempestStrError::InputContainsNullByte);
41        }
42        Ok(TempestStr(s.into()))
43    }
44
45    /// Constructs a [`TempestStr`] borrowing from `s`.
46    ///
47    /// # Safety
48    ///
49    /// The caller must ensure that `s` does not contain any null byte.
50    pub const unsafe fn from_borrowed_unchecked(s: &'a str) -> TempestStr<'a> {
51        Self(Cow::Borrowed(s))
52    }
53
54    /// Constructs a [`TempestStr`] from an owned [`String`], yielding a `'static` lifetime.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`TempestStrError::InputContainsNullByte`] if `s` contains a null byte.
59    pub fn from_owned(s: String) -> Result<TempestStr<'static>, TempestStrError> {
60        if contains_null(&s) {
61            return Err(TempestStrError::InputContainsNullByte);
62        }
63        Ok(TempestStr(s.into()))
64    }
65
66    /// Converts this [`TempestStr`] into an owned `'static` version, allocating
67    /// if the inner value is currently borrowed. No-op if it is already owned.
68    pub fn into_owned(self) -> TempestStr<'static> {
69        TempestStr(self.0.into_owned().into())
70    }
71}
72
73impl TryFrom<String> for TempestStr<'static> {
74    type Error = TempestStrError;
75
76    fn try_from(s: String) -> Result<Self, Self::Error> {
77        TempestStr::from_owned(s)
78    }
79}
80
81// NB: this is for test helpers and internal use where input is known-safe.
82// it is okay to panic, since the transform is deterministic and would always fail,
83// which is a logic error on the programmers side - my side :P
84impl From<&'static str> for TempestStr<'static> {
85    fn from(s: &'static str) -> Self {
86        TempestStr::from_borrowed(s).expect("static str must not contain null bytes")
87    }
88}
89
90impl From<u32> for TempestStr<'static> {
91    fn from(value: u32) -> Self {
92        TempestStr::from_owned(value.to_string())
93            .expect("stringified number does not contain null bytes")
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn assert_valid(s: &str) {
102        let ts = match TempestStr::from_borrowed(s) {
103            Ok(ts) => ts,
104            Err(e) => panic!("{} should be valid, but got error: {}", s, e),
105        };
106        assert_eq!(s, *ts);
107    }
108
109    fn assert_invalid(s: &str) {
110        assert!(
111            TempestStr::from_borrowed(s).is_err(),
112            "{} should not be valid",
113            s
114        );
115        assert!(
116            TempestStr::from_owned(s.to_owned()).is_err(),
117            "{} should not be valid, when converting into owned",
118            s
119        );
120    }
121
122    #[test]
123    fn test_tempest_str_valid() {
124        [
125            "sup",
126            "world",
127            "", // empty should also be valid
128            "tempest",
129            "juice",
130            "eventual consistency",
131        ]
132        .into_iter()
133        .for_each(assert_valid);
134    }
135
136    #[test]
137    fn test_tempest_str_invalid() {
138        [
139            "\x00",
140            "\x00hello",
141            "hel\x00lo",
142            "hello\x00",
143            "\x00\x00\x00",
144        ]
145        .into_iter()
146        .for_each(assert_invalid);
147    }
148
149    #[test]
150    fn test_into_owned_produces_static_lifetime() {
151        let s = String::from("hello");
152        let ts: TempestStr<'static> = TempestStr::from_owned(s).unwrap().into_owned();
153        assert_eq!(*ts, "hello");
154    }
155}