1use crate::model::{ArgumentSpec, ArgumentType, OptionSpec, ProbeResult};
2use serde::Serialize;
3
4#[derive(Debug, Serialize)]
6pub struct ValidationResult {
7 pub is_valid: bool,
9 pub errors: Vec<ValidationError>,
11 pub warnings: Vec<ValidationError>,
13}
14
15#[derive(Debug, Serialize, Clone)]
17pub struct ValidationError {
18 pub error_type: ValidationErrorType,
20 pub message: String,
22 pub target: Option<String>,
24}
25
26#[derive(Debug, Serialize, Clone)]
28pub enum ValidationErrorType {
29 MissingRequiredArgument,
31 MissingRequiredOption,
33 UnknownOption,
35 OptionMissingArgument,
37 OptionUnexpectedArgument,
39 InvalidArgumentType,
41 UnknownSubcommand,
43 TooManyArguments,
45 TooFewArguments,
47}
48
49pub fn validate_command(result: &ProbeResult, command: &str, args: &[String]) -> ValidationResult {
100 let mut errors = Vec::new();
101 let mut warnings = Vec::new();
102
103 if command != result.command {
105 warnings.push(ValidationError {
106 error_type: ValidationErrorType::UnknownSubcommand,
107 message: format!(
108 "Command name mismatch: expected '{}', got '{}'",
109 result.command, command
110 ),
111 target: Some(command.to_string()),
112 });
113 }
114
115 let parsed = parse_command_args(args, result);
117
118 validate_options(result, &parsed, &mut errors, &mut warnings);
120
121 validate_arguments(result, &parsed, &mut errors, &mut warnings);
123
124 validate_subcommands(result, &parsed, &mut errors, &mut warnings);
126
127 ValidationResult {
128 is_valid: errors.is_empty(),
129 errors,
130 warnings,
131 }
132}
133
134struct ParsedArgs {
136 options: std::collections::HashMap<String, Option<String>>,
138 subcommands: Vec<String>,
140 positional_args: Vec<String>,
142}
143
144fn parse_command_args(args: &[String], result: &ProbeResult) -> ParsedArgs {
146 let mut options = std::collections::HashMap::new();
147 let mut subcommands = Vec::new();
148 let mut positional_args = Vec::new();
149
150 let mut known_options = std::collections::HashSet::new();
152 for opt in &result.options {
153 for short in &opt.short_flags {
154 known_options.insert(short.clone());
155 }
156 for long in &opt.long_flags {
157 known_options.insert(long.clone());
158 }
159 }
160
161 let known_subcommands: std::collections::HashSet<String> =
163 result.subcommands.iter().map(|s| s.name.clone()).collect();
164
165 let mut i = 0;
166 while i < args.len() {
167 let arg = &args[i];
168
169 if arg.starts_with("--") {
171 let (opt_name, value) = if let Some(eq_pos) = arg.find('=') {
173 (
174 arg[..eq_pos].to_string(),
175 Some(arg[eq_pos + 1..].to_string()),
176 )
177 } else {
178 (arg.clone(), None)
179 };
180
181 if value.is_none() && i + 1 < args.len() {
183 let next = &args[i + 1];
184 if !next.starts_with('-') && !known_subcommands.contains(next) {
185 if let Some(opt_spec) = find_option_by_flag(&opt_name, result) {
187 if opt_spec.takes_argument {
188 options.insert(opt_name, Some(next.clone()));
189 i += 2;
190 continue;
191 }
192 }
193 }
194 }
195
196 options.insert(opt_name, value);
197 i += 1;
198 } else if arg.starts_with('-') && arg.len() > 1 {
199 let chars: Vec<char> = arg.chars().skip(1).collect();
201 for (idx, ch) in chars.iter().enumerate() {
202 let opt_name = format!("-{}", ch);
203 if known_options.contains(&opt_name) {
204 if let Some(opt_spec) = find_option_by_flag(&opt_name, result) {
206 if opt_spec.takes_argument {
207 if idx == chars.len() - 1 && i + 1 < args.len() {
209 let next = &args[i + 1];
210 if !next.starts_with('-') {
211 options.insert(opt_name, Some(next.clone()));
212 i += 2;
213 break;
214 }
215 }
216 }
217 }
218 options.insert(opt_name, None);
219 }
220 }
221 i += 1;
222 } else if known_subcommands.contains(arg) {
223 subcommands.push(arg.clone());
225 i += 1;
226 } else {
227 positional_args.push(arg.clone());
229 i += 1;
230 }
231 }
232
233 ParsedArgs {
234 options,
235 subcommands,
236 positional_args,
237 }
238}
239
240fn find_option_by_flag<'a>(flag: &str, result: &'a ProbeResult) -> Option<&'a OptionSpec> {
242 result.options.iter().find(|opt| {
243 opt.short_flags.contains(&flag.to_string()) || opt.long_flags.contains(&flag.to_string())
244 })
245}
246
247fn validate_options(
249 result: &ProbeResult,
250 parsed: &ParsedArgs,
251 errors: &mut Vec<ValidationError>,
252 warnings: &mut Vec<ValidationError>,
253) {
254 let mut known_options = std::collections::HashSet::new();
256 for opt in &result.options {
257 for short in &opt.short_flags {
258 known_options.insert(short.clone());
259 }
260 for long in &opt.long_flags {
261 known_options.insert(long.clone());
262 }
263 }
264
265 for opt_name in parsed.options.keys() {
267 if !known_options.contains(opt_name) {
268 errors.push(ValidationError {
269 error_type: ValidationErrorType::UnknownOption,
270 message: format!("Unknown option: {}", opt_name),
271 target: Some(opt_name.clone()),
272 });
273 }
274 }
275
276 for opt in &result.options {
278 if opt.required {
279 let found = opt
280 .short_flags
281 .iter()
282 .any(|f| parsed.options.contains_key(f))
283 || opt
284 .long_flags
285 .iter()
286 .any(|f| parsed.options.contains_key(f));
287
288 if !found {
289 let opt_name = opt
290 .long_flags
291 .first()
292 .or_else(|| opt.short_flags.first())
293 .unwrap();
294 errors.push(ValidationError {
295 error_type: ValidationErrorType::MissingRequiredOption,
296 message: format!("Required option missing: {}", opt_name),
297 target: Some(opt_name.clone()),
298 });
299 }
300 }
301
302 if opt.takes_argument {
304 for flag in &opt.short_flags {
305 if let Some(value) = parsed.options.get(flag) {
306 if value.is_none() {
307 errors.push(ValidationError {
308 error_type: ValidationErrorType::OptionMissingArgument,
309 message: format!("Option {} requires an argument", flag),
310 target: Some(flag.clone()),
311 });
312 }
313 }
314 }
315 for flag in &opt.long_flags {
316 if let Some(value) = parsed.options.get(flag) {
317 if value.is_none() {
318 errors.push(ValidationError {
319 error_type: ValidationErrorType::OptionMissingArgument,
320 message: format!("Option {} requires an argument", flag),
321 target: Some(flag.clone()),
322 });
323 }
324 }
325 }
326 } else {
327 for flag in &opt.short_flags {
329 if let Some(Some(_)) = parsed.options.get(flag) {
330 warnings.push(ValidationError {
331 error_type: ValidationErrorType::OptionUnexpectedArgument,
332 message: format!("Option {} does not take an argument", flag),
333 target: Some(flag.clone()),
334 });
335 }
336 }
337 for flag in &opt.long_flags {
338 if let Some(Some(_)) = parsed.options.get(flag) {
339 warnings.push(ValidationError {
340 error_type: ValidationErrorType::OptionUnexpectedArgument,
341 message: format!("Option {} does not take an argument", flag),
342 target: Some(flag.clone()),
343 });
344 }
345 }
346 }
347 }
348}
349
350fn validate_arguments(
352 result: &ProbeResult,
353 parsed: &ParsedArgs,
354 errors: &mut Vec<ValidationError>,
355 warnings: &mut Vec<ValidationError>,
356) {
357 let required_args: Vec<&ArgumentSpec> = result
358 .arguments
359 .iter()
360 .filter(|a| a.required && !a.variadic)
361 .collect();
362
363 let variadic_args: Vec<&ArgumentSpec> =
364 result.arguments.iter().filter(|a| a.variadic).collect();
365
366 let required_count = required_args.len();
368 if parsed.positional_args.len() < required_count {
369 errors.push(ValidationError {
370 error_type: ValidationErrorType::TooFewArguments,
371 message: format!(
372 "Too few arguments: expected at least {}, got {}",
373 required_count,
374 parsed.positional_args.len()
375 ),
376 target: None,
377 });
378 }
379
380 if variadic_args.is_empty() && parsed.positional_args.len() > required_count {
382 errors.push(ValidationError {
383 error_type: ValidationErrorType::TooManyArguments,
384 message: format!(
385 "Too many arguments: expected {}, got {}",
386 required_count,
387 parsed.positional_args.len()
388 ),
389 target: None,
390 });
391 }
392
393 for (idx, arg_value) in parsed.positional_args.iter().enumerate() {
395 if let Some(arg_spec) = result.arguments.get(idx) {
396 if let Some(arg_type) = &arg_spec.arg_type {
397 if !validate_argument_type(arg_value, arg_type) {
398 warnings.push(ValidationError {
399 error_type: ValidationErrorType::InvalidArgumentType,
400 message: format!(
401 "Argument '{}' may not match expected type {:?}",
402 arg_value, arg_type
403 ),
404 target: Some(arg_spec.name.clone()),
405 });
406 }
407 }
408 }
409 }
410}
411
412fn validate_argument_type(value: &str, arg_type: &ArgumentType) -> bool {
414 match arg_type {
415 ArgumentType::Number => value.parse::<f64>().is_ok() || value.parse::<i64>().is_ok(),
416 ArgumentType::Path => {
417 value.contains('/') || value.contains('\\') || !value.contains(' ')
419 }
420 ArgumentType::Url => value.starts_with("http://") || value.starts_with("https://"),
421 ArgumentType::Email => value.contains('@') && value.contains('.'),
422 ArgumentType::String => true, }
424}
425
426fn validate_subcommands(
428 result: &ProbeResult,
429 parsed: &ParsedArgs,
430 errors: &mut Vec<ValidationError>,
431 _warnings: &mut Vec<ValidationError>,
432) {
433 let known_subcommands: std::collections::HashSet<String> =
434 result.subcommands.iter().map(|s| s.name.clone()).collect();
435
436 for subcmd in &parsed.subcommands {
437 if !known_subcommands.contains(subcmd) {
438 errors.push(ValidationError {
439 error_type: ValidationErrorType::UnknownSubcommand,
440 message: format!("Unknown subcommand: {}", subcmd),
441 target: Some(subcmd.clone()),
442 });
443 }
444 }
445}