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/// Validate alphanumeric identifier
89pub fn validate_identifier(id: &str, field_name: &str) -> Result<()> {
90    if id.is_empty() {
91        bail!("{field_name} cannot be empty");
92    }
93    if !id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
94        bail!("{field_name} must be alphanumeric (can include _ or -)");
95    }
96    Ok(())
97}
98
99/// A validated string that is guaranteed to be non-empty after trimming.
100///
101/// Follows the **"Parse Don't Validate"** pattern (Ch 15): the constraint is
102/// enforced at construction time via [`TryFrom`], so downstream code never
103/// needs to re-check.
104///
105/// ```rust
106/// use vtcode_commons::validation::NonEmptyString;
107///
108/// let name = NonEmptyString::try_from("hello").unwrap();
109/// assert_eq!(name.as_str(), "hello");
110///
111/// assert!(NonEmptyString::try_from("").is_err());
112/// assert!(NonEmptyString::try_from("   ").is_err());
113/// ```
114#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
115pub struct NonEmptyString(String);
116
117impl NonEmptyString {
118    pub fn as_str(&self) -> &str {
119        &self.0
120    }
121
122    pub fn into_inner(self) -> String {
123        self.0
124    }
125}
126
127impl std::ops::Deref for NonEmptyString {
128    type Target = str;
129    fn deref(&self) -> &Self::Target {
130        &self.0
131    }
132}
133
134impl std::borrow::Borrow<str> for NonEmptyString {
135    fn borrow(&self) -> &str {
136        &self.0
137    }
138}
139
140impl AsRef<str> for NonEmptyString {
141    fn as_ref(&self) -> &str {
142        &self.0
143    }
144}
145
146impl std::fmt::Display for NonEmptyString {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        self.0.fmt(f)
149    }
150}
151
152impl TryFrom<String> for NonEmptyString {
153    type Error = String;
154
155    fn try_from(value: String) -> Result<Self, Self::Error> {
156        if value.trim().is_empty() {
157            Err("string must be non-empty".to_string())
158        } else {
159            Ok(Self(value))
160        }
161    }
162}
163
164impl TryFrom<&str> for NonEmptyString {
165    type Error = String;
166
167    fn try_from(value: &str) -> Result<Self, Self::Error> {
168        if value.trim().is_empty() {
169            Err("string must be non-empty".to_string())
170        } else {
171            Ok(Self(value.to_string()))
172        }
173    }
174}
175
176impl From<NonEmptyString> for String {
177    fn from(value: NonEmptyString) -> Self {
178        value.0
179    }
180}
181
182impl Serialize for NonEmptyString {
183    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
184    where
185        S: Serializer,
186    {
187        serializer.serialize_str(&self.0)
188    }
189}
190
191impl<'de> Deserialize<'de> for NonEmptyString {
192    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
193    where
194        D: Deserializer<'de>,
195    {
196        let raw = String::deserialize(deserializer)?;
197        if raw.trim().is_empty() {
198            return Err(D::Error::invalid_value(serde::de::Unexpected::Str(&raw), &"a non-empty string"));
199        }
200        Ok(Self(raw))
201    }
202}
203
204/// Error returned when a collection is empty but at least one element is required.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct EmptyCollectionError;
207
208impl std::fmt::Display for EmptyCollectionError {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        f.write_str("collection must contain at least one element")
211    }
212}
213
214impl std::error::Error for EmptyCollectionError {}
215
216/// A vector guaranteed to contain at least one element.
217///
218/// Follows the **"Parse, don't validate"** pattern: the non-empty invariant
219/// is established once via [`NonEmptyVec::from_vec`] or `TryFrom<Vec<T>>`,
220/// and [`NonEmptyVec::first`] is then infallible (returns `&T`, not `Option<&T>`).
221///
222/// The shape mirrors the `nonempty` crate discussed in Eli Bendersky's
223/// "Rusty thoughts on Parse, don't validate": `head` holds the first element,
224/// `tail` holds the rest, so no indexing or `unwrap` is needed downstream.
225///
226/// ```rust
227/// use vtcode_commons::validation::NonEmptyVec;
228///
229/// let parsed = NonEmptyVec::from_vec(vec!["a", "b"]).expect("non-empty");
230/// assert_eq!(parsed.first(), &"a");
231/// assert!(NonEmptyVec::<String>::from_vec(Vec::new()).is_none());
232/// ```
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct NonEmptyVec<T> {
235    /// First element; always present.
236    pub head: T,
237    /// Remaining elements; possibly empty.
238    pub tail: Vec<T>,
239}
240
241impl<T> NonEmptyVec<T> {
242    /// Create a singleton collection from one element.
243    pub const fn new(head: T) -> Self {
244        Self { head, tail: Vec::new() }
245    }
246
247    /// Alias for [`NonEmptyVec::new`], matching the `nonempty` crate naming.
248    pub const fn singleton(head: T) -> Self {
249        Self { head, tail: Vec::new() }
250    }
251
252    /// Parse a `Vec` into a non-empty collection, returning `None` when empty.
253    ///
254    /// This is the single boundary where the invariant is established.
255    /// Order is preserved: `head` is the original `vec[0]`.
256    pub fn from_vec(vec: Vec<T>) -> Option<Self> {
257        let mut iter = vec.into_iter();
258        let head = iter.next()?;
259        Some(Self { head, tail: iter.collect() })
260    }
261
262    /// Infallible access to the first element; no `Option` to re-check.
263    #[must_use]
264    pub const fn first(&self) -> &T {
265        &self.head
266    }
267
268    /// Access the last element.
269    #[must_use]
270    pub fn last(&self) -> &T {
271        self.tail.last().unwrap_or(&self.head)
272    }
273
274    /// Number of elements; always `>= 1`.
275    #[must_use]
276    pub fn len(&self) -> usize {
277        self.tail.len().saturating_add(1)
278    }
279
280    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
281    #[must_use]
282    pub const fn is_empty(&self) -> bool {
283        false
284    }
285
286    /// Iterate over all elements in order.
287    pub fn iter(&self) -> impl Iterator<Item = &T> {
288        std::iter::once(&self.head).chain(self.tail.iter())
289    }
290
291    /// Append an element to the end.
292    pub fn push(&mut self, value: T) {
293        self.tail.push(value);
294    }
295
296    /// Consume into a plain `Vec`, preserving order.
297    #[must_use]
298    pub fn into_vec(self) -> Vec<T> {
299        let mut vec = Vec::with_capacity(self.tail.len().saturating_add(1));
300        vec.push(self.head);
301        vec.extend(self.tail);
302        vec
303    }
304}
305
306impl<T> TryFrom<Vec<T>> for NonEmptyVec<T> {
307    type Error = EmptyCollectionError;
308
309    fn try_from(value: Vec<T>) -> std::result::Result<Self, Self::Error> {
310        Self::from_vec(value).ok_or(EmptyCollectionError)
311    }
312}
313
314impl<T> From<NonEmptyVec<T>> for Vec<T> {
315    fn from(value: NonEmptyVec<T>) -> Self {
316        value.into_vec()
317    }
318}
319
320impl<T: Serialize> Serialize for NonEmptyVec<T> {
321    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
322    where
323        S: Serializer,
324    {
325        serializer.collect_seq(self.iter())
326    }
327}
328
329impl<'de, T> Deserialize<'de> for NonEmptyVec<T>
330where
331    T: Deserialize<'de>,
332{
333    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
334    where
335        D: Deserializer<'de>,
336    {
337        let raw = Vec::<T>::deserialize(deserializer)?;
338        Self::from_vec(raw).ok_or_else(|| D::Error::invalid_length(0, &"a non-empty array"))
339    }
340}
341
342/// A borrowed slice guaranteed to contain at least one element.
343///
344/// The borrowed counterpart to [`NonEmptyVec`]: parse once via
345/// [`NonEmptySlice::from_slice`], then [`NonEmptySlice::first`] is
346/// infallible. Avoids cloning large payloads (e.g. LLM `choices` arrays)
347/// while still removing validate-then-index (`is_empty` + `[0]`) sites.
348///
349/// ```rust
350/// use vtcode_commons::validation::NonEmptySlice;
351///
352/// let values = vec![10, 20];
353/// let parsed = NonEmptySlice::from_slice(&values).expect("non-empty");
354/// assert_eq!(parsed.first(), &10);
355/// assert!(NonEmptySlice::<i32>::from_slice(&[]).is_none());
356/// ```
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub struct NonEmptySlice<'a, T> {
359    /// First element; always present.
360    first: &'a T,
361    /// Remaining elements; possibly empty.
362    rest: &'a [T],
363}
364
365impl<'a, T> NonEmptySlice<'a, T> {
366    /// Parse a slice into a non-empty view, returning `None` when empty.
367    ///
368    /// This is the single boundary where the invariant is established.
369    pub fn from_slice(slice: &'a [T]) -> Option<Self> {
370        let (first, rest) = slice.split_first()?;
371        Some(Self { first, rest })
372    }
373
374    /// Infallible access to the first element; no `Option` to re-check.
375    #[must_use]
376    pub const fn first(&self) -> &'a T {
377        self.first
378    }
379
380    /// Access the last element.
381    #[must_use]
382    pub fn last(&self) -> &'a T {
383        self.rest.last().unwrap_or(self.first)
384    }
385
386    /// Number of elements; always `>= 1`.
387    #[must_use]
388    pub fn len(&self) -> usize {
389        self.rest.len().saturating_add(1)
390    }
391
392    /// Always returns `false`; provided for generic `len`/`is_empty` pairing.
393    #[must_use]
394    pub const fn is_empty(&self) -> bool {
395        false
396    }
397
398    /// Borrow the tail after the first element; possibly empty.
399    ///
400    /// Useful for argv-style parsing where `first` is the program and
401    /// `rest` are the arguments.
402    #[must_use]
403    pub const fn rest(&self) -> &'a [T] {
404        self.rest
405    }
406
407    /// Iterate over all elements in order.
408    pub fn iter(&self) -> impl Iterator<Item = &'a T> {
409        std::iter::once(self.first).chain(self.rest.iter())
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    #[test]
418    fn test_validate_non_empty() {
419        assert!(validate_non_empty("test", "field").is_ok());
420        assert!(validate_non_empty("", "field").is_err());
421        assert!(validate_non_empty("   ", "field").is_err());
422    }
423
424    #[test]
425    fn test_validate_all_non_empty() {
426        assert!(validate_all_non_empty(&["a".to_string(), "b".to_string()], "field").is_ok());
427        assert!(validate_all_non_empty(&["a".to_string(), "".to_string()], "field").is_err());
428        assert!(validate_all_non_empty(&[], "field").is_ok());
429    }
430
431    #[test]
432    fn non_empty_string_accepts_valid() {
433        let s = NonEmptyString::try_from("hello").unwrap();
434        assert_eq!(s.as_str(), "hello");
435        assert_eq!(s.len(), 5);
436    }
437
438    #[test]
439    fn non_empty_string_rejects_empty() {
440        assert!(NonEmptyString::try_from("").is_err());
441        assert!(NonEmptyString::try_from("   ").is_err());
442        assert!(NonEmptyString::try_from("\t\n").is_err());
443    }
444
445    #[test]
446    fn non_empty_string_from_owned() {
447        let s = NonEmptyString::try_from("test".to_string()).unwrap();
448        assert_eq!(s.into_inner(), "test");
449    }
450
451    #[test]
452    fn non_empty_string_deref() {
453        let s = NonEmptyString::try_from("hello").unwrap();
454        assert!(s.starts_with("hel"));
455        assert_eq!(&*s, "hello");
456    }
457
458    #[test]
459    fn non_empty_string_serde_roundtrip_and_rejection() {
460        let parsed = NonEmptyString::try_from("hello").unwrap();
461        let json = serde_json::to_string(&parsed).unwrap();
462        assert_eq!(json, "\"hello\"");
463        let back: NonEmptyString = serde_json::from_str(&json).unwrap();
464        assert_eq!(back, parsed);
465
466        assert!(serde_json::from_str::<NonEmptyString>("\"\"").is_err());
467        assert!(serde_json::from_str::<NonEmptyString>("\"   \"").is_err());
468        let spaced: NonEmptyString = serde_json::from_str("\"  hello  \"").unwrap();
469        assert_eq!(spaced.as_str(), "  hello  ");
470    }
471
472    #[test]
473    fn non_empty_vec_preserves_order_asymmetric() {
474        let forward = NonEmptyVec::from_vec(vec!["a", "b"]).unwrap();
475        let backward = NonEmptyVec::from_vec(vec!["b", "a"]).unwrap();
476        assert_eq!(forward.first(), &"a");
477        assert_eq!(backward.first(), &"b");
478        assert_ne!(forward, backward);
479        assert_eq!(forward.into_vec(), vec!["a", "b"]);
480        assert_eq!(backward.into_vec(), vec!["b", "a"]);
481    }
482
483    #[test]
484    fn non_empty_vec_singleton_and_empty_boundary() {
485        assert!(NonEmptyVec::<String>::from_vec(Vec::new()).is_none());
486        let singleton = NonEmptyVec::singleton("only".to_string());
487        assert_eq!(singleton.first(), "only");
488        assert_eq!(singleton.last(), "only");
489        assert_eq!(singleton.len(), 1);
490        assert!(!singleton.is_empty());
491        assert_eq!(singleton.into_vec(), vec!["only".to_string()]);
492    }
493
494    #[test]
495    fn non_empty_vec_try_from_reports_typed_error() {
496        let parsed = NonEmptyVec::try_from(vec![1, 2, 3]).unwrap();
497        assert_eq!(parsed.first(), &1);
498        assert_eq!(parsed.last(), &3);
499        assert_eq!(parsed.len(), 3);
500        let err = NonEmptyVec::<i32>::try_from(Vec::new()).unwrap_err();
501        assert_eq!(err, EmptyCollectionError);
502        assert_eq!(err.to_string(), "collection must contain at least one element");
503    }
504
505    #[test]
506    fn non_empty_vec_iter_push_and_serde() {
507        let mut parsed = NonEmptyVec::new("x".to_string());
508        parsed.push("y".to_string());
509        let collected: Vec<&String> = parsed.iter().collect();
510        assert_eq!(collected, vec!["x", "y"]);
511        assert_eq!(parsed.len(), 2);
512
513        let json = serde_json::to_string(&parsed).unwrap();
514        assert_eq!(json, "[\"x\",\"y\"]");
515        let back: NonEmptyVec<String> = serde_json::from_str(&json).unwrap();
516        assert_eq!(back, parsed);
517        assert!(serde_json::from_str::<NonEmptyVec<String>>("[]").is_err());
518    }
519
520    #[test]
521    fn non_empty_slice_parses_once_then_first_is_infallible() {
522        let forward = vec!["a", "b"];
523        let backward = vec!["b", "a"];
524        let parsed_forward = NonEmptySlice::from_slice(&forward).unwrap();
525        let parsed_backward = NonEmptySlice::from_slice(&backward).unwrap();
526        assert_eq!(parsed_forward.first(), &"a");
527        assert_eq!(parsed_backward.first(), &"b");
528        assert_ne!(parsed_forward, parsed_backward);
529        assert_eq!(parsed_forward.len(), 2);
530        assert!(!parsed_forward.is_empty());
531
532        let empty: Vec<String> = Vec::new();
533        assert!(NonEmptySlice::from_slice(&empty).is_none());
534
535        let singleton = vec!["only"];
536        let parsed_single = NonEmptySlice::from_slice(&singleton).unwrap();
537        assert_eq!(parsed_single.first(), &"only");
538        assert_eq!(parsed_single.last(), &"only");
539        assert_eq!(parsed_single.rest(), &[] as &[&str]);
540        let collected: Vec<&&str> = parsed_forward.iter().collect();
541        assert_eq!(collected, vec![&"a", &"b"]);
542        assert_eq!(parsed_forward.rest(), &["b"] as &[&str]);
543    }
544}