non/
lib.rs

1//! Type-safe wrappers for strings with compile-time guarantees.
2//!
3//! This crate provides zero-cost wrapper types around `String` that encode
4//! specific invariants at the type level, preventing invalid states at compile time.
5//!
6//! # Types
7//!
8//! - [`NonEmpty`] - A string that is guaranteed to contain at least one character
9//! - [`NonBlank`] - A string that is guaranteed to be non-empty and contain at least one non-whitespace character
10//! - [`ASCII`] - A string that is guaranteed to contain only ASCII characters
11//! - [`ExactLength`] - A string that is guaranteed to have exactly N characters
12//! - [`Trimmed`] - A string that is guaranteed to be trimmed (no leading or trailing whitespace)
13//! - [`Alphanumeric`] - A string that is guaranteed to only contain alphanumeric characters
14//! - [`LowerCase`] - A string that is guaranteed to be all lowercase
15//! - [`UpperCase`] - A string that is guaranteed all uppercase
16//!
17//! # Examples
18//!
19//! ```
20//! use non::{NonEmpty, NonBlank, ASCII, ExactLength};
21//!
22//! // NonEmpty rejects empty strings
23//! let valid = NonEmpty::new("hello".to_string()).unwrap();
24//! let invalid = NonEmpty::new(String::new());
25//! assert!(invalid.is_none());
26//!
27//! // NonBlank rejects whitespace-only strings
28//! let valid = NonBlank::new("hello".to_string()).unwrap();
29//! let invalid = NonBlank::new("   ".to_string());
30//! assert!(invalid.is_none());
31//!
32//! // ASCII rejects non-ASCII characters
33//! let valid = ASCII::new("hello".to_string()).unwrap();
34//! let invalid = ASCII::new("cześć".to_string());
35//! assert!(invalid.is_none());
36//!
37//! // ExactLength rejects strings that don't have exactly N chars
38//! let valid = ExactLength::<2>::new("hi".to_string()).unwrap();
39//! let invalid = ExactLength::<3>::new("helo world".to_string());
40//! assert!(invalid.is_none());
41//! ```
42//!
43//! # Deref Behavior
44//!
45//! All types implement `Deref<Target = String>`, giving you access to all `String` methods:
46//!
47//! ```
48//! use non::NonEmpty;
49//!
50//! let s = NonEmpty::new("hello world".to_string()).unwrap();
51//! assert_eq!(s.len(), 11);
52//! assert!(s.contains("world"));
53//! ```
54//!
55//! # Conversions
56//!
57//! All types can be converted back to `String` using `Into`:
58//!
59//! ```
60//! use non::NonEmpty;
61//!
62//! let non_empty = NonEmpty::new("hello".to_string()).unwrap();
63//! let s: String = non_empty.into();
64//! ```
65//!
66//! # Composing Wrappers
67//!
68//! You can compose multiple wrappers together:
69//!
70//! ```
71//! use non::{NonEmpty, ASCII};
72//!
73//! // Create a non-empty ASCII string
74//! let non_empty = NonEmpty::new("hello".to_string()).unwrap();
75//! let ascii_non_empty = ASCII::new(non_empty).unwrap();
76//!
77//! // Type is: ASCII<NonEmpty<String>>
78//! assert_eq!(ascii_non_empty.as_ref(), "hello");
79//! ```
80//!
81//! # Stripping Wrappers
82//!
83//! Use the [`Strip`] trait to remove one layer of wrapping:
84//!
85//! ```
86//! use non::{NonEmpty, ASCII, Strip};
87//!
88//! let non_empty = NonEmpty::new("hello".to_string()).unwrap();
89//! let ascii_non_empty = ASCII::new(non_empty).unwrap();
90//!
91//! // Strip the NonEmpty layer: ASCII<NonEmpty<String>> -> ASCII<String>
92//! let ascii_only = ascii_non_empty.strip();
93//! assert_eq!(ascii_only, ASCII::new("hello".to_string()).unwrap());
94//! ```
95//!
96//! # `no_std` Support
97//!
98//! This crate supports `no_std` environments with `alloc`. To use in `no_std`:
99//!
100//! ```toml
101//! [dependencies]
102//! non = { version = "0.1", default-features = false }
103//! ```
104//!
105//! # Features
106//!
107//! - `std` (default) - Enables standard library support
108
109#![cfg_attr(not(feature = "std"), no_std)]
110
111#[cfg(not(feature = "std"))]
112extern crate alloc;
113
114#[cfg(feature = "std")]
115use std::string::String;
116
117#[cfg(not(feature = "std"))]
118use alloc::string::String;
119
120#[cfg(feature = "std")]
121use std::fmt::{Display, Formatter, Result};
122
123#[cfg(not(feature = "std"))]
124use core::fmt::{Display, Formatter, Result};
125
126#[cfg(feature = "std")]
127use std::ops::Deref;
128
129#[cfg(not(feature = "std"))]
130use core::ops::Deref;
131
132/// Strips one layer of wrapper from nested types.
133///
134/// All wrapper types support `.strip()` to unwrap one layer.
135/// This is crucial for converting `Type<Wrapper<String>>` to `Type<String>`.
136///
137/// # Example
138///
139/// ```
140/// use non::{NonEmpty, Strip, ASCII};
141/// let non_empty = NonEmpty::new("hello".to_string()).unwrap();
142/// let ascii = ASCII::new(non_empty).unwrap();
143/// assert_eq!(
144///     ascii.strip(), // ASCII<NonEmpty<String>> -> ASCII<String>
145///     ASCII::new("hello".to_string()).unwrap()
146/// );
147/// ```
148pub trait Strip {
149    type Output;
150
151    fn strip(&self) -> Self::Output;
152}
153
154macro_rules! non {
155    (string, $name: ident<$param: ident>, $func: expr, $guarantee: expr, $guarantee2: expr) => {
156        #[doc = "A string that is known to "]
157        #[doc = $guarantee2]
158        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
159        #[repr(transparent)]
160        pub struct $name<$param = String>($param);
161
162        impl<$param> $name<$param>
163        where
164            $param: AsRef<str>
165        {
166            #[doc = "Creates a new `" ]
167            #[doc = stringify!($name) ]
168            #[doc = "` string if the string is "]
169            #[doc = $guarantee]
170            #[inline]
171            pub fn new(value: $param) -> Option<Self> {
172                if $func(&value) {
173                    Some($name(value))
174                } else {
175                    None
176                }
177            }
178
179            #[doc = "Creates a new `" ]
180            #[doc = stringify!($name) ]
181            #[doc = "` without checking the value."]
182            ///
183            /// # Safety
184            #[doc = "The value must be "]
185            #[doc = $guarantee]
186            #[inline]
187            pub unsafe fn new_unchecked(value: $param) -> Self {
188                Self(value)
189            }
190
191            /// Returns the inner value
192            #[inline]
193            pub fn into_inner(self) -> $param {
194                self.0
195            }
196
197            /// Gets a reference to the inner value
198            #[inline]
199            pub fn inner(&self) -> &$param {
200                &self.0
201            }
202        }
203
204        impl<$param> Deref for $name<$param> {
205            type Target = $param;
206
207            fn deref(&self) -> &Self::Target {
208                &self.0
209            }
210        }
211
212        impl<$param> From<$name<$param>> for String 
213        where
214            $param: Into<String>
215        {
216            fn from(value: $name<$param>) -> Self {
217                value.0.into()
218            }
219        }
220
221        impl<$param> AsRef<str> for $name<$param> 
222        where
223            $param: AsRef<str>
224        {
225            fn as_ref(&self) -> &str {
226                self.0.as_ref()
227            }
228        }
229
230        impl<$param> Display for $name<$param> 
231        where
232            $param: Display
233        {
234            fn fmt(&self, f: &mut Formatter<'_>) -> Result {
235                self.0.fmt(f)
236            }
237        }
238
239        impl<$param, Z> Strip for $name<$param>
240        where
241            $param: NotString + AsRef<str> + Deref<Target = Z>,
242            Z: AsRef<str> + Clone
243        {
244            type Output = $name<Z>;
245            fn strip(&self) -> Self::Output{
246                unsafe { $name::<Z>::new_unchecked((*self.0).clone()) }
247            }
248        }
249    };
250    (string, $name: ident<$param: ident, const $param2: ident: $param3: ident>, $func: expr, $guarantee: expr, $guarantee2: expr) => {
251        #[doc = "A string that is known to "]
252        #[doc = $guarantee2]
253        pub struct $name<const $param2: $param3, $param = String>($param);
254
255        impl<$param, const $param2: $param3> $name<$param2, $param>
256        where
257            $param: AsRef<str>
258        {
259            #[doc = "Creates a new `" ]
260            #[doc = stringify!($name) ]
261            #[doc = "` string if the string is "]
262            #[doc = $guarantee]
263            #[inline]
264            pub fn new(value: $param) -> Option<Self> {
265                if $func(&value) {
266                    Some($name(value))
267                } else {
268                    None
269                }
270            }
271
272            #[doc = "Creates a new `" ]
273            #[doc = stringify!($name) ]
274            #[doc = "` without checking the value."]
275            ///
276            /// # Safety
277            #[doc = "The value must be "]
278            #[doc = $guarantee]
279            #[inline]
280            pub unsafe fn new_unchecked(value: $param) -> Self {
281                Self(value)
282            }
283
284            /// Returns the inner value
285            #[inline]
286            pub fn into_inner(self) -> $param {
287                self.0
288            }
289
290            /// Gets a reference to the inner value
291            #[inline]
292            pub fn inner(&self) -> &$param {
293                &self.0
294            }
295        }
296
297        impl<$param, const $param2: $param3> Deref for $name<$param2, $param> {
298            type Target = $param;
299
300            fn deref(&self) -> &Self::Target {
301                &self.0
302            }
303        }
304
305        impl<$param, const $param2: $param3> From<$name<$param2, $param>> for String
306        where
307            $param: Into<String>
308        {
309            fn from(value: $name<$param2, $param>) -> Self {
310                value.0.into()
311            }
312        }
313
314        impl<$param, const $param2: $param3> AsRef<str> for $name<$param2, $param>
315        where
316            $param: AsRef<str>
317        {
318            fn as_ref(&self) -> &str {
319                self.0.as_ref()
320            }
321        }
322
323        impl<$param, const $param2: $param3> Display for $name<$param2, $param>
324        where
325            $param: Display
326        {
327            fn fmt(&self, f: &mut Formatter<'_>) -> Result {
328                self.0.fmt(f)
329            }
330        }
331
332        impl<$param, Z, const $param2: $param3> Strip for $name<$param2, $param>
333        where
334            $param: NotString + AsRef<str> + Deref<Target = Z>,
335            Z: AsRef<str> + Clone
336        {
337            type Output = $name<$param2, Z>;
338            fn strip(&self) -> Self::Output{
339                unsafe { $name::<$param2, Z>::new_unchecked((*self.0).clone()) }
340            }
341        }
342    };
343}
344
345mod sealed {
346    pub trait NotString {}
347    impl<T> NotString for T where T: AsRef<str> + Clone {}
348}
349use sealed::NotString;
350
351non!(string, NonEmpty<T>,|x: &T| !x.as_ref().is_empty(),"not empty", "not be empty");
352non!(string, NonBlank<T>,|x: &T| !x.as_ref().chars().all(|y| y.is_whitespace()),"not only whitespace", "not be only whitespace");
353non!(string, ASCII<T>,|x: &T| x.as_ref().is_ascii(),"ascii encoded", "be ascii encoded");
354non!(string, ExactLength<T, const N: usize>, |x: &T| x.as_ref().chars().count() == N, "exactly N characters long", "have exactly N characters");
355non!(string, LowerCase<T>, |x: &T| { let s = x.as_ref(); s.to_lowercase() == s }, "all lowercase", "be all lowercase");
356non!(string, UpperCase<T>, |x: &T| { let s = x.as_ref(); s.to_uppercase() == s }, "all uppercase", "be all uppercase");
357non!(string, Trimmed<T>, |x: &T| x.as_ref() == x.as_ref().trim(), "trimmed (no leading or trailing whitespace)", "be trimmed");
358non!(string, Alphanumeric<T>, |x: &T| x.as_ref().chars().all(|c| c.is_alphanumeric()), "only alphanumeric characters", "contain only alphanumeric characters");
359
360#[cfg(test)]
361mod test {
362    use super::*;
363
364    #[test]
365    fn test_non_empty_new_some() {
366        let s = String::from("helo world");
367        let non_empty = NonEmpty::new(s);
368        assert!(non_empty.is_some());
369    }
370    
371    #[test]
372    fn test_non_empty_new_none() {
373        let s = String::new();
374        let empty = NonEmpty::new(s);
375        assert!(empty.is_none());
376    }
377    
378    #[test]
379    fn test_non_empty_new_unchecked() {
380        let s = String::from("helo world");
381        let non_empty = unsafe { NonEmpty::new_unchecked(s.clone()) };
382        assert_eq!(*non_empty, s);
383    }
384    
385    #[test]
386    fn test_ascii_new_some() {
387        let s = String::from("helo world");
388        let ascii = ASCII::new(s);
389        assert!(ascii.is_some());
390    }
391    
392    #[test]
393    fn test_ascii_new_none() {
394        let s = String::from("cześć");
395        let ascii = ASCII::new(s);
396        assert!(ascii.is_none());
397    }
398    
399    #[test]
400    fn test_ascii_new_unchecked() {
401        let s = String::from("helo world");
402        let ascii = unsafe { ASCII::new_unchecked(s.clone()) };
403        assert_eq!(*ascii, s);
404    }
405    
406    #[test]
407    fn test_non_blank_new_some() {
408        let s = String::from("helo world");
409        let nonblank = NonBlank::new(s);
410        assert!(nonblank.is_some());
411    }
412    
413    #[test]
414    fn test_non_blank_new_none() {
415        let s = String::from("    \n");
416        let nonblank = NonBlank::new(s);
417        assert!(nonblank.is_none());
418    }
419    
420    #[test]
421    fn test_non_blank_new_unchecked() {
422        let s = String::from("helo world");
423        let nonblank = unsafe { NonBlank::new_unchecked(s.clone()) };
424        assert_eq!(*nonblank, s);
425    }
426
427    #[test]
428    fn test_exact_length_new_some() {
429        let s = String::from("hi");
430        let exact_length = ExactLength::<2>::new(s);
431        assert!(exact_length.is_some());
432    }
433
434    #[test]
435    fn test_exact_length_new_none() {
436        let s = String::from("helo world");
437        let exact_length = ExactLength::<2>::new(s);
438        assert!(exact_length.is_none());
439    }
440
441    #[test]
442    fn test_exact_length_new_unchecked() {
443        let s = String::from("hi!");
444        let exact_length = unsafe { ExactLength::<3>::new_unchecked(s.clone()) };
445        assert_eq!(*exact_length, s);
446    }
447    
448    #[test]
449    fn test_combinations() {
450        let s = String::from("helo world");
451        let nonblank =  NonBlank::new(s.clone());
452        assert!(nonblank.is_some());
453        let ascii = ASCII::new(nonblank.unwrap());
454        assert!(ascii.is_some());
455        let ascii = ascii.unwrap();
456        assert_eq!(ascii.as_ref(), s);
457    }
458
459    #[test]
460    fn test_strip() {
461        let s = String::from("helo world");
462        let nonblank = NonBlank::new(s.clone());
463        assert!(nonblank.is_some());
464        let ascii = ASCII::new(nonblank.unwrap());
465        assert!(ascii.is_some());
466        let ascii = ascii.unwrap();
467        unsafe { assert_eq!(ascii.strip(), ASCII::new_unchecked(s)); }
468    }
469}