use crate::model::{ArgumentSpec, ArgumentType, OptionSpec, ProbeResult};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationError>,
}
#[derive(Debug, Serialize, Clone)]
pub struct ValidationError {
pub error_type: ValidationErrorType,
pub message: String,
pub target: Option<String>,
}
#[derive(Debug, Serialize, Clone)]
pub enum ValidationErrorType {
MissingRequiredArgument,
MissingRequiredOption,
UnknownOption,
OptionMissingArgument,
OptionUnexpectedArgument,
InvalidArgumentType,
UnknownSubcommand,
TooManyArguments,
TooFewArguments,
}
pub fn validate_command(result: &ProbeResult, command: &str, args: &[String]) -> ValidationResult {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if command != result.command {
warnings.push(ValidationError {
error_type: ValidationErrorType::UnknownSubcommand,
message: format!(
"Command name mismatch: expected '{}', got '{}'",
result.command, command
),
target: Some(command.to_string()),
});
}
let parsed = parse_command_args(args, result);
validate_options(result, &parsed, &mut errors, &mut warnings);
validate_arguments(result, &parsed, &mut errors, &mut warnings);
validate_subcommands(result, &parsed, &mut errors, &mut warnings);
ValidationResult {
is_valid: errors.is_empty(),
errors,
warnings,
}
}
struct ParsedArgs {
options: std::collections::HashMap<String, Option<String>>,
subcommands: Vec<String>,
positional_args: Vec<String>,
}
fn parse_command_args(args: &[String], result: &ProbeResult) -> ParsedArgs {
let mut options = std::collections::HashMap::new();
let mut subcommands = Vec::new();
let mut positional_args = Vec::new();
let mut known_options = std::collections::HashSet::new();
for opt in &result.options {
for short in &opt.short_flags {
known_options.insert(short.clone());
}
for long in &opt.long_flags {
known_options.insert(long.clone());
}
}
let known_subcommands: std::collections::HashSet<String> =
result.subcommands.iter().map(|s| s.name.clone()).collect();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg.starts_with("--") {
let (opt_name, value) = if let Some(eq_pos) = arg.find('=') {
(
arg[..eq_pos].to_string(),
Some(arg[eq_pos + 1..].to_string()),
)
} else {
(arg.clone(), None)
};
if value.is_none() && i + 1 < args.len() {
let next = &args[i + 1];
if !next.starts_with('-') && !known_subcommands.contains(next) {
if let Some(opt_spec) = find_option_by_flag(&opt_name, result) {
if opt_spec.takes_argument {
options.insert(opt_name, Some(next.clone()));
i += 2;
continue;
}
}
}
}
options.insert(opt_name, value);
i += 1;
} else if arg.starts_with('-') && arg.len() > 1 {
let chars: Vec<char> = arg.chars().skip(1).collect();
for (idx, ch) in chars.iter().enumerate() {
let opt_name = format!("-{}", ch);
if known_options.contains(&opt_name) {
if let Some(opt_spec) = find_option_by_flag(&opt_name, result) {
if opt_spec.takes_argument {
if idx == chars.len() - 1 && i + 1 < args.len() {
let next = &args[i + 1];
if !next.starts_with('-') {
options.insert(opt_name, Some(next.clone()));
i += 2;
break;
}
}
}
}
options.insert(opt_name, None);
}
}
i += 1;
} else if known_subcommands.contains(arg) {
subcommands.push(arg.clone());
i += 1;
} else {
positional_args.push(arg.clone());
i += 1;
}
}
ParsedArgs {
options,
subcommands,
positional_args,
}
}
fn find_option_by_flag<'a>(flag: &str, result: &'a ProbeResult) -> Option<&'a OptionSpec> {
result.options.iter().find(|opt| {
opt.short_flags.contains(&flag.to_string()) || opt.long_flags.contains(&flag.to_string())
})
}
fn validate_options(
result: &ProbeResult,
parsed: &ParsedArgs,
errors: &mut Vec<ValidationError>,
warnings: &mut Vec<ValidationError>,
) {
let mut known_options = std::collections::HashSet::new();
for opt in &result.options {
for short in &opt.short_flags {
known_options.insert(short.clone());
}
for long in &opt.long_flags {
known_options.insert(long.clone());
}
}
for opt_name in parsed.options.keys() {
if !known_options.contains(opt_name) {
errors.push(ValidationError {
error_type: ValidationErrorType::UnknownOption,
message: format!("Unknown option: {}", opt_name),
target: Some(opt_name.clone()),
});
}
}
for opt in &result.options {
if opt.required {
let found = opt
.short_flags
.iter()
.any(|f| parsed.options.contains_key(f))
|| opt
.long_flags
.iter()
.any(|f| parsed.options.contains_key(f));
if !found {
let opt_name = opt
.long_flags
.first()
.or_else(|| opt.short_flags.first())
.unwrap();
errors.push(ValidationError {
error_type: ValidationErrorType::MissingRequiredOption,
message: format!("Required option missing: {}", opt_name),
target: Some(opt_name.clone()),
});
}
}
if opt.takes_argument {
for flag in &opt.short_flags {
if let Some(value) = parsed.options.get(flag) {
if value.is_none() {
errors.push(ValidationError {
error_type: ValidationErrorType::OptionMissingArgument,
message: format!("Option {} requires an argument", flag),
target: Some(flag.clone()),
});
}
}
}
for flag in &opt.long_flags {
if let Some(value) = parsed.options.get(flag) {
if value.is_none() {
errors.push(ValidationError {
error_type: ValidationErrorType::OptionMissingArgument,
message: format!("Option {} requires an argument", flag),
target: Some(flag.clone()),
});
}
}
}
} else {
for flag in &opt.short_flags {
if let Some(Some(_)) = parsed.options.get(flag) {
warnings.push(ValidationError {
error_type: ValidationErrorType::OptionUnexpectedArgument,
message: format!("Option {} does not take an argument", flag),
target: Some(flag.clone()),
});
}
}
for flag in &opt.long_flags {
if let Some(Some(_)) = parsed.options.get(flag) {
warnings.push(ValidationError {
error_type: ValidationErrorType::OptionUnexpectedArgument,
message: format!("Option {} does not take an argument", flag),
target: Some(flag.clone()),
});
}
}
}
}
}
fn validate_arguments(
result: &ProbeResult,
parsed: &ParsedArgs,
errors: &mut Vec<ValidationError>,
warnings: &mut Vec<ValidationError>,
) {
let required_args: Vec<&ArgumentSpec> = result
.arguments
.iter()
.filter(|a| a.required && !a.variadic)
.collect();
let variadic_args: Vec<&ArgumentSpec> =
result.arguments.iter().filter(|a| a.variadic).collect();
let required_count = required_args.len();
if parsed.positional_args.len() < required_count {
errors.push(ValidationError {
error_type: ValidationErrorType::TooFewArguments,
message: format!(
"Too few arguments: expected at least {}, got {}",
required_count,
parsed.positional_args.len()
),
target: None,
});
}
if variadic_args.is_empty() && parsed.positional_args.len() > required_count {
errors.push(ValidationError {
error_type: ValidationErrorType::TooManyArguments,
message: format!(
"Too many arguments: expected {}, got {}",
required_count,
parsed.positional_args.len()
),
target: None,
});
}
for (idx, arg_value) in parsed.positional_args.iter().enumerate() {
if let Some(arg_spec) = result.arguments.get(idx) {
if let Some(arg_type) = &arg_spec.arg_type {
if !validate_argument_type(arg_value, arg_type) {
warnings.push(ValidationError {
error_type: ValidationErrorType::InvalidArgumentType,
message: format!(
"Argument '{}' may not match expected type {:?}",
arg_value, arg_type
),
target: Some(arg_spec.name.clone()),
});
}
}
}
}
}
fn validate_argument_type(value: &str, arg_type: &ArgumentType) -> bool {
match arg_type {
ArgumentType::Number => value.parse::<f64>().is_ok() || value.parse::<i64>().is_ok(),
ArgumentType::Path => {
value.contains('/') || value.contains('\\') || !value.contains(' ')
}
ArgumentType::Url => value.starts_with("http://") || value.starts_with("https://"),
ArgumentType::Email => value.contains('@') && value.contains('.'),
ArgumentType::String => true, }
}
fn validate_subcommands(
result: &ProbeResult,
parsed: &ParsedArgs,
errors: &mut Vec<ValidationError>,
_warnings: &mut Vec<ValidationError>,
) {
let known_subcommands: std::collections::HashSet<String> =
result.subcommands.iter().map(|s| s.name.clone()).collect();
for subcmd in &parsed.subcommands {
if !known_subcommands.contains(subcmd) {
errors.push(ValidationError {
error_type: ValidationErrorType::UnknownSubcommand,
message: format!("Unknown subcommand: {}", subcmd),
target: Some(subcmd.clone()),
});
}
}
}