Skip to main content

vtcode_commons/
validation.rs

1//! Validation utilities for common operations
2
3use anyhow::{Result, bail};
4use std::path::Path;
5
6/// Validate that a string is non-empty
7pub fn validate_non_empty(value: &str, field_name: &str) -> Result<()> {
8    if value.trim().is_empty() {
9        bail!("{field_name} cannot be empty");
10    }
11    Ok(())
12}
13
14/// Validate and return non-empty string
15pub fn validate_non_empty_string(value: String, field_name: &str) -> Result<String> {
16    if value.trim().is_empty() {
17        bail!("{field_name} cannot be empty");
18    }
19    Ok(value)
20}
21
22/// Validate optional non-empty string
23pub fn validate_optional_non_empty(value: &Option<String>, field_name: &str) -> Result<()> {
24    if let Some(v) = value {
25        validate_non_empty(v, field_name)?;
26    }
27    Ok(())
28}
29
30/// Validate collection is not empty
31pub fn validate_non_empty_collection<T>(collection: &[T], field_name: &str) -> Result<()> {
32    if collection.is_empty() {
33        bail!("{field_name} collection cannot be empty");
34    }
35    Ok(())
36}
37
38/// Validate that all strings in a slice are non-empty
39pub fn validate_all_non_empty(values: &[String], field_name: &str) -> Result<()> {
40    for (i, value) in values.iter().enumerate() {
41        if value.trim().is_empty() {
42            bail!("{field_name}[{i}] cannot be empty");
43        }
44    }
45    Ok(())
46}
47
48/// Validate path exists
49pub fn validate_path_exists(path: &Path, field_name: &str) -> Result<()> {
50    if !path.exists() {
51        bail!("{} path does not exist: {}", field_name, path.display());
52    }
53    Ok(())
54}
55
56/// Validate path is a file
57pub fn validate_is_file(path: &Path, field_name: &str) -> Result<()> {
58    validate_path_exists(path, field_name)?;
59    if !path.is_file() {
60        bail!("{} is not a file: {}", field_name, path.display());
61    }
62    Ok(())
63}
64
65/// Validate path is a directory
66pub fn validate_is_directory(path: &Path, field_name: &str) -> Result<()> {
67    validate_path_exists(path, field_name)?;
68    if !path.is_dir() {
69        bail!("{} is not a directory: {}", field_name, path.display());
70    }
71    Ok(())
72}
73
74/// Basic URL format validation
75pub fn validate_url_format(url: &str, field_name: &str) -> Result<()> {
76    if !url.starts_with("http://") && !url.starts_with("https://") {
77        bail!("{field_name} must be a valid URL starting with http:// or https://");
78    }
79    Ok(())
80}
81
82/// Validate alphanumeric identifier
83pub fn validate_identifier(id: &str, field_name: &str) -> Result<()> {
84    if id.is_empty() {
85        bail!("{field_name} cannot be empty");
86    }
87    if !id.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
88        bail!("{field_name} must be alphanumeric (can include _ or -)");
89    }
90    Ok(())
91}
92
93/// A validated string that is guaranteed to be non-empty after trimming.
94///
95/// Follows the **"Parse Don't Validate"** pattern (Ch 15): the constraint is
96/// enforced at construction time via [`TryFrom`], so downstream code never
97/// needs to re-check.
98///
99/// ```rust
100/// use vtcode_commons::validation::NonEmptyString;
101///
102/// let name = NonEmptyString::try_from("hello").unwrap();
103/// assert_eq!(name.as_str(), "hello");
104///
105/// assert!(NonEmptyString::try_from("").is_err());
106/// assert!(NonEmptyString::try_from("   ").is_err());
107/// ```
108#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
109pub struct NonEmptyString(String);
110
111impl NonEmptyString {
112    pub fn as_str(&self) -> &str {
113        &self.0
114    }
115
116    pub fn into_inner(self) -> String {
117        self.0
118    }
119}
120
121impl std::ops::Deref for NonEmptyString {
122    type Target = str;
123    fn deref(&self) -> &Self::Target {
124        &self.0
125    }
126}
127
128impl std::borrow::Borrow<str> for NonEmptyString {
129    fn borrow(&self) -> &str {
130        &self.0
131    }
132}
133
134impl AsRef<str> for NonEmptyString {
135    fn as_ref(&self) -> &str {
136        &self.0
137    }
138}
139
140impl std::fmt::Display for NonEmptyString {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        self.0.fmt(f)
143    }
144}
145
146impl TryFrom<String> for NonEmptyString {
147    type Error = String;
148
149    fn try_from(value: String) -> Result<Self, Self::Error> {
150        if value.trim().is_empty() {
151            Err("string must be non-empty".to_string())
152        } else {
153            Ok(Self(value))
154        }
155    }
156}
157
158impl TryFrom<&str> for NonEmptyString {
159    type Error = String;
160
161    fn try_from(value: &str) -> Result<Self, Self::Error> {
162        if value.trim().is_empty() {
163            Err("string must be non-empty".to_string())
164        } else {
165            Ok(Self(value.to_string()))
166        }
167    }
168}
169
170impl From<NonEmptyString> for String {
171    fn from(value: NonEmptyString) -> Self {
172        value.0
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_validate_non_empty() {
182        assert!(validate_non_empty("test", "field").is_ok());
183        assert!(validate_non_empty("", "field").is_err());
184        assert!(validate_non_empty("   ", "field").is_err());
185    }
186
187    #[test]
188    fn test_validate_all_non_empty() {
189        assert!(validate_all_non_empty(&["a".to_string(), "b".to_string()], "field").is_ok());
190        assert!(validate_all_non_empty(&["a".to_string(), "".to_string()], "field").is_err());
191        assert!(validate_all_non_empty(&[], "field").is_ok());
192    }
193
194    #[test]
195    fn non_empty_string_accepts_valid() {
196        let s = NonEmptyString::try_from("hello").unwrap();
197        assert_eq!(s.as_str(), "hello");
198        assert_eq!(s.len(), 5);
199    }
200
201    #[test]
202    fn non_empty_string_rejects_empty() {
203        assert!(NonEmptyString::try_from("").is_err());
204        assert!(NonEmptyString::try_from("   ").is_err());
205        assert!(NonEmptyString::try_from("\t\n").is_err());
206    }
207
208    #[test]
209    fn non_empty_string_from_owned() {
210        let s = NonEmptyString::try_from("test".to_string()).unwrap();
211        assert_eq!(s.into_inner(), "test");
212    }
213
214    #[test]
215    fn non_empty_string_deref() {
216        let s = NonEmptyString::try_from("hello").unwrap();
217        assert!(s.starts_with("hel"));
218        assert_eq!(&*s, "hello");
219    }
220}