use crate::{
Data, Error, Site, SiteConf, Valid, Validate,
errors::{ErrorCommandContext, ErrorConf},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::args::{parse_args, parse_schema_to_args};
use super::error::CommandError;
use super::registry::CommandRegistry;
use super::types::{CommandConf, command};
async fn make_site() -> Site {
let conf = SiteConf::default().log_init(false);
let bundle = crate::bundles::Bundle::new();
Site::build(conf, bundle).await.unwrap()
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
struct TestArgs {
name: String,
age: i32,
#[serde(default)]
verbose: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
struct ParserArgs {
name: String,
age: i32,
#[serde(default)]
verbose: bool,
tags: Vec<String>,
}
fn command_arg_defs<T: JsonSchema>() -> Vec<super::args::CommandArg> {
let mut settings = schemars::generate::SchemaSettings::default();
settings.inline_subschemas = true;
let mut generator = schemars::SchemaGenerator::new(settings);
let schema = generator.subschema_for::<T>();
parse_schema_to_args(&schema).unwrap()
}
fn test_command() -> super::Command {
async fn handler(_args: Data<TestArgs>) -> Result<(), Error> {
Ok(())
}
command::<TestArgs, _, _>(handler, CommandConf::new("test")).unwrap()
}
#[tokio::test]
async fn test_execute_command() {
async fn handler(args: Data<TestArgs>) -> Result<(), Error> {
assert_eq!(args.name, "Alice");
assert_eq!(args.age, 30);
assert!(args.verbose);
Ok(())
}
let mut registry = CommandRegistry::new();
let cmd = command::<TestArgs, _, _>(handler, CommandConf::new("test")).unwrap();
registry.register(cmd).unwrap();
let site = make_site().await;
let result = registry
.execute(
"test",
&["--name", "Alice", "--age", "30", "--verbose"],
site,
)
.await;
assert!(result.is_ok());
}
#[test]
fn test_parse_arrays_booleans_and_scalars() {
let args = command_arg_defs::<ParserArgs>();
let parsed = parse_args::<ParserArgs>(
"parse",
&[
"--name",
"Alice",
"--age",
"30",
"--tags",
"api",
"web",
"--verbose",
"false",
"--tags",
"admin",
],
&args,
)
.unwrap();
assert_eq!(
parsed,
ParserArgs {
name: "Alice".to_string(),
age: 30,
verbose: false,
tags: vec!["api".to_string(), "web".to_string(), "admin".to_string()],
}
);
}
#[test]
fn test_parse_no_bool_flag() {
let args = command_arg_defs::<ParserArgs>();
let parsed = parse_args::<ParserArgs>(
"parse",
&[
"--name",
"Alice",
"--age",
"30",
"--tags",
"api",
"--no-verbose",
],
&args,
)
.unwrap();
assert!(!parsed.verbose);
}
#[test]
fn test_parse_errors_are_specific() {
let args = command_arg_defs::<ParserArgs>();
let err = parse_args::<ParserArgs>("parse", &["--unknown"], &args).unwrap_err();
assert!(matches!(
err,
CommandError::UnknownFlag {
command,
flag
} if command == "parse" && flag == "unknown"
));
let err = parse_args::<ParserArgs>("parse", &["--name", "Alice", "--tags", "api"], &args)
.unwrap_err();
assert!(err.to_string().contains("Missing required argument: --age"));
let err = parse_args::<ParserArgs>(
"parse",
&["--name", "Alice", "--age", "not-int", "--tags", "api"],
&args,
)
.unwrap_err();
assert!(matches!(
err,
CommandError::ParseError {
flag,
expected_type,
..
} if flag == "age" && expected_type == "integer"
));
}
#[tokio::test]
async fn test_duplicate_and_reserved_command_names_fail() {
let mut registry = CommandRegistry::new();
registry.register(test_command()).unwrap();
let duplicate = registry.register(test_command()).unwrap_err();
assert!(matches!(duplicate, CommandError::AlreadyExists(name) if name == "test"));
async fn help_handler(_args: Data<TestArgs>) -> Result<(), Error> {
Ok(())
}
let reserved = command::<TestArgs, _, _>(help_handler, CommandConf::new("help")).unwrap();
let err = CommandRegistry::new().register(reserved).unwrap_err();
assert!(matches!(err, CommandError::AlreadyExists(name) if name == "help"));
}
#[tokio::test]
async fn test_unknown_command_mentions_help() {
let registry = CommandRegistry::new();
let site = make_site().await;
let err = registry.execute("missing", &[], site).await.unwrap_err();
assert!(matches!(err, CommandError::UnknownCommand(ref name) if name == "missing"));
assert!(err.to_string().contains("help"));
}
#[test]
fn test_help_is_sorted_and_uses_descriptions() {
async fn alpha(_args: Data<TestArgs>) -> Result<(), Error> {
Ok(())
}
async fn beta(_args: Data<TestArgs>) -> Result<(), Error> {
Ok(())
}
let mut registry = CommandRegistry::new();
registry
.register(
command::<TestArgs, _, _>(beta, CommandConf::new("beta").description("Beta command."))
.unwrap(),
)
.unwrap();
registry
.register(
command::<TestArgs, _, _>(
alpha,
CommandConf::new("alpha").description("Alpha command."),
)
.unwrap(),
)
.unwrap();
let help = registry.execute_help();
assert!(help.find("alpha").unwrap() < help.find("beta").unwrap());
assert!(help.contains("Alpha command."));
assert!(help.contains("Beta command."));
let command_help = registry.generate_help("alpha").unwrap();
assert!(command_help.find("--age").unwrap() < command_help.find("--name").unwrap());
assert!(command_help.contains("Alpha command."));
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Validate)]
struct ValidatedArgs {
#[validate(email)]
email: String,
#[validate(min_length = 3)]
name: String,
}
#[tokio::test]
async fn test_valid_data_validates_command_args() {
async fn handler(Valid(Data(args)): Valid<Data<ValidatedArgs>>) -> Result<(), Error> {
assert_eq!(args.email, "person@example.com");
Ok(())
}
let mut registry = CommandRegistry::new();
registry
.register(command::<ValidatedArgs, _, _>(handler, CommandConf::new("create-user")).unwrap())
.unwrap();
let site = make_site().await;
registry
.execute(
"create-user",
&["--email", "person@example.com", "--name", "Ada"],
site.clone(),
)
.await
.unwrap();
let err = registry
.execute(
"create-user",
&["--email", "not-email", "--name", "Al"],
site,
)
.await
.unwrap_err();
let CommandError::Validation(_) = err else {
panic!("expected validation error, got {err:?}");
};
let rendered = ErrorConf::default().render_command(
ErrorCommandContext {
command: "create-user".to_string(),
args: vec![],
},
err.to_view(),
);
assert!(rendered.contains("Validation failed for command 'create-user'"));
assert!(rendered.contains("--email"));
assert!(rendered.contains("--name"));
let help = registry.generate_help("create-user").unwrap();
assert!(help.contains("min length: 3"));
}
#[test]
fn test_custom_command_error_renderer_receives_error_view() {
let err = CommandError::MissingRequired {
flag: "name".to_string(),
};
let rendered = ErrorConf::default()
.command(|ctx, view| {
format!(
"{}:{}:{}",
ctx.command,
view.code,
view.message.contains("--name")
)
})
.render_command(
ErrorCommandContext {
command: "greet".to_string(),
args: vec![],
},
err.to_view(),
);
assert_eq!(rendered, "greet:command_parse_error:true");
}