1use crate::{PrimitiveError, PrimitiveResult};
2use core::{
3 cmp::Ordering,
4 fmt,
5 hash::{Hash, Hasher},
6 ops::Deref,
7 str::FromStr,
8};
9
10#[derive(Clone, Copy)]
40pub struct InlineStr<const MIN: usize, const MAX: usize> {
41 buf: [u8; MAX],
42 len: usize,
43}
44
45impl<const MIN: usize, const MAX: usize> InlineStr<MIN, MAX> {
46 pub fn new(value: &str) -> PrimitiveResult<Self> {
53 let bytes = value.as_bytes();
54 let actual = bytes.len();
55
56 if MIN > MAX {
57 return Err(PrimitiveError::OutOfRange {
58 min: MIN as u128,
59 max: MAX as u128,
60 actual: actual as u128,
61 });
62 }
63 if actual < MIN {
64 return Err(PrimitiveError::TooShort { min: MIN, actual });
65 }
66 if actual > MAX {
67 return Err(PrimitiveError::TooLong { max: MAX, actual });
68 }
69 if MIN > 0 && value.trim().is_empty() {
70 return Err(PrimitiveError::Empty);
71 }
72
73 let mut buf = [0u8; MAX];
74 buf[..actual].copy_from_slice(bytes);
75 Ok(Self { buf, len: actual })
76 }
77
78 pub fn as_str(&self) -> &str {
80 core::str::from_utf8(self.as_bytes())
81 .expect("InlineStr always holds valid UTF-8 by construction")
82 }
83
84 pub fn as_bytes(&self) -> &[u8] {
86 &self.buf[..self.len]
87 }
88
89 pub fn len(&self) -> usize {
91 self.len
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.len == 0
97 }
98
99 pub fn min_len(&self) -> usize {
101 MIN
102 }
103
104 pub fn max_len(&self) -> usize {
106 MAX
107 }
108}
109
110impl<const MIN: usize, const MAX: usize> fmt::Display for InlineStr<MIN, MAX> {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116impl<const MIN: usize, const MAX: usize> fmt::Debug for InlineStr<MIN, MAX> {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 fmt::Debug::fmt(self.as_str(), f)
119 }
120}
121
122impl<const MIN: usize, const MAX: usize> AsRef<str> for InlineStr<MIN, MAX> {
123 fn as_ref(&self) -> &str {
124 self.as_str()
125 }
126}
127
128impl<const MIN: usize, const MAX: usize> Deref for InlineStr<MIN, MAX> {
129 type Target = str;
130
131 fn deref(&self) -> &Self::Target {
132 self.as_str()
133 }
134}
135
136impl<const MIN: usize, const MAX: usize> PartialEq for InlineStr<MIN, MAX> {
140 fn eq(&self, other: &Self) -> bool {
141 self.as_bytes() == other.as_bytes()
142 }
143}
144
145impl<const MIN: usize, const MAX: usize> Eq for InlineStr<MIN, MAX> {}
146
147impl<const MIN: usize, const MAX: usize> Hash for InlineStr<MIN, MAX> {
148 fn hash<H: Hasher>(&self, state: &mut H) {
149 self.as_bytes().hash(state);
150 }
151}
152
153impl<const MIN: usize, const MAX: usize> PartialOrd for InlineStr<MIN, MAX> {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 Some(self.cmp(other))
156 }
157}
158
159impl<const MIN: usize, const MAX: usize> Ord for InlineStr<MIN, MAX> {
160 fn cmp(&self, other: &Self) -> Ordering {
161 self.as_bytes().cmp(other.as_bytes())
162 }
163}
164
165impl<const MIN: usize, const MAX: usize> PartialEq<str> for InlineStr<MIN, MAX> {
166 fn eq(&self, other: &str) -> bool {
167 self.as_bytes() == other.as_bytes()
168 }
169}
170
171impl<const MIN: usize, const MAX: usize> PartialEq<&str> for InlineStr<MIN, MAX> {
172 fn eq(&self, other: &&str) -> bool {
173 self.as_bytes() == other.as_bytes()
174 }
175}
176
177impl<const MIN: usize, const MAX: usize> TryFrom<&str> for InlineStr<MIN, MAX> {
178 type Error = PrimitiveError;
179
180 fn try_from(value: &str) -> Result<Self, Self::Error> {
181 Self::new(value)
182 }
183}
184
185impl<const MIN: usize, const MAX: usize> FromStr for InlineStr<MIN, MAX> {
186 type Err = PrimitiveError;
187
188 fn from_str(s: &str) -> Result<Self, Self::Err> {
189 Self::new(s)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::InlineStr;
196 use crate::PrimitiveError;
197
198 #[test]
199 fn accepts_valid_length() {
200 let value = InlineStr::<3, 12>::new("service").unwrap();
201 assert_eq!(value.as_str(), "service");
202 assert_eq!(value.len(), 7);
203 assert_eq!(value.min_len(), 3);
204 assert_eq!(value.max_len(), 12);
205 assert!(!value.is_empty());
206 }
207
208 #[test]
209 fn rejects_too_short() {
210 assert_eq!(
211 InlineStr::<3, 12>::new("ab").unwrap_err(),
212 PrimitiveError::TooShort { min: 3, actual: 2 }
213 );
214 }
215
216 #[test]
217 fn rejects_too_long() {
218 assert_eq!(
219 InlineStr::<1, 5>::new("service").unwrap_err(),
220 PrimitiveError::TooLong { max: 5, actual: 7 }
221 );
222 }
223
224 #[test]
225 fn counts_bytes_not_chars() {
226 let value = InlineStr::<4, 4>::new("éå").unwrap();
228 assert_eq!(value.len(), 4);
229 assert_eq!(value.as_str(), "éå");
230 assert_eq!(
232 InlineStr::<1, 1>::new("é").unwrap_err(),
233 PrimitiveError::TooLong { max: 1, actual: 2 }
234 );
235 }
236
237 #[test]
238 fn rejects_whitespace_only_when_min_positive() {
239 assert_eq!(
240 InlineStr::<1, 5>::new(" ").unwrap_err(),
241 PrimitiveError::Empty
242 );
243 }
244
245 #[test]
246 fn allows_zero_min_whitespace_only() {
247 let value = InlineStr::<0, 5>::new(" ").unwrap();
248 assert_eq!(value.as_str(), " ");
249 assert_eq!(value.len(), 3);
250 }
251
252 #[test]
253 fn allows_empty_when_min_zero() {
254 let value = InlineStr::<0, 4>::new("").unwrap();
255 assert!(value.is_empty());
256 assert_eq!(value.as_str(), "");
257 }
258
259 #[test]
260 fn zero_capacity_only_holds_empty() {
261 assert!(InlineStr::<0, 0>::new("").unwrap().is_empty());
262 assert_eq!(
263 InlineStr::<0, 0>::new("x").unwrap_err(),
264 PrimitiveError::TooLong { max: 0, actual: 1 }
265 );
266 }
267
268 #[test]
269 fn handles_invalid_bounds() {
270 assert_eq!(
271 InlineStr::<5, 3>::new("abcd").unwrap_err(),
272 PrimitiveError::OutOfRange {
273 min: 5,
274 max: 3,
275 actual: 4
276 }
277 );
278 }
279
280 #[test]
281 fn exact_min_and_max_fit() {
282 assert_eq!(InlineStr::<3, 3>::new("abc").unwrap().len(), 3);
283 assert!(InlineStr::<3, 3>::new("ab").is_err());
284 assert!(InlineStr::<3, 3>::new("abcd").is_err());
285 }
286
287 #[test]
288 fn is_copy() {
289 let a = InlineStr::<1, 8>::new("copy").unwrap();
290 let b = a;
291 assert_eq!(a.as_str(), "copy");
293 assert_eq!(b.as_str(), "copy");
294 }
295
296 #[test]
297 fn as_bytes_excludes_unused_tail() {
298 let value = InlineStr::<1, 16>::new("hi").unwrap();
299 assert_eq!(value.as_bytes(), b"hi");
300 }
301
302 #[test]
303 fn deref_and_as_ref_to_str() {
304 let value = InlineStr::<1, 8>::new("api").unwrap();
305 let s: &str = value.as_ref();
306 assert_eq!(s, "api");
307 assert!(value.starts_with("ap"));
309 }
310
311 #[test]
312 fn equality_and_ordering() {
313 let a = InlineStr::<1, 8>::new("alpha").unwrap();
314 let b = InlineStr::<1, 8>::new("beta").unwrap();
315 assert_eq!(a, InlineStr::<1, 8>::new("alpha").unwrap());
316 assert_ne!(a, b);
317 assert!(a < b);
318 assert_eq!(a, "alpha");
319 }
320
321 #[test]
322 fn from_str_and_try_from() {
323 let parsed: InlineStr<1, 8> = "node".parse().unwrap();
324 assert_eq!(parsed.as_str(), "node");
325 let converted = InlineStr::<1, 8>::try_from("node").unwrap();
326 assert_eq!(parsed, converted);
327 assert!("toolongforbudget".parse::<InlineStr<1, 8>>().is_err());
328 }
329
330 #[test]
331 fn display_and_debug() {
332 use alloc::string::ToString;
333 let value = InlineStr::<3, 12>::new("service").unwrap();
334 assert_eq!(value.to_string(), "service");
335 assert_eq!(alloc::format!("{value:?}"), "\"service\"");
336 }
337
338 #[test]
339 fn hash_distinguishes_contents() {
340 use core::hash::{Hash, Hasher};
341
342 fn fnv(value: &InlineStr<1, 8>) -> u64 {
345 struct Fnv(u64);
346 impl Hasher for Fnv {
347 fn finish(&self) -> u64 {
348 self.0
349 }
350 fn write(&mut self, bytes: &[u8]) {
351 for &b in bytes {
352 self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
353 }
354 }
355 }
356 let mut hasher = Fnv(0xcbf2_9ce4_8422_2325);
357 value.hash(&mut hasher);
358 hasher.finish()
359 }
360
361 let a = InlineStr::<1, 8>::new("alpha").unwrap();
362 assert_eq!(fnv(&a), fnv(&InlineStr::<1, 8>::new("alpha").unwrap()));
363 assert_ne!(fnv(&a), fnv(&InlineStr::<1, 8>::new("beta").unwrap()));
365 }
366
367 #[test]
368 fn str_equality_both_impls() {
369 let a = InlineStr::<1, 8>::new("alpha").unwrap();
370 assert!(a == "alpha");
372 assert!(a != "other");
373 fn eq_str<T: PartialEq<str> + Copy>(value: T, s: &str) -> bool {
375 value == *s
376 }
377 assert!(eq_str(a, "alpha"));
378 assert!(!eq_str(a, "other"));
379 }
380}