vika_cli/config/
validator.rs

1use crate::config::model::Config;
2use crate::error::{ConfigError, Result};
3use std::path::{Path, PathBuf};
4
5pub fn validate_config(config: &Config) -> Result<()> {
6    // Validate that at least one spec is defined
7    if config.specs.is_empty() {
8        return Err(ConfigError::NoSpecDefined.into());
9    }
10
11    // Validate specs configuration
12    // Check for duplicate names
13    let mut seen_names = std::collections::HashSet::new();
14    for spec in &config.specs {
15        if seen_names.contains(&spec.name) {
16            return Err(ConfigError::DuplicateSpecName {
17                name: spec.name.clone(),
18            }
19            .into());
20        }
21        seen_names.insert(&spec.name);
22
23        // Validate spec name
24        if spec.name.is_empty() {
25            return Err(ConfigError::InvalidSpecName {
26                name: spec.name.clone(),
27            }
28            .into());
29        }
30
31        // Validate spec name format (alphanumeric, hyphens, underscores only)
32        if !spec
33            .name
34            .chars()
35            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
36        {
37            return Err(ConfigError::InvalidSpecName {
38                name: spec.name.clone(),
39            }
40            .into());
41        }
42
43        // Validate spec path is not empty
44        if spec.path.is_empty() {
45            return Err(ConfigError::Invalid {
46                message: format!("Spec '{}' has an empty path", spec.name),
47            }
48            .into());
49        }
50
51        // Validate per-spec schemas output path
52        let schemas_output = PathBuf::from(&spec.schemas.output);
53        if schemas_output.is_absolute() {
54            validate_safe_path(&schemas_output)?;
55        }
56
57        // Validate per-spec apis output path
58        let apis_output = PathBuf::from(&spec.apis.output);
59        if apis_output.is_absolute() {
60            validate_safe_path(&apis_output)?;
61        }
62
63        // Validate per-spec API style
64        if spec.apis.style != "fetch" {
65            return Err(ConfigError::Invalid {
66                message: format!(
67                    "Unsupported API style for spec '{}': {}. Only 'fetch' is supported.",
68                    spec.name, spec.apis.style
69                ),
70            }
71            .into());
72        }
73    }
74
75    // Validate root_dir
76    let root_dir = PathBuf::from(&config.root_dir);
77    if root_dir.is_absolute() && !root_dir.exists() {
78        return Err(ConfigError::Invalid {
79            message: format!("Root directory does not exist: {}", config.root_dir),
80        }
81        .into());
82    }
83
84    Ok(())
85}
86
87fn validate_safe_path(path: &Path) -> Result<()> {
88    // Prevent writing to system directories
89    let path_str = path.to_string_lossy();
90
91    if path_str.contains("/etc/")
92        || path_str.contains("/usr/")
93        || path_str.contains("/bin/")
94        || path_str.contains("/sbin/")
95        || path_str.contains("/var/")
96        || path_str.contains("/opt/")
97        || path_str == "/"
98        || path_str == "/root"
99    {
100        return Err(ConfigError::InvalidOutputDirectory {
101            path: path_str.to_string(),
102        }
103        .into());
104    }
105
106    Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::config::model::Config;
113
114    #[test]
115    fn test_validate_config_valid() {
116        let mut config = Config::default();
117        config.specs = vec![crate::config::model::SpecEntry {
118            name: "test".to_string(),
119            path: "test.yaml".to_string(),
120            schemas: crate::config::model::SchemasConfig::default(),
121            apis: crate::config::model::ApisConfig::default(),
122            modules: crate::config::model::ModulesConfig::default(),
123        }];
124        assert!(validate_config(&config).is_ok());
125    }
126
127    #[test]
128    fn test_validate_config_invalid_style() {
129        let mut config = Config::default();
130        let mut apis = crate::config::model::ApisConfig::default();
131        apis.style = "invalid".to_string();
132        config.specs = vec![crate::config::model::SpecEntry {
133            name: "test".to_string(),
134            path: "test.yaml".to_string(),
135            schemas: crate::config::model::SchemasConfig::default(),
136            apis,
137            modules: crate::config::model::ModulesConfig::default(),
138        }];
139
140        let result = validate_config(&config);
141        assert!(result.is_err());
142        let error = result.unwrap_err();
143        assert!(error.to_string().contains("Unsupported API style"));
144    }
145
146    #[test]
147    fn test_validate_safe_path_etc() {
148        let path = PathBuf::from("/etc/test");
149        let result = validate_safe_path(&path);
150        assert!(result.is_err());
151    }
152
153    #[test]
154    fn test_validate_safe_path_usr() {
155        let path = PathBuf::from("/usr/test");
156        let result = validate_safe_path(&path);
157        assert!(result.is_err());
158    }
159
160    #[test]
161    fn test_validate_safe_path_bin() {
162        let path = PathBuf::from("/bin/test");
163        let result = validate_safe_path(&path);
164        assert!(result.is_err());
165    }
166
167    #[test]
168    fn test_validate_safe_path_root() {
169        let path = PathBuf::from("/");
170        let result = validate_safe_path(&path);
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_validate_safe_path_valid() {
176        let path = PathBuf::from("/home/user/project");
177        let result = validate_safe_path(&path);
178        assert!(result.is_ok());
179    }
180
181    #[test]
182    fn test_validate_config_absolute_paths() {
183        let mut config = Config::default();
184        let mut schemas = crate::config::model::SchemasConfig::default();
185        schemas.output = "/home/user/schemas".to_string();
186        let mut apis = crate::config::model::ApisConfig::default();
187        apis.output = "/home/user/apis".to_string();
188        config.specs = vec![crate::config::model::SpecEntry {
189            name: "test".to_string(),
190            path: "test.yaml".to_string(),
191            schemas,
192            apis,
193            modules: crate::config::model::ModulesConfig::default(),
194        }];
195
196        let result = validate_config(&config);
197        assert!(result.is_ok());
198    }
199
200    #[test]
201    fn test_validate_config_unsafe_schemas_path() {
202        let mut config = Config::default();
203        let mut schemas = crate::config::model::SchemasConfig::default();
204        schemas.output = "/etc/schemas".to_string();
205        config.specs = vec![crate::config::model::SpecEntry {
206            name: "test".to_string(),
207            path: "test.yaml".to_string(),
208            schemas,
209            apis: crate::config::model::ApisConfig::default(),
210            modules: crate::config::model::ModulesConfig::default(),
211        }];
212
213        let result = validate_config(&config);
214        assert!(result.is_err());
215    }
216
217    #[test]
218    fn test_validate_config_unsafe_apis_path() {
219        let mut config = Config::default();
220        let mut apis = crate::config::model::ApisConfig::default();
221        apis.output = "/usr/apis".to_string();
222        config.specs = vec![crate::config::model::SpecEntry {
223            name: "test".to_string(),
224            path: "test.yaml".to_string(),
225            schemas: crate::config::model::SchemasConfig::default(),
226            apis,
227            modules: crate::config::model::ModulesConfig::default(),
228        }];
229
230        let result = validate_config(&config);
231        assert!(result.is_err());
232    }
233
234    #[test]
235    fn test_validate_config_no_spec_defined() {
236        let config = Config::default();
237        // Default config has no specs, so this should fail
238        let result = validate_config(&config);
239        assert!(result.is_err());
240        let error = result.unwrap_err();
241        assert!(error.to_string().contains("No specs are defined"));
242    }
243
244    #[test]
245    fn test_validate_config_empty_specs_array() {
246        let mut config = Config::default();
247        config.specs = vec![];
248
249        let result = validate_config(&config);
250        assert!(result.is_err());
251        let error = result.unwrap_err();
252        assert!(error.to_string().contains("No specs are defined"));
253    }
254
255    #[test]
256    fn test_validate_config_duplicate_spec_names() {
257        let mut config = Config::default();
258        config.specs = vec![
259            crate::config::model::SpecEntry {
260                name: "auth".to_string(),
261                path: "specs/auth.yaml".to_string(),
262                schemas: crate::config::model::SchemasConfig::default(),
263                apis: crate::config::model::ApisConfig::default(),
264                modules: crate::config::model::ModulesConfig::default(),
265            },
266            crate::config::model::SpecEntry {
267                name: "auth".to_string(),
268                path: "specs/auth2.yaml".to_string(),
269                schemas: crate::config::model::SchemasConfig::default(),
270                apis: crate::config::model::ApisConfig::default(),
271                modules: crate::config::model::ModulesConfig::default(),
272            },
273        ];
274
275        let result = validate_config(&config);
276        assert!(result.is_err());
277        let error = result.unwrap_err();
278        assert!(error.to_string().contains("Duplicate spec name"));
279    }
280
281    #[test]
282    fn test_validate_config_invalid_spec_name() {
283        let mut config = Config::default();
284        config.specs = vec![crate::config::model::SpecEntry {
285            name: "invalid name".to_string(), // contains space
286            path: "specs/auth.yaml".to_string(),
287            schemas: crate::config::model::SchemasConfig::default(),
288            apis: crate::config::model::ApisConfig::default(),
289            modules: crate::config::model::ModulesConfig::default(),
290        }];
291
292        let result = validate_config(&config);
293        assert!(result.is_err());
294        let error = result.unwrap_err();
295        assert!(error.to_string().contains("Invalid spec name"));
296    }
297
298    #[test]
299    fn test_validate_config_empty_spec_name() {
300        let mut config = Config::default();
301        config.specs = vec![crate::config::model::SpecEntry {
302            name: "".to_string(),
303            path: "specs/auth.yaml".to_string(),
304            schemas: crate::config::model::SchemasConfig::default(),
305            apis: crate::config::model::ApisConfig::default(),
306            modules: crate::config::model::ModulesConfig::default(),
307        }];
308
309        let result = validate_config(&config);
310        assert!(result.is_err());
311        let error = result.unwrap_err();
312        assert!(error.to_string().contains("Invalid spec name"));
313    }
314
315    #[test]
316    fn test_validate_config_empty_spec_path() {
317        let mut config = Config::default();
318        config.specs = vec![crate::config::model::SpecEntry {
319            name: "auth".to_string(),
320            path: "".to_string(),
321            schemas: crate::config::model::SchemasConfig::default(),
322            apis: crate::config::model::ApisConfig::default(),
323            modules: crate::config::model::ModulesConfig::default(),
324        }];
325
326        let result = validate_config(&config);
327        assert!(result.is_err());
328        let error = result.unwrap_err();
329        assert!(error.to_string().contains("empty path"));
330    }
331
332    #[test]
333    fn test_validate_config_valid_multi_spec() {
334        let mut config = Config::default();
335        config.specs = vec![
336            crate::config::model::SpecEntry {
337                name: "auth".to_string(),
338                path: "specs/auth.yaml".to_string(),
339                schemas: crate::config::model::SchemasConfig::default(),
340                apis: crate::config::model::ApisConfig::default(),
341                modules: crate::config::model::ModulesConfig::default(),
342            },
343            crate::config::model::SpecEntry {
344                name: "orders".to_string(),
345                path: "specs/orders.json".to_string(),
346                schemas: crate::config::model::SchemasConfig::default(),
347                apis: crate::config::model::ApisConfig::default(),
348                modules: crate::config::model::ModulesConfig::default(),
349            },
350        ];
351
352        let result = validate_config(&config);
353        assert!(result.is_ok());
354    }
355
356    #[test]
357    fn test_validate_config_valid_single_spec() {
358        let mut config = Config::default();
359        config.specs = vec![crate::config::model::SpecEntry {
360            name: "default".to_string(),
361            path: "openapi.json".to_string(),
362            schemas: crate::config::model::SchemasConfig::default(),
363            apis: crate::config::model::ApisConfig::default(),
364            modules: crate::config::model::ModulesConfig::default(),
365        }];
366
367        let result = validate_config(&config);
368        assert!(result.is_ok());
369    }
370}