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 a borrowed optional string without taking ownership.
29///
30/// # Errors
31/// Returns an error naming `field_name` if a present value is whitespace-only.
32/// An absent value is valid.
33pub 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
40/// Validate collection is not empty
41pub 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
48/// Validate that all strings in a slice are non-empty
49pub 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
58/// Validate path exists
59pub 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
66/// Validate path is a file
67pub 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
75/// Validate path is a directory
76pub 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
84/// Basic URL format validation
85pub 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/// Whether `origin` is a bare `http(s)://host[:port]` web origin: no wildcard,
93/// whitespace, credentials, path (including a trailing slash), query, or fragment.
94#[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
111/// Validate alphanumeric identifier
112pub 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/// A validated string that is guaranteed to be non-empty after trimming.
123///
124/// Follows the **"Parse Don't Validate"** pattern (Ch 15): the constraint is
125/// enforced at construction time via [`TryFrom`], so downstream code never
126/// needs to re-check.
127///
128/// ```rust
129/// use vtcode_commons::validation::NonEmptyString;
130///
131/// let name = NonEmptyString::try_from("hello").unwrap();
132/// assert_eq!(name.as_str(), "hello");
133///
134/// assert!(NonEmptyString::try_from("").is_err());
135/// assert!(NonEmptyString::try_from("   ").is_err());
136/// ```
137#[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/// Error returned when a collection is empty but at least one element is required.
228#[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/// A vector guaranteed to contain at least one element.
240///
241/// Follows the **"Parse, don't validate"** pattern: the non-empty invariant
242/// is established once via [`NonEmptyVec::from_vec`] or `TryFrom<Vec<T>>`,
243/// and [`NonEmptyVec::first`] is then infallible (returns `&T`, not `Option<&T>`).
244///
245/// The shape mirrors the `nonempty` crate discussed in Eli Bendersky's
246/// "Rusty thoughts on Parse, don't validate": `head` holds the first element,
247/// `tail` holds the rest, so no indexing or `unwrap` is needed downstream.
248///
249/// ```rust
250/// use vtcode_commons::validation::NonEmptyVec;
251///
252/// let parsed = NonEmptyVec::from_vec(vec!["a", "b"]).expect("non-empty");
253/// assert_eq!(parsed.first(), &"a");
254/// assert!(NonEmptyVec::<String>::from_vec(Vec::new()).is_none());
255/// ```
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct NonEmptyVec<T> {
258    /// First element; always present.
259    pub head: T,
260    /// Remaining elements; possibly empty.
261    pub tail: Vec<T>,
262}
263
264impl<T> NonEmptyVec<T> {
265    /// Create a singleton collection from one element.
266    pub const fn new(head: T) -> Self {
267        Self { head, tail: Vec::new() }
268    }
269
270    /// Alias for [`NonEmptyVec::new`], matching the `nonempty` crate naming.
271    pub const fn singleton(head: T) -> Self {
272        Self { head, tail: Vec::new() }
273    }
274
275    /// Parse a `Vec` into a non-empty collection, returning `None` when empty.
276    ///
277    /// This is the single boundary where the invariant is established.
278    /// Order is preserved: `head` is the original `vec[0]`.
279    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    /// Infallible access to the first element; no `Option` to re-check.
286    #[must_use]
287    pub const fn first(&self) -> &T {
288        &self.head
289    }
290
291    /// Access the last element.
292    #[must_use]
293    pub fn last(&self) -> &T {
294        self.tail.last().unwrap_or(&self.head)
295    }
296
297    /// Number of elements; always `>= 1`.
298    #[must_use]
299    pub fn len(&self) -> usize {
300        self.tail.len().saturating_add(1)
301    }
302
303    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
304    #[must_use]
305    pub const fn is_empty(&self) -> bool {
306        false
307    }
308
309    /// Iterate over all elements in order.
310    pub fn iter(&self) -> impl Iterator<Item = &T> {
311        std::iter::once(&self.head).chain(self.tail.iter())
312    }
313
314    /// Append an element to the end.
315    pub fn push(&mut self, value: T) {
316        self.tail.push(value);
317    }
318
319    /// Consume into a plain `Vec`, preserving order.
320    #[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/// A borrowed slice guaranteed to contain at least one element.
366///
367/// The borrowed counterpart to [`NonEmptyVec`]: parse once via
368/// [`NonEmptySlice::from_slice`], then [`NonEmptySlice::first`] is
369/// infallible. Avoids cloning large payloads (e.g. LLM `choices` arrays)
370/// while still removing validate-then-index (`is_empty` + `[0]`) sites.
371///
372/// ```rust
373/// use vtcode_commons::validation::NonEmptySlice;
374///
375/// let values = vec![10, 20];
376/// let parsed = NonEmptySlice::from_slice(&values).expect("non-empty");
377/// assert_eq!(parsed.first(), &10);
378/// assert!(NonEmptySlice::<i32>::from_slice(&[]).is_none());
379/// ```
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub struct NonEmptySlice<'a, T> {
382    /// First element; always present.
383    first: &'a T,
384    /// Remaining elements; possibly empty.
385    rest: &'a [T],
386}
387
388impl<'a, T> NonEmptySlice<'a, T> {
389    /// Parse a slice into a non-empty view, returning `None` when empty.
390    ///
391    /// This is the single boundary where the invariant is established.
392    pub fn from_slice(slice: &'a [T]) -> Option<Self> {
393        let (first, rest) = slice.split_first()?;
394        Some(Self { first, rest })
395    }
396
397    /// Infallible access to the first element; no `Option` to re-check.
398    #[must_use]
399    pub const fn first(&self) -> &'a T {
400        self.first
401    }
402
403    /// Access the last element.
404    #[must_use]
405    pub fn last(&self) -> &'a T {
406        self.rest.last().unwrap_or(self.first)
407    }
408
409    /// Number of elements; always `>= 1`.
410    #[must_use]
411    pub fn len(&self) -> usize {
412        self.rest.len().saturating_add(1)
413    }
414
415    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
416    #[must_use]
417    pub const fn is_empty(&self) -> bool {
418        false
419    }
420
421    /// Borrow the tail after the first element; possibly empty.
422    ///
423    /// Useful for argv-style parsing where `first` is the program and
424    /// `rest` are the arguments.
425    #[must_use]
426    pub const fn rest(&self) -> &'a [T] {
427        self.rest
428    }
429
430    /// Iterate over all elements in order.
431    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}