vtcode_commons/
validation.rs1use anyhow::{Result, bail};
4use std::path::Path;
5
6pub 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
14pub 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
22pub 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
30pub 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
38pub 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
48pub 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
56pub 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
65pub 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
74pub 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
82pub 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#[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}