cli_ui/prompt/
validate.rs1use std::sync::Arc;
44
45pub trait Validate: Send + Sync + 'static {
50 fn check(&self, value: &str) -> Result<(), String>;
53}
54
55impl<F> Validate for F
56where
57 F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
58{
59 fn check(&self, value: &str) -> Result<(), String> {
60 self(value)
61 }
62}
63
64#[derive(Clone)]
67pub struct Validator(Arc<dyn Validate>);
68
69impl Validator {
70 pub fn new<V: Validate>(v: V) -> Self {
73 Self(Arc::new(v))
74 }
75
76 pub fn check(&self, v: &str) -> Result<(), String> {
78 self.0.check(v)
79 }
80
81 pub fn and(self, other: Validator) -> Validator {
83 let a = self.0;
84 let b = other.0;
85 Validator::new(move |v: &str| {
86 a.check(v)?;
87 b.check(v)
88 })
89 }
90
91 pub fn or(self, other: Validator) -> Validator {
94 let a = self.0;
95 let b = other.0;
96 Validator::new(move |v: &str| {
97 if a.check(v).is_ok() {
98 return Ok(());
99 }
100 b.check(v)
101 })
102 }
103
104 pub fn msg(self, message: impl Into<String>) -> Validator {
106 let inner = self.0;
107 let msg = message.into();
108 Validator::new(move |v: &str| inner.check(v).map_err(|_| msg.clone()))
109 }
110}
111
112impl Validate for Validator {
113 fn check(&self, v: &str) -> Result<(), String> {
114 self.0.check(v)
115 }
116}
117
118impl<F> From<F> for Validator
119where
120 F: Fn(&str) -> Result<(), String> + Send + Sync + 'static,
121{
122 fn from(f: F) -> Self {
123 Validator::new(f)
124 }
125}
126
127pub fn required() -> Validator {
131 Validator::new(|v: &str| {
132 if v.trim().is_empty() {
133 Err("Value is required".into())
134 } else {
135 Ok(())
136 }
137 })
138}
139
140pub fn min_chars(n: usize) -> Validator {
142 Validator::new(move |v: &str| {
143 if v.chars().count() < n {
144 Err(format!("Must be at least {n} characters"))
145 } else {
146 Ok(())
147 }
148 })
149}
150
151pub fn max_chars(n: usize) -> Validator {
153 Validator::new(move |v: &str| {
154 if v.chars().count() > n {
155 Err(format!("Must be at most {n} characters"))
156 } else {
157 Ok(())
158 }
159 })
160}
161
162pub fn exact_chars(n: usize) -> Validator {
164 Validator::new(move |v: &str| {
165 let c = v.chars().count();
166 if c != n {
167 Err(format!("Must be exactly {n} characters (got {c})"))
168 } else {
169 Ok(())
170 }
171 })
172}
173
174pub fn word_count(n: usize) -> Validator {
176 Validator::new(move |v: &str| {
177 let c = v.split_whitespace().count();
178 if c != n {
179 Err(format!("Must be exactly {n} words (got {c})"))
180 } else {
181 Ok(())
182 }
183 })
184}
185
186pub fn words_between(min: usize, max: usize) -> Validator {
188 Validator::new(move |v: &str| {
189 let c = v.split_whitespace().count();
190 if c < min || c > max {
191 Err(format!("Must be {min}–{max} words (got {c})"))
192 } else {
193 Ok(())
194 }
195 })
196}
197
198pub fn has_upper() -> Validator {
200 Validator::new(|v: &str| {
201 if v.chars().any(|c| c.is_ascii_uppercase()) {
202 Ok(())
203 } else {
204 Err("Must contain an uppercase letter".into())
205 }
206 })
207}
208
209pub fn has_lower() -> Validator {
211 Validator::new(|v: &str| {
212 if v.chars().any(|c| c.is_ascii_lowercase()) {
213 Ok(())
214 } else {
215 Err("Must contain a lowercase letter".into())
216 }
217 })
218}
219
220pub fn has_digit() -> Validator {
222 Validator::new(|v: &str| {
223 if v.chars().any(|c| c.is_ascii_digit()) {
224 Ok(())
225 } else {
226 Err("Must contain a digit".into())
227 }
228 })
229}
230
231pub fn has_special() -> Validator {
233 Validator::new(|v: &str| {
234 if v.chars().any(|c| c.is_ascii_punctuation()) {
235 Ok(())
236 } else {
237 Err("Must contain a special character".into())
238 }
239 })
240}
241
242pub fn alpha_only() -> Validator {
244 Validator::new(|v: &str| {
245 if v.chars().all(|c| c.is_alphabetic()) {
246 Ok(())
247 } else {
248 Err("Letters only".into())
249 }
250 })
251}
252
253pub fn alphanumeric() -> Validator {
255 Validator::new(|v: &str| {
256 if v.chars().all(|c| c.is_alphanumeric()) {
257 Ok(())
258 } else {
259 Err("Letters and digits only".into())
260 }
261 })
262}
263
264pub fn only_chars(allowed: &'static str) -> Validator {
266 Validator::new(move |v: &str| {
267 if v.chars().all(|c| allowed.contains(c)) {
268 Ok(())
269 } else {
270 Err(format!("Allowed characters: {allowed}"))
271 }
272 })
273}
274
275pub fn forbid_chars(forbidden: &'static str) -> Validator {
277 Validator::new(move |v: &str| {
278 if let Some(c) = v.chars().find(|c| forbidden.contains(*c)) {
279 Err(format!("'{c}' is not allowed"))
280 } else {
281 Ok(())
282 }
283 })
284}
285
286pub fn int_between(min: i64, max: i64) -> Validator {
288 Validator::new(move |v: &str| match v.trim().parse::<i64>() {
289 Ok(n) if (min..=max).contains(&n) => Ok(()),
290 Ok(n) => Err(format!("Must be between {min} and {max} (got {n})")),
291 Err(_) => Err("Must be an integer".into()),
292 })
293}
294
295pub fn float_between(min: f64, max: f64) -> Validator {
297 Validator::new(move |v: &str| match v.trim().parse::<f64>() {
298 Ok(n) if n >= min && n <= max => Ok(()),
299 Ok(n) => Err(format!("Must be between {min} and {max} (got {n})")),
300 Err(_) => Err("Must be a number".into()),
301 })
302}
303
304pub fn email() -> Validator {
306 Validator::new(|v: &str| {
307 let s = v.trim();
308 let parts: Vec<&str> = s.splitn(2, '@').collect();
309 if parts.len() == 2
310 && !parts[0].is_empty()
311 && parts[1].contains('.')
312 && !parts[1].starts_with('.')
313 && !parts[1].ends_with('.')
314 {
315 Ok(())
316 } else {
317 Err("Must be a valid email".into())
318 }
319 })
320}
321
322pub fn starts_with(prefix: &'static str) -> Validator {
324 Validator::new(move |v: &str| {
325 if v.starts_with(prefix) {
326 Ok(())
327 } else {
328 Err(format!("Must start with `{prefix}`"))
329 }
330 })
331}
332
333pub fn ends_with(suffix: &'static str) -> Validator {
335 Validator::new(move |v: &str| {
336 if v.ends_with(suffix) {
337 Ok(())
338 } else {
339 Err(format!("Must end with `{suffix}`"))
340 }
341 })
342}
343
344pub fn one_of(values: &'static [&'static str]) -> Validator {
346 Validator::new(move |v: &str| {
347 if values.contains(&v) {
348 Ok(())
349 } else {
350 Err(format!("Must be one of: {}", values.join(", ")))
351 }
352 })
353}
354
355