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> {
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> FromStr for BoundedStr<MIN, MAX> {
109 type Err = PrimitiveError;
110
111 fn from_str(s: &str) -> Result<Self, Self::Err> {
112 Self::new(s)
113 }
114}
115
116impl<const MIN: usize, const MAX: usize> From<BoundedStr<MIN, MAX>> for String {
117 fn from(value: BoundedStr<MIN, MAX>) -> Self {
118 value.into_inner()
119 }
120}
121
122impl<const MIN: usize, const MAX: usize> PartialEq<str> for BoundedStr<MIN, MAX> {
123 fn eq(&self, other: &str) -> bool {
124 self.as_str() == other
125 }
126}
127
128impl<const MIN: usize, const MAX: usize> PartialEq<&str> for BoundedStr<MIN, MAX> {
129 fn eq(&self, other: &&str) -> bool {
130 self.as_str() == *other
131 }
132}
133
134impl<const MIN: usize, const MAX: usize> PartialEq<String> for BoundedStr<MIN, MAX> {
135 fn eq(&self, other: &String) -> bool {
136 self.as_str() == other.as_str()
137 }
138}
139
140impl<const MIN: usize, const MAX: usize> PartialEq<&String> for BoundedStr<MIN, MAX> {
141 fn eq(&self, other: &&String) -> bool {
142 self.as_str() == other.as_str()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::BoundedStr;
149 use crate::PrimitiveError;
150 use alloc::string::{String, ToString};
151
152 #[test]
153 fn accepts_valid_length() {
154 let value = BoundedStr::<3, 12>::new("service").unwrap();
155 assert_eq!(value.as_str(), "service");
156 assert_eq!(value.len(), 7);
157 assert_eq!(value.min_len(), 3);
158 assert_eq!(value.max_len(), 12);
159 }
160
161 #[test]
162 fn rejects_too_short() {
163 assert_eq!(
164 BoundedStr::<3, 12>::new("ab").unwrap_err(),
165 PrimitiveError::TooShort { min: 3, actual: 2 }
166 );
167 }
168
169 #[test]
170 fn rejects_too_long() {
171 assert_eq!(
172 BoundedStr::<3, 5>::new("service").unwrap_err(),
173 PrimitiveError::TooLong { max: 5, actual: 7 }
174 );
175 }
176
177 #[test]
178 fn counts_unicode_chars() {
179 let value = BoundedStr::<2, 2>::new("éå").unwrap();
180 assert_eq!(value.len(), 2);
181 assert_eq!(value.as_str().len(), 4);
182 }
183
184 #[test]
185 fn rejects_whitespace_only_when_min_positive() {
186 assert_eq!(
187 BoundedStr::<1, 5>::new(" ").unwrap_err(),
188 PrimitiveError::Empty
189 );
190 }
191
192 #[test]
193 fn handles_invalid_bounds() {
194 assert_eq!(
195 BoundedStr::<5, 3>::new("abcd").unwrap_err(),
196 PrimitiveError::OutOfRange {
197 min: 5,
198 max: 3,
199 actual: 4
200 }
201 );
202 }
203
204 #[test]
205 fn into_inner_returns_string() {
206 let value = BoundedStr::<3, 10>::new("hello").unwrap();
207 assert_eq!(value.into_inner(), "hello");
208 }
209
210 #[test]
211 fn is_empty_returns_false_for_valid() {
212 let value = BoundedStr::<3, 10>::new("hello").unwrap();
213 assert!(!value.is_empty());
214 }
215
216 #[test]
217 fn display_formats_inner_string() {
218 let value = BoundedStr::<3, 10>::new("hello").unwrap();
219 assert_eq!(value.to_string(), "hello");
220 }
221
222 #[test]
223 fn as_ref_returns_str() {
224 let value = BoundedStr::<3, 10>::new("hello").unwrap();
225 let s: &str = value.as_ref();
226 assert_eq!(s, "hello");
227 }
228
229 #[test]
230 fn deref_to_str() {
231 let value = BoundedStr::<3, 10>::new("hello").unwrap();
232 assert_eq!(&*value, "hello");
233 }
234
235 #[test]
236 fn try_from_string() {
237 let value = BoundedStr::<3, 10>::try_from(String::from("hello")).unwrap();
238 assert_eq!(value.as_str(), "hello");
239 }
240
241 #[test]
242 fn try_from_str_ref() {
243 let value = BoundedStr::<3, 10>::try_from("hello").unwrap();
244 assert_eq!(value.as_str(), "hello");
245 }
246
247 #[test]
248 fn from_bounded_str_into_string() {
249 let value = BoundedStr::<3, 10>::new("hello").unwrap();
250 let s = String::from(value);
251 assert_eq!(s, "hello");
252 }
253
254 #[test]
255 fn allows_zero_min_whitespace_only() {
256 let value = BoundedStr::<0, 5>::new(" ").unwrap();
257 assert_eq!(value.as_str(), " ");
258 }
259
260 #[test]
261 fn from_str_and_string_comparisons() {
262 let value = "hello".parse::<BoundedStr<3, 10>>().unwrap();
263 let owned = String::from("hello");
264 assert_eq!(value, "hello");
265 assert_eq!(value, owned);
266 assert!("hi".parse::<BoundedStr<3, 10>>().is_err());
267 }
268}