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