ggen_cli_lib/cmds/project/
validate.rs1use clap::Args;
22use ggen_utils::error::Result;
23use std::path::{Component, Path, PathBuf};
24
25#[derive(Args, Debug)]
26pub struct ValidateArgs {
27 pub path: PathBuf,
29
30 #[arg(long, short = 's')]
32 pub schema: Option<PathBuf>,
33
34 #[arg(long, short = 'r')]
36 pub recursive: bool,
37
38 #[arg(long, default_value = "*")]
40 pub pattern: String,
41
42 #[arg(long)]
44 pub strict: bool,
45
46 #[arg(long)]
48 pub verbose: bool,
49
50 #[arg(long)]
52 pub json: bool,
53}
54
55fn validate_path(path: &Path) -> Result<()> {
57 if path.components().any(|c| matches!(c, Component::ParentDir)) {
58 return Err(ggen_utils::error::Error::new(
59 "Path traversal detected: paths containing '..' are not allowed",
60 ));
61 }
62 Ok(())
63}
64
65fn detect_validation_type(path: &Path) -> Result<String> {
67 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
68
69 let validation_type = match extension {
70 "json" => "json-plan",
71 "yaml" | "yml" => "yaml-plan",
72 "toml" => "toml-plan",
73 "rs" => "rust-file",
74 "ts" | "tsx" => "typescript-file",
75 "js" | "jsx" => "javascript-file",
76 "py" => "python-file",
77 _ => "generic",
78 };
79
80 Ok(validation_type.to_string())
81}
82
83pub async fn run(args: &ValidateArgs) -> Result<()> {
85 validate_path(&args.path)?;
87
88 if let Some(schema_path) = &args.schema {
89 validate_path(schema_path)?;
90 if !schema_path.exists() {
91 return Err(ggen_utils::error::Error::new_fmt(format_args!(
92 "Schema file not found: {}",
93 schema_path.display()
94 )));
95 }
96 }
97
98 println!("✅ Validating files...");
99
100 if !args.path.exists() {
102 return Err(ggen_utils::error::Error::new_fmt(format_args!(
103 "Path not found: {}",
104 args.path.display()
105 )));
106 }
107
108 let validation_type = detect_validation_type(&args.path)?;
110
111 if args.verbose {
112 println!("📋 Validation type: {}", validation_type);
113 }
114
115 let mut cmd = std::process::Command::new("cargo");
117 cmd.args(["make", "project-validate"]);
118 cmd.arg("--path").arg(&args.path);
119 cmd.arg("--type").arg(&validation_type);
120
121 if let Some(schema_path) = &args.schema {
122 cmd.arg("--schema").arg(schema_path);
123 }
124
125 if args.recursive {
126 cmd.arg("--recursive");
127 }
128
129 if !args.pattern.is_empty() && args.pattern != "*" {
130 cmd.arg("--pattern").arg(&args.pattern);
131 }
132
133 if args.strict {
134 cmd.arg("--strict");
135 }
136
137 if args.verbose {
138 cmd.arg("--verbose");
139 }
140
141 if args.json {
142 cmd.arg("--json");
143 }
144
145 let output = cmd.output().map_err(ggen_utils::error::Error::from)?;
146
147 if !output.status.success() {
148 let stderr = String::from_utf8_lossy(&output.stderr);
149 return Err(ggen_utils::error::Error::new_fmt(format_args!(
150 "Validation failed: {}",
151 stderr
152 )));
153 }
154
155 let stdout = String::from_utf8_lossy(&output.stdout);
156
157 if args.json {
158 println!("{}", stdout);
159 } else {
160 println!("{}", stdout);
161 println!("✅ Validation completed successfully");
162 }
163
164 Ok(())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_detect_validation_type_json() {
173 let path = Path::new("plan.json");
174 let result = detect_validation_type(path).unwrap();
175 assert_eq!(result, "json-plan");
176 }
177
178 #[test]
179 fn test_detect_validation_type_yaml() {
180 let path = Path::new("config.yaml");
181 let result = detect_validation_type(path).unwrap();
182 assert_eq!(result, "yaml-plan");
183 }
184
185 #[test]
186 fn test_detect_validation_type_rust() {
187 let path = Path::new("main.rs");
188 let result = detect_validation_type(path).unwrap();
189 assert_eq!(result, "rust-file");
190 }
191
192 #[test]
193 fn test_detect_validation_type_generic() {
194 let path = Path::new("README.md");
195 let result = detect_validation_type(path).unwrap();
196 assert_eq!(result, "generic");
197 }
198
199 #[test]
200 fn test_validate_path_safe() {
201 let path = Path::new("src/main.rs");
202 assert!(validate_path(path).is_ok());
203 }
204
205 #[test]
206 fn test_validate_path_traversal() {
207 let path = Path::new("../etc/passwd");
208 assert!(validate_path(path).is_err());
209 }
210
211 #[tokio::test]
212 async fn test_validate_args_path_not_found() {
213 let args = ValidateArgs {
214 path: PathBuf::from("nonexistent.json"),
215 schema: None,
216 recursive: false,
217 pattern: "*".to_string(),
218 strict: false,
219 verbose: false,
220 json: false,
221 };
222
223 let result = run(&args).await;
224 assert!(result.is_err());
225 assert!(result.unwrap_err().to_string().contains("Path not found"));
226 }
227}