dynamic_cli/validator/
file_validator.rs1use crate::error::{Result, ValidationError};
25use std::path::Path;
26
27pub fn validate_file_exists(path: &Path, arg_name: &str) -> Result<()> {
49 if !path.exists() {
50 return Err(ValidationError::FileNotFound {
51 path: path.to_path_buf(),
52 arg_name: arg_name.to_string(),
53 suggestion: Some(format!(
54 "Check that the file '{}' exists and the path is correct",
55 path.display()
56 )),
57 }
58 .into());
59 }
60 Ok(())
61}
62
63pub fn validate_file_extension(path: &Path, arg_name: &str, allowed: &[String]) -> Result<()> {
90 if allowed.is_empty() {
91 return Err(ValidationError::InvalidExtension {
92 path: path.to_path_buf(),
93 arg_name: arg_name.to_string(),
94 expected: allowed.to_vec(),
95 }
96 .into());
97 }
98
99 let ext = path
100 .extension()
101 .and_then(|e| e.to_str())
102 .map(|e| e.to_lowercase());
103
104 match ext {
105 Some(ext) if allowed.iter().any(|a| a.to_lowercase() == ext) => Ok(()),
106 _ => Err(ValidationError::InvalidExtension {
107 path: path.to_path_buf(),
108 arg_name: arg_name.to_string(),
109 expected: allowed.to_vec(),
110 }
111 .into()),
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use std::io::Write;
119 use tempfile::NamedTempFile;
120
121 fn temp_file_with_ext(ext: &str, content: &str) -> NamedTempFile {
126 let mut f = tempfile::Builder::new()
127 .suffix(&format!(".{}", ext))
128 .tempfile()
129 .expect("failed to create NamedTempFile");
130 f.write_all(content.as_bytes())
131 .expect("failed to write to NamedTempFile");
132 f
133 }
134
135 #[test]
140 fn test_validate_file_exists_valid_file() {
141 let f = temp_file_with_ext("txt", "content");
142 assert!(validate_file_exists(f.path(), "test_file").is_ok());
143 }
144
145 #[test]
146 fn test_validate_file_exists_valid_directory() {
147 let dir = std::env::temp_dir();
149 assert!(validate_file_exists(&dir, "test_dir").is_ok());
150 }
151
152 #[test]
153 fn test_validate_file_exists_nonexistent() {
154 let path = std::path::Path::new("/tmp/dynamic_cli_no_such_file_xyz.txt");
155 let result = validate_file_exists(path, "missing_file");
156 assert!(result.is_err());
157 match result.unwrap_err() {
158 crate::error::DynamicCliError::Validation(ValidationError::FileNotFound {
159 path: p,
160 arg_name,
161 ..
162 }) => {
163 assert_eq!(arg_name, "missing_file");
164 assert_eq!(p, path);
165 }
166 other => panic!("Expected FileNotFound error, got {:?}", other),
167 }
168 }
169
170 #[test]
171 fn test_validate_file_exists_relative_path() {
172 let relative = std::path::Path::new("Cargo.toml");
176 assert!(validate_file_exists(relative, "cargo_manifest").is_ok());
177 }
178
179 #[test]
184 fn test_validate_file_extension_valid_single() {
185 let path = Path::new("config.yaml");
186 let allowed = vec!["yaml".to_string()];
187 assert!(validate_file_extension(path, "config", &allowed).is_ok());
188 }
189
190 #[test]
191 fn test_validate_file_extension_valid_multiple() {
192 let path = Path::new("data.csv");
193 let allowed = vec!["csv".to_string(), "tsv".to_string(), "txt".to_string()];
194 assert!(validate_file_extension(path, "data_file", &allowed).is_ok());
195 }
196
197 #[test]
198 fn test_validate_file_extension_case_insensitive() {
199 let allowed = vec!["yaml".to_string()];
200 assert!(validate_file_extension(Path::new("config.YAML"), "config", &allowed).is_ok());
201 assert!(validate_file_extension(Path::new("config.YaML"), "config", &allowed).is_ok());
202 let allowed_upper = vec!["YAML".to_string()];
203 assert!(
204 validate_file_extension(Path::new("config.yaml"), "config", &allowed_upper).is_ok()
205 );
206 }
207
208 #[test]
209 fn test_validate_file_extension_invalid() {
210 let path = Path::new("document.txt");
211 let allowed = vec!["yaml".to_string(), "yml".to_string()];
212 let result = validate_file_extension(path, "doc", &allowed);
213 assert!(result.is_err());
214 match result.unwrap_err() {
215 crate::error::DynamicCliError::Validation(ValidationError::InvalidExtension {
216 arg_name,
217 path: error_path,
218 expected,
219 }) => {
220 assert_eq!(arg_name, "doc");
221 assert_eq!(error_path, path);
222 assert_eq!(expected, allowed);
223 }
224 other => panic!("Expected InvalidExtension error, got {:?}", other),
225 }
226 }
227
228 #[test]
229 fn test_validate_file_extension_no_extension() {
230 let path = Path::new("makefile");
231 let allowed = vec!["txt".to_string()];
232 let result = validate_file_extension(path, "build_file", &allowed);
233 assert!(result.is_err());
234 match result.unwrap_err() {
235 crate::error::DynamicCliError::Validation(ValidationError::InvalidExtension {
236 ..
237 }) => {}
238 other => panic!("Expected InvalidExtension error, got {:?}", other),
239 }
240 }
241
242 #[test]
243 fn test_validate_file_extension_hidden_file_with_extension() {
244 let path = Path::new(".hidden.txt");
245 let allowed = vec!["txt".to_string()];
246 assert!(validate_file_extension(path, "hidden_file", &allowed).is_ok());
247 }
248
249 #[test]
250 fn test_validate_file_extension_hidden_file_no_extension() {
251 let path = Path::new(".gitignore");
252 let allowed = vec!["txt".to_string()];
253 assert!(validate_file_extension(path, "git_file", &allowed).is_err());
254 }
255
256 #[test]
257 fn test_validate_file_extension_multiple_dots() {
258 let path = Path::new("archive.tar.gz");
259 let allowed = vec!["gz".to_string()];
260 assert!(validate_file_extension(path, "archive", &allowed).is_ok());
261 }
262
263 #[test]
264 fn test_validate_file_extension_empty_allowed_list() {
265 let path = Path::new("file.txt");
266 let allowed: Vec<String> = vec![];
267 assert!(validate_file_extension(path, "file", &allowed).is_err());
268 }
269
270 #[test]
271 fn test_validate_file_extension_with_leading_dot() {
272 let path = Path::new("config.yaml");
273 let allowed = vec!["yaml".to_string()];
274 assert!(validate_file_extension(path, "config", &allowed).is_ok());
275 }
276
277 #[test]
282 fn test_validate_both_file_and_extension() {
283 let f = temp_file_with_ext("yaml", "key: value");
284 assert!(validate_file_exists(f.path(), "config_file").is_ok());
285 let allowed = vec!["yaml".to_string(), "yml".to_string()];
286 assert!(validate_file_extension(f.path(), "config_file", &allowed).is_ok());
287 }
288
289 #[test]
290 fn test_validate_wrong_extension_existing_file() {
291 let f = temp_file_with_ext("txt", "some data");
292 assert!(validate_file_exists(f.path(), "data_file").is_ok());
293 let allowed = vec!["csv".to_string()];
294 assert!(validate_file_extension(f.path(), "data_file", &allowed).is_err());
295 }
296
297 #[test]
298 fn test_validate_extension_nonexistent_file() {
299 let path = Path::new("nonexistent.yaml");
300 let allowed = vec!["yaml".to_string()];
301 assert!(validate_file_extension(path, "config", &allowed).is_ok());
302 assert!(validate_file_exists(path, "config").is_err());
303 }
304}