1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::String;
3use core::{fmt, hash::Hash, ops::Deref, str::FromStr};
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> {
17 let value = value.into();
18 let actual = value.chars().count();
19
20 if MIN > MAX {
21 return Err(PrimitiveError::OutOfRange {
22 min: MIN as u128,
23 max: MAX as u128,
24 actual: actual as u128,
25 });
26 }
27
28 if actual < MIN {
29 return Err(PrimitiveError::TooShort { min: MIN, actual });
30 }
31
32 if actual > MAX {
33 return Err(PrimitiveError::TooLong { max: MAX, actual });
34 }
35
36 if MIN > 0 && value.trim().is_empty() {
37 return Err(PrimitiveError::Empty);
38 }
39
40 Ok(Self(value))
41 }
42
43 pub fn as_str(&self) -> &str {
45 &self.0
46 }
47
48 pub fn into_inner(self) -> String {
50 self.0
51 }
52
53 pub fn len(&self) -> usize {
55 self.0.chars().count()
56 }
57
58 pub fn is_empty(&self) -> bool {
60 self.0.is_empty()
61 }
62
63 pub fn min_len(&self) -> usize {
65 MIN
66 }
67
68 pub fn max_len(&self) -> usize {
70 MAX
71 }
72}
73
74impl<const MIN: usize, const MAX: usize> fmt::Display for BoundedStr<MIN, MAX> {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(&self.0)
77 }
78}
79
80impl<const MIN: usize, const MAX: usize> AsRef<str> for BoundedStr<MIN, MAX> {
81 fn as_ref(&self) -> &str {
82 self.as_str()
83 }
84}
85
86impl<const MIN: usize, const MAX: usize> Deref for BoundedStr<MIN, MAX> {
87 type Target = str;
88
89 fn deref(&self) -> &Self::Target {
90 self.as_str()
91 }
92}
93
94impl<const MIN: usize, const MAX: usize> TryFrom<String> for BoundedStr<MIN, MAX> {
95 type Error = PrimitiveError;
96
97 fn try_from(value: String) -> Result<Self, Self::Error> {
98 Self::new(value)
99 }
100}
101
102impl<const MIN: usize, const MAX: usize> TryFrom<&str> for BoundedStr<MIN, MAX> {
103 type Error = PrimitiveError;
104
105 fn try_from(value: &str) -> Result<Self, Self::Error> {
106 Self::new(value)
107 }
108}
109
110impl<const MIN: usize, const MAX: usize> FromStr for BoundedStr<MIN, MAX> {
111 type Err = PrimitiveError;
112
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
114 Self::new(s)
115 }
116}
117
118impl<const MIN: usize, const MAX: usize> From<BoundedStr<MIN, MAX>> for String {
119 fn from(value: BoundedStr<MIN, MAX>) -> Self {
120 value.into_inner()
121 }
122}
123
124impl<const MIN: usize, const MAX: usize> PartialEq<str> for BoundedStr<MIN, MAX> {
125 fn eq(&self, other: &str) -> bool {
126 self.as_str() == other
127 }
128}
129
130impl<const MIN: usize, const MAX: usize> PartialEq<&str> for BoundedStr<MIN, MAX> {
131 fn eq(&self, other: &&str) -> bool {
132 self.as_str() == *other
133 }
134}
135
136impl<const MIN: usize, const MAX: usize> PartialEq<String> for BoundedStr<MIN, MAX> {
137 fn eq(&self, other: &String) -> bool {
138 self.as_str() == other.as_str()
139 }
140}
141
142impl<const MIN: usize, const MAX: usize> PartialEq<&String> for BoundedStr<MIN, MAX> {
143 fn eq(&self, other: &&String) -> bool {
144 self.as_str() == other.as_str()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::BoundedStr;
151 use crate::PrimitiveError;
152 use alloc::string::{String, ToString};
153
154 #[test]
155 fn accepts_valid_length() {
156 let value = BoundedStr::<3, 12>::new("service").unwrap();
157 assert_eq!(value.as_str(), "service");
158 assert_eq!(value.len(), 7);
159 assert_eq!(value.min_len(), 3);
160 assert_eq!(value.max_len(), 12);
161 }
162
163 #[test]
164 fn rejects_too_short() {
165 assert_eq!(
166 BoundedStr::<3, 12>::new("ab").unwrap_err(),
167 PrimitiveError::TooShort { min: 3, actual: 2 }
168 );
169 }
170
171 #[test]
172 fn rejects_too_long() {
173 assert_eq!(
174 BoundedStr::<3, 5>::new("service").unwrap_err(),
175 PrimitiveError::TooLong { max: 5, actual: 7 }
176 );
177 }
178
179 #[test]
180 fn counts_unicode_chars() {
181 let value = BoundedStr::<2, 2>::new("éå").unwrap();
182 assert_eq!(value.len(), 2);
183 assert_eq!(value.as_str().len(), 4);
184 }
185
186 #[test]
187 fn rejects_whitespace_only_when_min_positive() {
188 assert_eq!(
189 BoundedStr::<1, 5>::new(" ").unwrap_err(),
190 PrimitiveError::Empty
191 );
192 }
193
194 #[test]
195 fn handles_invalid_bounds() {
196 assert_eq!(
197 BoundedStr::<5, 3>::new("abcd").unwrap_err(),
198 PrimitiveError::OutOfRange {
199 min: 5,
200 max: 3,
201 actual: 4
202 }
203 );
204 }
205
206 #[test]
207 fn into_inner_returns_string() {
208 let value = BoundedStr::<3, 10>::new("hello").unwrap();
209 assert_eq!(value.into_inner(), "hello");
210 }
211
212 #[test]
213 fn is_empty_returns_false_for_valid() {
214 let value = BoundedStr::<3, 10>::new("hello").unwrap();
215 assert!(!value.is_empty());
216 }
217
218 #[test]
219 fn display_formats_inner_string() {
220 let value = BoundedStr::<3, 10>::new("hello").unwrap();
221 assert_eq!(value.to_string(), "hello");
222 }
223
224 #[test]
225 fn as_ref_returns_str() {
226 let value = BoundedStr::<3, 10>::new("hello").unwrap();
227 let s: &str = value.as_ref();
228 assert_eq!(s, "hello");
229 }
230
231 #[test]
232 fn deref_to_str() {
233 let value = BoundedStr::<3, 10>::new("hello").unwrap();
234 assert_eq!(&*value, "hello");
235 }
236
237 #[test]
238 fn try_from_string() {
239 let value = BoundedStr::<3, 10>::try_from(String::from("hello")).unwrap();
240 assert_eq!(value.as_str(), "hello");
241 }
242
243 #[test]
244 fn try_from_str_ref() {
245 let value = BoundedStr::<3, 10>::try_from("hello").unwrap();
246 assert_eq!(value.as_str(), "hello");
247 }
248
249 #[test]
250 fn from_bounded_str_into_string() {
251 let value = BoundedStr::<3, 10>::new("hello").unwrap();
252 let s = String::from(value);
253 assert_eq!(s, "hello");
254 }
255
256 #[test]
257 fn allows_zero_min_whitespace_only() {
258 let value = BoundedStr::<0, 5>::new(" ").unwrap();
259 assert_eq!(value.as_str(), " ");
260 }
261
262 #[test]
263 fn from_str_and_string_comparisons() {
264 let value = "hello".parse::<BoundedStr<3, 10>>().unwrap();
265 let owned = String::from("hello");
266 assert_eq!(value, "hello");
267 assert_eq!(value, owned);
268 assert!("hi".parse::<BoundedStr<3, 10>>().is_err());
269 }
270}