Skip to main content

rama_utils/str/
non_empty.rs

1use core::{fmt, ops::Deref};
2
3use crate::{std::string::String, str::arcstr::ArcStr};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
6/// A read-only string which can never be empty.
7pub struct NonEmptyStr(ArcStr);
8
9impl NonEmptyStr {
10    /// Create a new [`NonEmptyStr`] without checking if it's empty or not.
11    ///
12    /// # Safety
13    ///
14    /// Callee guarantees the given ArcStr is empty.
15    ///
16    /// Usually this is not what you want, but it can be userful to have
17    /// in cases where you already have an [`ArcStr`] and have checked that
18    /// it is not empty. Examples are cases where you decoded data from the wire,
19    /// or constructed it at compile time with a literal that you length checked.
20    #[must_use]
21    pub const unsafe fn new_unchecked(s: ArcStr) -> Self {
22        Self(s)
23    }
24}
25
26impl AsRef<str> for NonEmptyStr {
27    fn as_ref(&self) -> &str {
28        self.0.as_str()
29    }
30}
31
32impl Deref for NonEmptyStr {
33    type Target = str;
34
35    #[inline(always)]
36    fn deref(&self) -> &Self::Target {
37        self.0.as_str()
38    }
39}
40
41crate::macros::error::static_str_error! {
42    #[doc = "empty string"]
43    pub struct EmptyStrErr;
44}
45
46/// Create a [`NonEmptyStr`] at compile time from a literal
47///
48/// # Panics
49///
50/// Panics in case the literal is empty.
51#[macro_export]
52#[doc(hidden)]
53macro_rules! __non_empty_str {
54    ($text:expr $(,)?) => {{
55        if ($text).is_empty() {
56            panic!("literal is empty");
57        }
58        // SAFETY: above check satisfied the contract
59        unsafe { $crate::str::NonEmptyStr::new_unchecked($crate::str::arcstr::arcstr!($text)) }
60    }};
61}
62
63impl fmt::Display for NonEmptyStr {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        self.0.fmt(f)
66    }
67}
68
69impl From<NonEmptyStr> for String {
70    fn from(value: NonEmptyStr) -> Self {
71        value.0.to_string()
72    }
73}
74
75impl TryFrom<String> for NonEmptyStr {
76    type Error = EmptyStrErr;
77
78    fn try_from(value: String) -> Result<Self, Self::Error> {
79        if value.is_empty() {
80            Err(Self::Error::default())
81        } else {
82            Ok(Self(value.into()))
83        }
84    }
85}
86
87impl TryFrom<ArcStr> for NonEmptyStr {
88    type Error = EmptyStrErr;
89
90    fn try_from(value: ArcStr) -> Result<Self, Self::Error> {
91        if value.is_empty() {
92            Err(Self::Error::default())
93        } else {
94            Ok(Self(value))
95        }
96    }
97}
98
99impl TryFrom<&String> for NonEmptyStr {
100    type Error = EmptyStrErr;
101
102    fn try_from(value: &String) -> Result<Self, Self::Error> {
103        if value.is_empty() {
104            Err(Self::Error::default())
105        } else {
106            Ok(Self(value.into()))
107        }
108    }
109}
110
111impl TryFrom<&str> for NonEmptyStr {
112    type Error = EmptyStrErr;
113
114    fn try_from(value: &str) -> Result<Self, Self::Error> {
115        if value.is_empty() {
116            Err(Self::Error::default())
117        } else {
118            Ok(Self(value.into()))
119        }
120    }
121}
122
123impl core::str::FromStr for NonEmptyStr {
124    type Err = EmptyStrErr;
125
126    #[inline(always)]
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        s.try_into()
129    }
130}
131
132impl PartialEq<str> for NonEmptyStr {
133    fn eq(&self, other: &str) -> bool {
134        self.0 == other
135    }
136}
137
138impl PartialEq<String> for NonEmptyStr {
139    fn eq(&self, other: &String) -> bool {
140        self.0.as_str() == other
141    }
142}
143
144impl PartialEq<&String> for NonEmptyStr {
145    fn eq(&self, other: &&String) -> bool {
146        self.0.as_str() == *other
147    }
148}
149
150impl PartialEq<&str> for NonEmptyStr {
151    fn eq(&self, other: &&str) -> bool {
152        self.0 == *other
153    }
154}
155
156impl PartialEq<NonEmptyStr> for str {
157    fn eq(&self, other: &NonEmptyStr) -> bool {
158        other == self
159    }
160}
161
162impl PartialEq<NonEmptyStr> for String {
163    fn eq(&self, other: &NonEmptyStr) -> bool {
164        other == self
165    }
166}
167
168impl PartialEq<NonEmptyStr> for &String {
169    #[inline(always)]
170    fn eq(&self, other: &NonEmptyStr) -> bool {
171        other == *self
172    }
173}
174
175impl PartialEq<NonEmptyStr> for &str {
176    #[inline(always)]
177    fn eq(&self, other: &NonEmptyStr) -> bool {
178        other == *self
179    }
180}
181
182impl serde::Serialize for NonEmptyStr {
183    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
184    where
185        S: serde::Serializer,
186    {
187        self.0.serialize(serializer)
188    }
189}
190
191impl<'de> serde::Deserialize<'de> for NonEmptyStr {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: serde::Deserializer<'de>,
195    {
196        let s = ArcStr::deserialize(deserializer)?;
197        if s.is_empty() {
198            return Err(serde::de::Error::custom(EmptyStrErr::default()));
199        }
200        Ok(Self(s))
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    fn assert_try_into_err(src: impl TryInto<NonEmptyStr>) {
209        _ = src.try_into().unwrap_err();
210    }
211
212    #[cfg(not(loom))]
213    fn assert_try_into_ok<S>(src: S)
214    where
215        S: TryInto<NonEmptyStr, Error: std::error::Error>
216            + fmt::Debug
217            + Clone
218            + PartialEq<NonEmptyStr>,
219    {
220        let expected = src.clone();
221        let value: NonEmptyStr = src.try_into().unwrap();
222        assert_eq!(expected, value);
223    }
224
225    #[test]
226    fn test_non_empty_string_construction_failure() {
227        assert_try_into_err("");
228        assert_try_into_err(String::from(""));
229        #[expect(
230            clippy::needless_borrows_for_generic_args,
231            reason = "exercise &String→TryInto path"
232        )]
233        assert_try_into_err(&String::from(""));
234    }
235
236    #[test]
237    #[cfg(not(loom))]
238    fn test_non_empty_string_construction_success() {
239        assert_try_into_ok("a");
240        assert_try_into_ok(String::from("b"));
241        #[expect(
242            clippy::needless_borrows_for_generic_args,
243            reason = "exercise &String→TryInto path"
244        )]
245        assert_try_into_ok(&String::from("c"));
246    }
247
248    #[test]
249    #[cfg(not(loom))]
250    fn test_serde_json_compat() {
251        let source = r##"{"greeting": "Hello", "language": "en"}"##.to_owned();
252
253        #[derive(Debug, serde::Deserialize)]
254        struct Test {
255            greeting: NonEmptyStr,
256            language: NonEmptyStr,
257        }
258
259        let test: Test = serde_json::from_str(&source).unwrap();
260        assert_eq!("Hello", test.greeting);
261        assert_eq!("en", test.language);
262    }
263}