reliakit_primitives/
bounded.rs1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::String;
3use core::{fmt, hash::Hash, ops::Deref};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct BoundedStr<const MIN: usize, const MAX: usize>(String);
8
9impl<const MIN: usize, const MAX: usize> BoundedStr<MIN, MAX> {
10 pub fn new(value: impl Into<String>) -> PrimitiveResult<Self> {
15 let value = value.into();
16 let actual = value.chars().count();
17
18 if MIN > MAX {
19 return Err(PrimitiveError::OutOfRange {
20 min: MIN as u128,
21 max: MAX as u128,
22 actual: actual as u128,
23 });
24 }
25
26 if actual < MIN {
27 return Err(PrimitiveError::TooShort { min: MIN, actual });
28 }
29
30 if actual > MAX {
31 return Err(PrimitiveError::TooLong { max: MAX, actual });
32 }
33
34 if MIN > 0 && value.trim().is_empty() {
35 return Err(PrimitiveError::Empty);
36 }
37
38 Ok(Self(value))
39 }
40
41 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45
46 pub fn into_inner(self) -> String {
48 self.0
49 }
50
51 pub fn len(&self) -> usize {
53 self.0.chars().count()
54 }
55
56 pub fn is_empty(&self) -> bool {
58 self.0.is_empty()
59 }
60
61 pub fn min_len(&self) -> usize {
63 MIN
64 }
65
66 pub fn max_len(&self) -> usize {
68 MAX
69 }
70}
71
72impl<const MIN: usize, const MAX: usize> fmt::Display for BoundedStr<MIN, MAX> {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(&self.0)
75 }
76}
77
78impl<const MIN: usize, const MAX: usize> AsRef<str> for BoundedStr<MIN, MAX> {
79 fn as_ref(&self) -> &str {
80 self.as_str()
81 }
82}
83
84impl<const MIN: usize, const MAX: usize> Deref for BoundedStr<MIN, MAX> {
85 type Target = str;
86
87 fn deref(&self) -> &Self::Target {
88 self.as_str()
89 }
90}
91
92impl<const MIN: usize, const MAX: usize> TryFrom<String> for BoundedStr<MIN, MAX> {
93 type Error = PrimitiveError;
94
95 fn try_from(value: String) -> Result<Self, Self::Error> {
96 Self::new(value)
97 }
98}
99
100impl<const MIN: usize, const MAX: usize> TryFrom<&str> for BoundedStr<MIN, MAX> {
101 type Error = PrimitiveError;
102
103 fn try_from(value: &str) -> Result<Self, Self::Error> {
104 Self::new(value)
105 }
106}
107
108impl<const MIN: usize, const MAX: usize> From<BoundedStr<MIN, MAX>> for String {
109 fn from(value: BoundedStr<MIN, MAX>) -> Self {
110 value.into_inner()
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::BoundedStr;
117 use crate::PrimitiveError;
118 use alloc::string::{String, ToString};
119
120 #[test]
121 fn accepts_valid_length() {
122 let value = BoundedStr::<3, 12>::new("service").unwrap();
123 assert_eq!(value.as_str(), "service");
124 assert_eq!(value.len(), 7);
125 assert_eq!(value.min_len(), 3);
126 assert_eq!(value.max_len(), 12);
127 }
128
129 #[test]
130 fn rejects_too_short() {
131 assert_eq!(
132 BoundedStr::<3, 12>::new("ab").unwrap_err(),
133 PrimitiveError::TooShort { min: 3, actual: 2 }
134 );
135 }
136
137 #[test]
138 fn rejects_too_long() {
139 assert_eq!(
140 BoundedStr::<3, 5>::new("service").unwrap_err(),
141 PrimitiveError::TooLong { max: 5, actual: 7 }
142 );
143 }
144
145 #[test]
146 fn counts_unicode_chars() {
147 let value = BoundedStr::<2, 2>::new("éå").unwrap();
148 assert_eq!(value.len(), 2);
149 assert_eq!(value.as_str().len(), 4);
150 }
151
152 #[test]
153 fn rejects_whitespace_only_when_min_positive() {
154 assert_eq!(
155 BoundedStr::<1, 5>::new(" ").unwrap_err(),
156 PrimitiveError::Empty
157 );
158 }
159
160 #[test]
161 fn handles_invalid_bounds() {
162 assert_eq!(
163 BoundedStr::<5, 3>::new("abcd").unwrap_err(),
164 PrimitiveError::OutOfRange {
165 min: 5,
166 max: 3,
167 actual: 4
168 }
169 );
170 }
171
172 #[test]
173 fn into_inner_returns_string() {
174 let value = BoundedStr::<3, 10>::new("hello").unwrap();
175 assert_eq!(value.into_inner(), "hello");
176 }
177
178 #[test]
179 fn is_empty_returns_false_for_valid() {
180 let value = BoundedStr::<3, 10>::new("hello").unwrap();
181 assert!(!value.is_empty());
182 }
183
184 #[test]
185 fn display_formats_inner_string() {
186 let value = BoundedStr::<3, 10>::new("hello").unwrap();
187 assert_eq!(value.to_string(), "hello");
188 }
189
190 #[test]
191 fn as_ref_returns_str() {
192 let value = BoundedStr::<3, 10>::new("hello").unwrap();
193 let s: &str = value.as_ref();
194 assert_eq!(s, "hello");
195 }
196
197 #[test]
198 fn deref_to_str() {
199 let value = BoundedStr::<3, 10>::new("hello").unwrap();
200 assert_eq!(&*value, "hello");
201 }
202
203 #[test]
204 fn try_from_string() {
205 let value = BoundedStr::<3, 10>::try_from(String::from("hello")).unwrap();
206 assert_eq!(value.as_str(), "hello");
207 }
208
209 #[test]
210 fn try_from_str_ref() {
211 let value = BoundedStr::<3, 10>::try_from("hello").unwrap();
212 assert_eq!(value.as_str(), "hello");
213 }
214
215 #[test]
216 fn from_bounded_str_into_string() {
217 let value = BoundedStr::<3, 10>::new("hello").unwrap();
218 let s = String::from(value);
219 assert_eq!(s, "hello");
220 }
221
222 #[test]
223 fn allows_zero_min_whitespace_only() {
224 let value = BoundedStr::<0, 5>::new(" ").unwrap();
225 assert_eq!(value.as_str(), " ");
226 }
227}