Skip to main content

vtcode_commons/
validation.rs

1//! Validation utilities for common operations
2//!
3//! This module follows the **"Parse, don't validate"** pattern:
4//! boundary functions transform raw input into types that carry
5//! their invariants (`NonEmptyString`, `NonEmptyVec`, `NonEmptySlice`),
6//! so downstream code never re-checks.
7
8use anyhow::{Result, bail};
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
10use std::path::Path;
11
12/// Validate that a string is non-empty
13pub 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
20/// Validate and return non-empty string
21pub 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
28/// Validate optional non-empty string
29pub 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
36/// Validate collection is not empty
37pub 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
44/// Validate that all strings in a slice are non-empty
45pub 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
54/// Validate path exists
55pub 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
62/// Validate path is a file
63pub 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
71/// Validate path is a directory
72pub 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
80/// Basic URL format validation
81pub 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/// Whether `origin` is a bare `http(s)://host[:port]` web origin: no wildcard,
89/// whitespace, credentials, path (including a trailing slash), query, or fragment.
90#[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
107/// Validate alphanumeric identifier
108pub 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/// A validated string that is guaranteed to be non-empty after trimming.
119///
120/// Follows the **"Parse Don't Validate"** pattern (Ch 15): the constraint is
121/// enforced at construction time via [`TryFrom`], so downstream code never
122/// needs to re-check.
123///
124/// ```rust
125/// use vtcode_commons::validation::NonEmptyString;
126///
127/// let name = NonEmptyString::try_from("hello").unwrap();
128/// assert_eq!(name.as_str(), "hello");
129///
130/// assert!(NonEmptyString::try_from("").is_err());
131/// assert!(NonEmptyString::try_from("   ").is_err());
132/// ```
133#[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/// Error returned when a collection is empty but at least one element is required.
224#[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/// A vector guaranteed to contain at least one element.
236///
237/// Follows the **"Parse, don't validate"** pattern: the non-empty invariant
238/// is established once via [`NonEmptyVec::from_vec`] or `TryFrom<Vec<T>>`,
239/// and [`NonEmptyVec::first`] is then infallible (returns `&T`, not `Option<&T>`).
240///
241/// The shape mirrors the `nonempty` crate discussed in Eli Bendersky's
242/// "Rusty thoughts on Parse, don't validate": `head` holds the first element,
243/// `tail` holds the rest, so no indexing or `unwrap` is needed downstream.
244///
245/// ```rust
246/// use vtcode_commons::validation::NonEmptyVec;
247///
248/// let parsed = NonEmptyVec::from_vec(vec!["a", "b"]).expect("non-empty");
249/// assert_eq!(parsed.first(), &"a");
250/// assert!(NonEmptyVec::<String>::from_vec(Vec::new()).is_none());
251/// ```
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct NonEmptyVec<T> {
254    /// First element; always present.
255    pub head: T,
256    /// Remaining elements; possibly empty.
257    pub tail: Vec<T>,
258}
259
260impl<T> NonEmptyVec<T> {
261    /// Create a singleton collection from one element.
262    pub const fn new(head: T) -> Self {
263        Self { head, tail: Vec::new() }
264    }
265
266    /// Alias for [`NonEmptyVec::new`], matching the `nonempty` crate naming.
267    pub const fn singleton(head: T) -> Self {
268        Self { head, tail: Vec::new() }
269    }
270
271    /// Parse a `Vec` into a non-empty collection, returning `None` when empty.
272    ///
273    /// This is the single boundary where the invariant is established.
274    /// Order is preserved: `head` is the original `vec[0]`.
275    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    /// Infallible access to the first element; no `Option` to re-check.
282    #[must_use]
283    pub const fn first(&self) -> &T {
284        &self.head
285    }
286
287    /// Access the last element.
288    #[must_use]
289    pub fn last(&self) -> &T {
290        self.tail.last().unwrap_or(&self.head)
291    }
292
293    /// Number of elements; always `>= 1`.
294    #[must_use]
295    pub fn len(&self) -> usize {
296        self.tail.len().saturating_add(1)
297    }
298
299    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
300    #[must_use]
301    pub const fn is_empty(&self) -> bool {
302        false
303    }
304
305    /// Iterate over all elements in order.
306    pub fn iter(&self) -> impl Iterator<Item = &T> {
307        std::iter::once(&self.head).chain(self.tail.iter())
308    }
309
310    /// Append an element to the end.
311    pub fn push(&mut self, value: T) {
312        self.tail.push(value);
313    }
314
315    /// Consume into a plain `Vec`, preserving order.
316    #[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/// A borrowed slice guaranteed to contain at least one element.
362///
363/// The borrowed counterpart to [`NonEmptyVec`]: parse once via
364/// [`NonEmptySlice::from_slice`], then [`NonEmptySlice::first`] is
365/// infallible. Avoids cloning large payloads (e.g. LLM `choices` arrays)
366/// while still removing validate-then-index (`is_empty` + `[0]`) sites.
367///
368/// ```rust
369/// use vtcode_commons::validation::NonEmptySlice;
370///
371/// let values = vec![10, 20];
372/// let parsed = NonEmptySlice::from_slice(&values).expect("non-empty");
373/// assert_eq!(parsed.first(), &10);
374/// assert!(NonEmptySlice::<i32>::from_slice(&[]).is_none());
375/// ```
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub struct NonEmptySlice<'a, T> {
378    /// First element; always present.
379    first: &'a T,
380    /// Remaining elements; possibly empty.
381    rest: &'a [T],
382}
383
384impl<'a, T> NonEmptySlice<'a, T> {
385    /// Parse a slice into a non-empty view, returning `None` when empty.
386    ///
387    /// This is the single boundary where the invariant is established.
388    pub fn from_slice(slice: &'a [T]) -> Option<Self> {
389        let (first, rest) = slice.split_first()?;
390        Some(Self { first, rest })
391    }
392
393    /// Infallible access to the first element; no `Option` to re-check.
394    #[must_use]
395    pub const fn first(&self) -> &'a T {
396        self.first
397    }
398
399    /// Access the last element.
400    #[must_use]
401    pub fn last(&self) -> &'a T {
402        self.rest.last().unwrap_or(self.first)
403    }
404
405    /// Number of elements; always `>= 1`.
406    #[must_use]
407    pub fn len(&self) -> usize {
408        self.rest.len().saturating_add(1)
409    }
410
411    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
412    #[must_use]
413    pub const fn is_empty(&self) -> bool {
414        false
415    }
416
417    /// Borrow the tail after the first element; possibly empty.
418    ///
419    /// Useful for argv-style parsing where `first` is the program and
420    /// `rest` are the arguments.
421    #[must_use]
422    pub const fn rest(&self) -> &'a [T] {
423        self.rest
424    }
425
426    /// Iterate over all elements in order.
427    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}