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
88        .chars()
89        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
90    {
91        bail!("{field_name} must be alphanumeric (can include _ or -)");
92    }
93    Ok(())
94}
95
96/// A validated string that is guaranteed to be non-empty after trimming.
97///
98/// Follows the **"Parse Don't Validate"** pattern (Ch 15): the constraint is
99/// enforced at construction time via [`TryFrom`], so downstream code never
100/// needs to re-check.
101///
102/// ```rust
103/// use vtcode_commons::validation::NonEmptyString;
104///
105/// let name = NonEmptyString::try_from("hello").unwrap();
106/// assert_eq!(name.as_str(), "hello");
107///
108/// assert!(NonEmptyString::try_from("").is_err());
109/// assert!(NonEmptyString::try_from("   ").is_err());
110/// ```
111#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
112pub struct NonEmptyString(String);
113
114impl NonEmptyString {
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118
119    pub fn into_inner(self) -> String {
120        self.0
121    }
122}
123
124impl std::ops::Deref for NonEmptyString {
125    type Target = str;
126    fn deref(&self) -> &Self::Target {
127        &self.0
128    }
129}
130
131impl std::borrow::Borrow<str> for NonEmptyString {
132    fn borrow(&self) -> &str {
133        &self.0
134    }
135}
136
137impl AsRef<str> for NonEmptyString {
138    fn as_ref(&self) -> &str {
139        &self.0
140    }
141}
142
143impl std::fmt::Display for NonEmptyString {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        self.0.fmt(f)
146    }
147}
148
149impl TryFrom<String> for NonEmptyString {
150    type Error = String;
151
152    fn try_from(value: String) -> Result<Self, Self::Error> {
153        if value.trim().is_empty() {
154            Err("string must be non-empty".to_string())
155        } else {
156            Ok(Self(value))
157        }
158    }
159}
160
161impl TryFrom<&str> for NonEmptyString {
162    type Error = String;
163
164    fn try_from(value: &str) -> Result<Self, Self::Error> {
165        if value.trim().is_empty() {
166            Err("string must be non-empty".to_string())
167        } else {
168            Ok(Self(value.to_string()))
169        }
170    }
171}
172
173impl From<NonEmptyString> for String {
174    fn from(value: NonEmptyString) -> Self {
175        value.0
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_validate_non_empty() {
185        assert!(validate_non_empty("test", "field").is_ok());
186        assert!(validate_non_empty("", "field").is_err());
187        assert!(validate_non_empty("   ", "field").is_err());
188    }
189
190    #[test]
191    fn test_validate_all_non_empty() {
192        assert!(validate_all_non_empty(&["a".to_string(), "b".to_string()], "field").is_ok());
193        assert!(validate_all_non_empty(&["a".to_string(), "".to_string()], "field").is_err());
194        assert!(validate_all_non_empty(&[], "field").is_ok());
195    }
196
197    #[test]
198    fn non_empty_string_accepts_valid() {
199        let s = NonEmptyString::try_from("hello").unwrap();
200        assert_eq!(s.as_str(), "hello");
201        assert_eq!(s.len(), 5);
202    }
203
204    #[test]
205    fn non_empty_string_rejects_empty() {
206        assert!(NonEmptyString::try_from("").is_err());
207        assert!(NonEmptyString::try_from("   ").is_err());
208        assert!(NonEmptyString::try_from("\t\n").is_err());
209    }
210
211    #[test]
212    fn non_empty_string_from_owned() {
213        let s = NonEmptyString::try_from("test".to_string()).unwrap();
214        assert_eq!(s.into_inner(), "test");
215    }
216
217    #[test]
218    fn non_empty_string_deref() {
219        let s = NonEmptyString::try_from("hello").unwrap();
220        assert!(s.starts_with("hel"));
221        assert_eq!(&*s, "hello");
222    }
223}