1use anyhow::{Result, bail};
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
10use std::path::Path;
11
12pub fn validate_non_empty(value: &str, field_name: &str) -> Result<()> {
14 if value.trim().is_empty() {
15 bail!("{field_name} cannot be empty");
16 }
17 Ok(())
18}
19
20pub fn validate_non_empty_string(value: String, field_name: &str) -> Result<String> {
22 if value.trim().is_empty() {
23 bail!("{field_name} cannot be empty");
24 }
25 Ok(value)
26}
27
28pub fn validate_optional_non_empty(value: Option<&str>, field_name: &str) -> Result<()> {
34 if let Some(v) = value {
35 validate_non_empty(v, field_name)?;
36 }
37 Ok(())
38}
39
40pub fn validate_non_empty_collection<T>(collection: &[T], field_name: &str) -> Result<()> {
42 if collection.is_empty() {
43 bail!("{field_name} collection cannot be empty");
44 }
45 Ok(())
46}
47
48pub fn validate_all_non_empty(values: &[String], field_name: &str) -> Result<()> {
50 for (i, value) in values.iter().enumerate() {
51 if value.trim().is_empty() {
52 bail!("{field_name}[{i}] cannot be empty");
53 }
54 }
55 Ok(())
56}
57
58pub fn validate_path_exists(path: &Path, field_name: &str) -> Result<()> {
60 if !path.exists() {
61 bail!("{} path does not exist: {}", field_name, path.display());
62 }
63 Ok(())
64}
65
66pub fn validate_is_file(path: &Path, field_name: &str) -> Result<()> {
68 validate_path_exists(path, field_name)?;
69 if !path.is_file() {
70 bail!("{} is not a file: {}", field_name, path.display());
71 }
72 Ok(())
73}
74
75pub fn validate_is_directory(path: &Path, field_name: &str) -> Result<()> {
77 validate_path_exists(path, field_name)?;
78 if !path.is_dir() {
79 bail!("{} is not a directory: {}", field_name, path.display());
80 }
81 Ok(())
82}
83
84pub fn validate_url_format(url: &str, field_name: &str) -> Result<()> {
86 if !url.starts_with("http://") && !url.starts_with("https://") {
87 bail!("{field_name} must be a valid URL starting with http:// or https://");
88 }
89 Ok(())
90}
91
92#[must_use]
95pub fn is_valid_origin(origin: &str) -> bool {
96 let Ok(parsed) = url::Url::parse(origin) else {
97 return false;
98 };
99 origin == origin.trim()
100 && !origin.chars().any(char::is_whitespace)
101 && !origin.contains('*')
102 && matches!(parsed.scheme(), "http" | "https")
103 && parsed.host_str().is_some_and(|host| !host.is_empty())
104 && parsed.username().is_empty()
105 && parsed.password().is_none()
106 && (parsed.path().is_empty() || (parsed.path() == "/" && !origin.ends_with('/')))
107 && parsed.query().is_none()
108 && parsed.fragment().is_none()
109}
110
111pub fn validate_identifier(id: &str, field_name: &str) -> Result<()> {
113 if id.is_empty() {
114 bail!("{field_name} cannot be empty");
115 }
116 if !id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
117 bail!("{field_name} must be alphanumeric (can include _ or -)");
118 }
119 Ok(())
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
138pub struct NonEmptyString(String);
139
140impl NonEmptyString {
141 pub fn as_str(&self) -> &str {
142 &self.0
143 }
144
145 pub fn into_inner(self) -> String {
146 self.0
147 }
148}
149
150impl std::ops::Deref for NonEmptyString {
151 type Target = str;
152 fn deref(&self) -> &Self::Target {
153 &self.0
154 }
155}
156
157impl std::borrow::Borrow<str> for NonEmptyString {
158 fn borrow(&self) -> &str {
159 &self.0
160 }
161}
162
163impl AsRef<str> for NonEmptyString {
164 fn as_ref(&self) -> &str {
165 &self.0
166 }
167}
168
169impl std::fmt::Display for NonEmptyString {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 self.0.fmt(f)
172 }
173}
174
175impl TryFrom<String> for NonEmptyString {
176 type Error = String;
177
178 fn try_from(value: String) -> Result<Self, Self::Error> {
179 if value.trim().is_empty() {
180 Err("string must be non-empty".to_string())
181 } else {
182 Ok(Self(value))
183 }
184 }
185}
186
187impl TryFrom<&str> for NonEmptyString {
188 type Error = String;
189
190 fn try_from(value: &str) -> Result<Self, Self::Error> {
191 if value.trim().is_empty() {
192 Err("string must be non-empty".to_string())
193 } else {
194 Ok(Self(value.to_string()))
195 }
196 }
197}
198
199impl From<NonEmptyString> for String {
200 fn from(value: NonEmptyString) -> Self {
201 value.0
202 }
203}
204
205impl Serialize for NonEmptyString {
206 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
207 where
208 S: Serializer,
209 {
210 serializer.serialize_str(&self.0)
211 }
212}
213
214impl<'de> Deserialize<'de> for NonEmptyString {
215 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
216 where
217 D: Deserializer<'de>,
218 {
219 let raw = String::deserialize(deserializer)?;
220 if raw.trim().is_empty() {
221 return Err(D::Error::invalid_value(serde::de::Unexpected::Str(&raw), &"a non-empty string"));
222 }
223 Ok(Self(raw))
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct EmptyCollectionError;
230
231impl std::fmt::Display for EmptyCollectionError {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.write_str("collection must contain at least one element")
234 }
235}
236
237impl std::error::Error for EmptyCollectionError {}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct NonEmptyVec<T> {
258 pub head: T,
260 pub tail: Vec<T>,
262}
263
264impl<T> NonEmptyVec<T> {
265 pub const fn new(head: T) -> Self {
267 Self { head, tail: Vec::new() }
268 }
269
270 pub const fn singleton(head: T) -> Self {
272 Self { head, tail: Vec::new() }
273 }
274
275 pub fn from_vec(vec: Vec<T>) -> Option<Self> {
280 let mut iter = vec.into_iter();
281 let head = iter.next()?;
282 Some(Self { head, tail: iter.collect() })
283 }
284
285 #[must_use]
287 pub const fn first(&self) -> &T {
288 &self.head
289 }
290
291 #[must_use]
293 pub fn last(&self) -> &T {
294 self.tail.last().unwrap_or(&self.head)
295 }
296
297 #[must_use]
299 pub fn len(&self) -> usize {
300 self.tail.len().saturating_add(1)
301 }
302
303 #[must_use]
305 pub const fn is_empty(&self) -> bool {
306 false
307 }
308
309 pub fn iter(&self) -> impl Iterator<Item = &T> {
311 std::iter::once(&self.head).chain(self.tail.iter())
312 }
313
314 pub fn push(&mut self, value: T) {
316 self.tail.push(value);
317 }
318
319 #[must_use]
321 pub fn into_vec(self) -> Vec<T> {
322 let mut vec = Vec::with_capacity(self.tail.len().saturating_add(1));
323 vec.push(self.head);
324 vec.extend(self.tail);
325 vec
326 }
327}
328
329impl<T> TryFrom<Vec<T>> for NonEmptyVec<T> {
330 type Error = EmptyCollectionError;
331
332 fn try_from(value: Vec<T>) -> std::result::Result<Self, Self::Error> {
333 Self::from_vec(value).ok_or(EmptyCollectionError)
334 }
335}
336
337impl<T> From<NonEmptyVec<T>> for Vec<T> {
338 fn from(value: NonEmptyVec<T>) -> Self {
339 value.into_vec()
340 }
341}
342
343impl<T: Serialize> Serialize for NonEmptyVec<T> {
344 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
345 where
346 S: Serializer,
347 {
348 serializer.collect_seq(self.iter())
349 }
350}
351
352impl<'de, T> Deserialize<'de> for NonEmptyVec<T>
353where
354 T: Deserialize<'de>,
355{
356 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
357 where
358 D: Deserializer<'de>,
359 {
360 let raw = Vec::<T>::deserialize(deserializer)?;
361 Self::from_vec(raw).ok_or_else(|| D::Error::invalid_length(0, &"a non-empty array"))
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub struct NonEmptySlice<'a, T> {
382 first: &'a T,
384 rest: &'a [T],
386}
387
388impl<'a, T> NonEmptySlice<'a, T> {
389 pub fn from_slice(slice: &'a [T]) -> Option<Self> {
393 let (first, rest) = slice.split_first()?;
394 Some(Self { first, rest })
395 }
396
397 #[must_use]
399 pub const fn first(&self) -> &'a T {
400 self.first
401 }
402
403 #[must_use]
405 pub fn last(&self) -> &'a T {
406 self.rest.last().unwrap_or(self.first)
407 }
408
409 #[must_use]
411 pub fn len(&self) -> usize {
412 self.rest.len().saturating_add(1)
413 }
414
415 #[must_use]
417 pub const fn is_empty(&self) -> bool {
418 false
419 }
420
421 #[must_use]
426 pub const fn rest(&self) -> &'a [T] {
427 self.rest
428 }
429
430 pub fn iter(&self) -> impl Iterator<Item = &'a T> {
432 std::iter::once(self.first).chain(self.rest.iter())
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn optional_non_empty_accepts_borrowed_strings_and_absence() {
442 assert!(validate_optional_non_empty(None, "label").is_ok());
443 assert!(validate_optional_non_empty(Some("label"), "label").is_ok());
444 for value in ["", " \t\n"] {
445 let error = validate_optional_non_empty(Some(value), "label").unwrap_err();
446 assert_eq!(error.to_string(), "label cannot be empty");
447 }
448 }
449
450 #[test]
451 fn is_valid_origin_accepts_only_bare_web_origins() {
452 for ok in ["http://localhost", "https://example.com", "http://127.0.0.1:3000"] {
453 assert!(is_valid_origin(ok), "{ok}");
454 }
455 for bad in [
456 "",
457 "https://*.example.com",
458 "https://example.com/",
459 "https://example.com/path",
460 "https://example.com?q=1",
461 "https://example.com#frag",
462 "https://user:pw@example.com",
463 "ftp://example.com",
464 " https://example.com",
465 "https://exa mple.com",
466 ] {
467 assert!(!is_valid_origin(bad), "{bad}");
468 }
469 }
470
471 #[test]
472 fn test_validate_non_empty() {
473 assert!(validate_non_empty("test", "field").is_ok());
474 assert!(validate_non_empty("", "field").is_err());
475 assert!(validate_non_empty(" ", "field").is_err());
476 }
477
478 #[test]
479 fn test_validate_all_non_empty() {
480 assert!(validate_all_non_empty(&["a".to_string(), "b".to_string()], "field").is_ok());
481 assert!(validate_all_non_empty(&["a".to_string(), "".to_string()], "field").is_err());
482 assert!(validate_all_non_empty(&[], "field").is_ok());
483 }
484
485 #[test]
486 fn non_empty_string_accepts_valid() {
487 let s = NonEmptyString::try_from("hello").unwrap();
488 assert_eq!(s.as_str(), "hello");
489 assert_eq!(s.len(), 5);
490 }
491
492 #[test]
493 fn non_empty_string_rejects_empty() {
494 assert!(NonEmptyString::try_from("").is_err());
495 assert!(NonEmptyString::try_from(" ").is_err());
496 assert!(NonEmptyString::try_from("\t\n").is_err());
497 }
498
499 #[test]
500 fn non_empty_string_from_owned() {
501 let s = NonEmptyString::try_from("test".to_string()).unwrap();
502 assert_eq!(s.into_inner(), "test");
503 }
504
505 #[test]
506 fn non_empty_string_deref() {
507 let s = NonEmptyString::try_from("hello").unwrap();
508 assert!(s.starts_with("hel"));
509 assert_eq!(&*s, "hello");
510 }
511
512 #[test]
513 fn non_empty_string_serde_roundtrip_and_rejection() {
514 let parsed = NonEmptyString::try_from("hello").unwrap();
515 let json = serde_json::to_string(&parsed).unwrap();
516 assert_eq!(json, "\"hello\"");
517 let back: NonEmptyString = serde_json::from_str(&json).unwrap();
518 assert_eq!(back, parsed);
519
520 assert!(serde_json::from_str::<NonEmptyString>("\"\"").is_err());
521 assert!(serde_json::from_str::<NonEmptyString>("\" \"").is_err());
522 let spaced: NonEmptyString = serde_json::from_str("\" hello \"").unwrap();
523 assert_eq!(spaced.as_str(), " hello ");
524 }
525
526 #[test]
527 fn non_empty_vec_preserves_order_asymmetric() {
528 let forward = NonEmptyVec::from_vec(vec!["a", "b"]).unwrap();
529 let backward = NonEmptyVec::from_vec(vec!["b", "a"]).unwrap();
530 assert_eq!(forward.first(), &"a");
531 assert_eq!(backward.first(), &"b");
532 assert_ne!(forward, backward);
533 assert_eq!(forward.into_vec(), vec!["a", "b"]);
534 assert_eq!(backward.into_vec(), vec!["b", "a"]);
535 }
536
537 #[test]
538 fn non_empty_vec_singleton_and_empty_boundary() {
539 assert!(NonEmptyVec::<String>::from_vec(Vec::new()).is_none());
540 let singleton = NonEmptyVec::singleton("only".to_string());
541 assert_eq!(singleton.first(), "only");
542 assert_eq!(singleton.last(), "only");
543 assert_eq!(singleton.len(), 1);
544 assert!(!singleton.is_empty());
545 assert_eq!(singleton.into_vec(), vec!["only".to_string()]);
546 }
547
548 #[test]
549 fn non_empty_vec_try_from_reports_typed_error() {
550 let parsed = NonEmptyVec::try_from(vec![1, 2, 3]).unwrap();
551 assert_eq!(parsed.first(), &1);
552 assert_eq!(parsed.last(), &3);
553 assert_eq!(parsed.len(), 3);
554 let err = NonEmptyVec::<i32>::try_from(Vec::new()).unwrap_err();
555 assert_eq!(err, EmptyCollectionError);
556 assert_eq!(err.to_string(), "collection must contain at least one element");
557 }
558
559 #[test]
560 fn non_empty_vec_iter_push_and_serde() {
561 let mut parsed = NonEmptyVec::new("x".to_string());
562 parsed.push("y".to_string());
563 let collected: Vec<&String> = parsed.iter().collect();
564 assert_eq!(collected, vec!["x", "y"]);
565 assert_eq!(parsed.len(), 2);
566
567 let json = serde_json::to_string(&parsed).unwrap();
568 assert_eq!(json, "[\"x\",\"y\"]");
569 let back: NonEmptyVec<String> = serde_json::from_str(&json).unwrap();
570 assert_eq!(back, parsed);
571 assert!(serde_json::from_str::<NonEmptyVec<String>>("[]").is_err());
572 }
573
574 #[test]
575 fn non_empty_slice_parses_once_then_first_is_infallible() {
576 let forward = vec!["a", "b"];
577 let backward = vec!["b", "a"];
578 let parsed_forward = NonEmptySlice::from_slice(&forward).unwrap();
579 let parsed_backward = NonEmptySlice::from_slice(&backward).unwrap();
580 assert_eq!(parsed_forward.first(), &"a");
581 assert_eq!(parsed_backward.first(), &"b");
582 assert_ne!(parsed_forward, parsed_backward);
583 assert_eq!(parsed_forward.len(), 2);
584 assert!(!parsed_forward.is_empty());
585
586 let empty: Vec<String> = Vec::new();
587 assert!(NonEmptySlice::from_slice(&empty).is_none());
588
589 let singleton = vec!["only"];
590 let parsed_single = NonEmptySlice::from_slice(&singleton).unwrap();
591 assert_eq!(parsed_single.first(), &"only");
592 assert_eq!(parsed_single.last(), &"only");
593 assert_eq!(parsed_single.rest(), &[] as &[&str]);
594 let collected: Vec<&&str> = parsed_forward.iter().collect();
595 assert_eq!(collected, vec![&"a", &"b"]);
596 assert_eq!(parsed_forward.rest(), &["b"] as &[&str]);
597 }
598}