use std::collections::HashMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::{Argument, BuildError, Example, Flag};
pub type HandlerFn =
Arc<dyn for<'a> Fn(&ParsedCommand<'a>) -> Result<(), Box<dyn std::error::Error>> + Send + Sync>;
#[cfg(feature = "async")]
pub type AsyncHandlerFn = std::sync::Arc<
dyn for<'a> Fn(
&'a ParsedCommand<'a>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<(), Box<dyn std::error::Error + Send + Sync>>,
> + Send
+ 'a,
>,
> + Send
+ Sync,
>;
#[derive(Debug)]
pub struct ParsedCommand<'a> {
pub command: &'a Command,
pub args: HashMap<String, String>,
pub flags: HashMap<String, String>,
}
impl<'a> ParsedCommand<'a> {
pub fn arg(&self, name: &str) -> Option<&str> {
self.args.get(name).map(String::as_str)
}
pub fn flag(&self, name: &str) -> Option<&str> {
self.flags.get(name).map(String::as_str)
}
pub fn flag_bool(&self, name: &str) -> bool {
self.flags.get(name).map(|v| v == "true").unwrap_or(false)
}
pub fn flag_count(&self, name: &str) -> u64 {
match self.flags.get(name) {
None => 0,
Some(v) if v == "true" => 1,
Some(v) if v == "false" => 0,
Some(v) => v.parse().unwrap_or(0),
}
}
pub fn flag_values(&self, name: &str) -> Vec<String> {
match self.flags.get(name) {
None => vec![],
Some(v) => serde_json::from_str::<Vec<String>>(v).unwrap_or_else(|_| vec![v.clone()]),
}
}
pub fn has_flag(&self, name: &str) -> bool {
self.flags.contains_key(name)
}
pub fn arg_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
self.args.get(name).map(|v| v.parse())
}
pub fn flag_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
self.flags.get(name).map(|v| v.parse())
}
pub fn arg_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
self.arg_as(name).and_then(|r| r.ok()).unwrap_or(default)
}
pub fn flag_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
self.flag_as(name).and_then(|r| r.ok()).unwrap_or(default)
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Command {
pub canonical: String,
pub aliases: Vec<String>,
pub spellings: Vec<String>,
pub summary: String,
pub description: String,
pub arguments: Vec<Argument>,
pub flags: Vec<Flag>,
pub examples: Vec<Example>,
pub subcommands: Vec<Command>,
pub best_practices: Vec<String>,
pub anti_patterns: Vec<String>,
pub semantic_aliases: Vec<String>,
#[serde(skip)]
pub handler: Option<HandlerFn>,
#[cfg(feature = "async")]
#[serde(skip)]
pub async_handler: Option<AsyncHandlerFn>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub extra: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclusive_groups: Vec<Vec<String>>,
#[serde(default)]
pub mutating: bool,
}
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut ds = f.debug_struct("Command");
ds.field("canonical", &self.canonical)
.field("aliases", &self.aliases)
.field("spellings", &self.spellings)
.field("summary", &self.summary)
.field("description", &self.description)
.field("arguments", &self.arguments)
.field("flags", &self.flags)
.field("examples", &self.examples)
.field("subcommands", &self.subcommands)
.field("best_practices", &self.best_practices)
.field("anti_patterns", &self.anti_patterns)
.field("semantic_aliases", &self.semantic_aliases)
.field("handler", &self.handler.as_ref().map(|_| "<handler>"));
#[cfg(feature = "async")]
ds.field(
"async_handler",
&self.async_handler.as_ref().map(|_| "<async_handler>"),
);
ds.field("extra", &self.extra)
.field("exclusive_groups", &self.exclusive_groups)
.field("mutating", &self.mutating)
.finish()
}
}
impl PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.canonical == other.canonical
&& self.aliases == other.aliases
&& self.spellings == other.spellings
&& self.summary == other.summary
&& self.description == other.description
&& self.arguments == other.arguments
&& self.flags == other.flags
&& self.examples == other.examples
&& self.subcommands == other.subcommands
&& self.best_practices == other.best_practices
&& self.anti_patterns == other.anti_patterns
&& self.semantic_aliases == other.semantic_aliases
&& self.extra == other.extra
&& self.exclusive_groups == other.exclusive_groups
&& self.mutating == other.mutating
}
}
impl Eq for Command {}
impl std::hash::Hash for Command {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.canonical.hash(state);
self.aliases.hash(state);
self.spellings.hash(state);
self.summary.hash(state);
self.description.hash(state);
self.arguments.hash(state);
self.flags.hash(state);
self.examples.hash(state);
self.subcommands.hash(state);
self.best_practices.hash(state);
self.anti_patterns.hash(state);
self.semantic_aliases.hash(state);
{
let mut keys: Vec<&String> = self.extra.keys().collect();
keys.sort();
for k in keys {
k.hash(state);
self.extra[k].to_string().hash(state);
}
}
self.exclusive_groups.hash(state);
self.mutating.hash(state);
}
}
impl PartialOrd for Command {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Command {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.canonical
.cmp(&other.canonical)
.then_with(|| self.summary.cmp(&other.summary))
.then_with(|| self.aliases.cmp(&other.aliases))
}
}
impl Command {
pub fn builder(canonical: impl Into<String>) -> CommandBuilder {
CommandBuilder {
canonical: canonical.into(),
aliases: Vec::new(),
spellings: Vec::new(),
summary: String::new(),
description: String::new(),
arguments: Vec::new(),
flags: Vec::new(),
examples: Vec::new(),
subcommands: Vec::new(),
best_practices: Vec::new(),
anti_patterns: Vec::new(),
semantic_aliases: Vec::new(),
handler: None,
#[cfg(feature = "async")]
async_handler: None,
extra: HashMap::new(),
exclusive_groups: Vec::new(),
mutating: false,
}
}
pub(crate) fn matchable_strings(&self) -> Vec<String> {
let mut v = vec![self.canonical.to_lowercase()];
v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
v.extend(self.spellings.iter().map(|s| s.to_lowercase()));
v
}
pub(crate) fn prefix_matchable_strings(&self) -> Vec<String> {
let mut v = vec![self.canonical.to_lowercase()];
v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
v
}
}
pub struct CommandBuilder {
canonical: String,
aliases: Vec<String>,
spellings: Vec<String>,
summary: String,
description: String,
arguments: Vec<Argument>,
flags: Vec<Flag>,
examples: Vec<Example>,
subcommands: Vec<Command>,
best_practices: Vec<String>,
anti_patterns: Vec<String>,
semantic_aliases: Vec<String>,
handler: Option<HandlerFn>,
#[cfg(feature = "async")]
async_handler: Option<AsyncHandlerFn>,
extra: HashMap<String, serde_json::Value>,
exclusive_groups: Vec<Vec<String>>,
mutating: bool,
}
impl CommandBuilder {
pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.aliases = aliases.into_iter().map(Into::into).collect();
self
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
pub fn spellings(mut self, spellings: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.spellings = spellings.into_iter().map(Into::into).collect();
self
}
pub fn spelling(mut self, s: impl Into<String>) -> Self {
self.spellings.push(s.into());
self
}
pub fn summary(mut self, s: impl Into<String>) -> Self {
self.summary = s.into();
self
}
pub fn description(mut self, d: impl Into<String>) -> Self {
self.description = d.into();
self
}
pub fn argument(mut self, arg: Argument) -> Self {
self.arguments.push(arg);
self
}
pub fn flag(mut self, flag: Flag) -> Self {
self.flags.push(flag);
self
}
pub fn example(mut self, example: Example) -> Self {
self.examples.push(example);
self
}
pub fn subcommand(mut self, cmd: Command) -> Self {
self.subcommands.push(cmd);
self
}
pub fn best_practice(mut self, bp: impl Into<String>) -> Self {
self.best_practices.push(bp.into());
self
}
pub fn anti_pattern(mut self, ap: impl Into<String>) -> Self {
self.anti_patterns.push(ap.into());
self
}
pub fn semantic_aliases(
mut self,
aliases: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.semantic_aliases = aliases.into_iter().map(Into::into).collect();
self
}
pub fn semantic_alias(mut self, s: impl Into<String>) -> Self {
self.semantic_aliases.push(s.into());
self
}
pub fn handler(mut self, h: HandlerFn) -> Self {
self.handler = Some(h);
self
}
#[cfg(feature = "async")]
pub fn async_handler(mut self, h: AsyncHandlerFn) -> Self {
self.async_handler = Some(h);
self
}
pub fn meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.extra.insert(key.into(), value);
self
}
pub fn exclusive(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.exclusive_groups
.push(flags.into_iter().map(Into::into).collect());
self
}
pub fn mutating(mut self) -> Self {
self.mutating = true;
self
}
pub fn build(self) -> Result<Command, BuildError> {
if self.canonical.trim().is_empty() {
return Err(BuildError::EmptyCanonical);
}
let canonical_lower = self.canonical.to_lowercase();
for alias in &self.aliases {
if alias.to_lowercase() == canonical_lower {
return Err(BuildError::AliasEqualsCanonical(alias.clone()));
}
}
let mut seen_aliases = std::collections::HashSet::new();
for alias in &self.aliases {
let key = alias.to_lowercase();
if !seen_aliases.insert(key) {
return Err(BuildError::DuplicateAlias(alias.clone()));
}
}
let mut seen_flag_names = std::collections::HashSet::new();
for flag in &self.flags {
if !seen_flag_names.insert(flag.name.clone()) {
return Err(BuildError::DuplicateFlagName(flag.name.clone()));
}
}
let mut seen_short_flags = std::collections::HashSet::new();
for flag in &self.flags {
if let Some(c) = flag.short {
if !seen_short_flags.insert(c) {
return Err(BuildError::DuplicateShortFlag(c));
}
}
}
let mut seen_arg_names = std::collections::HashSet::new();
for arg in &self.arguments {
if !seen_arg_names.insert(arg.name.clone()) {
return Err(BuildError::DuplicateArgumentName(arg.name.clone()));
}
}
let mut seen_sub_names = std::collections::HashSet::new();
for sub in &self.subcommands {
if !seen_sub_names.insert(sub.canonical.clone()) {
return Err(BuildError::DuplicateSubcommandName(sub.canonical.clone()));
}
}
for (i, arg) in self.arguments.iter().enumerate() {
if arg.variadic && i != self.arguments.len() - 1 {
return Err(BuildError::VariadicNotLast(arg.name.clone()));
}
}
for flag in &self.flags {
if let Some(choices) = &flag.choices {
if choices.is_empty() {
return Err(BuildError::EmptyChoices(flag.name.clone()));
}
}
}
for group in &self.exclusive_groups {
if group.len() < 2 {
return Err(BuildError::ExclusiveGroupTooSmall);
}
for flag_name in group {
if !self.flags.iter().any(|f| &f.name == flag_name) {
return Err(BuildError::ExclusiveGroupUnknownFlag(flag_name.clone()));
}
}
}
Ok(Command {
canonical: self.canonical,
aliases: self.aliases,
spellings: self.spellings,
summary: self.summary,
description: self.description,
arguments: self.arguments,
flags: self.flags,
examples: self.examples,
subcommands: self.subcommands,
best_practices: self.best_practices,
anti_patterns: self.anti_patterns,
semantic_aliases: self.semantic_aliases,
handler: self.handler,
#[cfg(feature = "async")]
async_handler: self.async_handler,
extra: self.extra,
exclusive_groups: self.exclusive_groups,
mutating: self.mutating,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Argument, Flag};
fn make_simple_cmd() -> Command {
Command::builder("run")
.alias("r")
.spelling("RUN")
.summary("Run something")
.description("Runs the thing.")
.build()
.unwrap()
}
#[test]
fn test_builder_happy_path() {
let cmd = make_simple_cmd();
assert_eq!(cmd.canonical, "run");
assert_eq!(cmd.aliases, vec!["r"]);
assert_eq!(cmd.spellings, vec!["RUN"]);
}
#[test]
fn test_builder_empty_canonical() {
assert_eq!(
Command::builder("").build().unwrap_err(),
BuildError::EmptyCanonical
);
assert_eq!(
Command::builder(" ").build().unwrap_err(),
BuildError::EmptyCanonical
);
}
#[test]
fn test_partial_eq_ignores_handler() {
let cmd1 = Command::builder("run").build().unwrap();
let mut cmd2 = cmd1.clone();
cmd2.handler = Some(Arc::new(|_| Ok(())));
assert_eq!(cmd1, cmd2);
}
#[test]
fn test_serde_round_trip_skips_handler() {
let cmd = Command::builder("deploy")
.summary("Deploy the app")
.argument(
Argument::builder("env")
.description("target env")
.required()
.build()
.unwrap(),
)
.flag(
Flag::builder("dry-run")
.description("dry run mode")
.build()
.unwrap(),
)
.handler(Arc::new(|_| Ok(())))
.build()
.unwrap();
let json = serde_json::to_string(&cmd).unwrap();
let de: Command = serde_json::from_str(&json).unwrap();
assert_eq!(cmd, de);
assert!(de.handler.is_none());
}
#[test]
fn test_matchable_strings() {
let cmd = Command::builder("Git")
.alias("g")
.spelling("GIT")
.build()
.unwrap();
let matchables = cmd.matchable_strings();
assert!(matchables.contains(&"git".to_string()));
assert!(matchables.contains(&"g".to_string()));
assert!(matchables.contains(&"git".to_string())); }
#[test]
fn test_clone_shares_handler() {
let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let called_clone = called.clone();
let cmd = Command::builder("x")
.handler(Arc::new(move |_| {
called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
}))
.build()
.unwrap();
let cmd2 = cmd.clone();
assert!(std::sync::Arc::ptr_eq(
cmd.handler.as_ref().unwrap(),
cmd2.handler.as_ref().unwrap()
));
}
#[test]
fn test_duplicate_alias_rejected() {
let err = Command::builder("cmd")
.alias("a")
.alias("a")
.build()
.unwrap_err();
assert!(matches!(err, BuildError::DuplicateAlias(_)));
}
#[test]
fn test_alias_equals_canonical_rejected() {
let err = Command::builder("deploy")
.alias("deploy")
.build()
.unwrap_err();
assert!(matches!(err, BuildError::AliasEqualsCanonical(_)));
}
#[test]
fn test_duplicate_flag_name_rejected() {
let flag = Flag::builder("verbose").build().unwrap();
let err = Command::builder("cmd")
.flag(flag.clone())
.flag(flag)
.build()
.unwrap_err();
assert!(matches!(err, BuildError::DuplicateFlagName(_)));
}
#[test]
fn test_duplicate_short_flag_rejected() {
let f1 = Flag::builder("verbose").short('v').build().unwrap();
let f2 = Flag::builder("version").short('v').build().unwrap();
let err = Command::builder("cmd")
.flag(f1)
.flag(f2)
.build()
.unwrap_err();
assert!(matches!(err, BuildError::DuplicateShortFlag('v')));
}
#[test]
fn test_duplicate_argument_name_rejected() {
let arg = Argument::builder("env").build().unwrap();
let err = Command::builder("cmd")
.argument(arg.clone())
.argument(arg)
.build()
.unwrap_err();
assert!(matches!(err, BuildError::DuplicateArgumentName(_)));
}
#[test]
fn test_duplicate_subcommand_name_rejected() {
let sub = Command::builder("add").build().unwrap();
let err = Command::builder("remote")
.subcommand(sub.clone())
.subcommand(sub)
.build()
.unwrap_err();
assert!(matches!(err, BuildError::DuplicateSubcommandName(_)));
}
#[test]
fn test_variadic_must_be_last() {
let variadic = Argument::builder("files").variadic().build().unwrap();
let after = Argument::builder("extra").build().unwrap();
let err = Command::builder("cmd")
.argument(variadic)
.argument(after)
.build()
.unwrap_err();
assert!(matches!(err, BuildError::VariadicNotLast(_)));
}
#[test]
fn test_meta_field_serde() {
let cmd = Command::builder("x")
.meta("role", serde_json::json!("admin"))
.build()
.unwrap();
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("admin"));
let de: Command = serde_json::from_str(&json).unwrap();
assert_eq!(de.extra["role"], serde_json::json!("admin"));
}
#[test]
fn test_meta_empty_not_serialized() {
let cmd = Command::builder("x").build().unwrap();
let json = serde_json::to_string(&cmd).unwrap();
assert!(!json.contains("extra"));
}
#[test]
fn test_semantic_alias_builder() {
let cmd = Command::builder("deploy")
.semantic_alias("release to production")
.semantic_alias("push to environment")
.build()
.unwrap();
assert_eq!(
cmd.semantic_aliases,
vec!["release to production", "push to environment"]
);
}
#[test]
fn test_semantic_aliases_bulk_builder() {
let cmd = Command::builder("deploy")
.semantic_aliases(["release to production", "push to environment"])
.build()
.unwrap();
assert_eq!(
cmd.semantic_aliases,
vec!["release to production", "push to environment"]
);
}
#[test]
fn test_semantic_alias_not_in_canonical_aliases() {
let cmd = Command::builder("deploy")
.alias("d")
.semantic_alias("release to production")
.build()
.unwrap();
assert_eq!(cmd.aliases, vec!["d"]);
assert_eq!(cmd.semantic_aliases, vec!["release to production"]);
assert!(!cmd.aliases.contains(&"release to production".to_string()));
assert!(!cmd.semantic_aliases.contains(&"d".to_string()));
}
#[test]
fn test_debug_impl_includes_fields() {
let cmd = Command::builder("debug-test")
.alias("dt")
.spelling("DEBUGTEST")
.summary("Test debug")
.description("A debug test command")
.handler(Arc::new(|_| Ok(())))
.build()
.unwrap();
let s = format!("{:?}", cmd);
assert!(s.contains("debug-test"));
assert!(s.contains("dt"));
assert!(s.contains("Test debug"));
assert!(s.contains("<handler>"));
assert!(s.contains("DEBUGTEST"));
}
#[test]
fn test_hash_deterministic() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let cmd1 = Command::builder("hash-test")
.summary("Hash")
.meta("key", serde_json::json!("value"))
.build()
.unwrap();
let cmd2 = Command::builder("hash-test")
.summary("Hash")
.meta("key", serde_json::json!("value"))
.build()
.unwrap();
let mut h1 = DefaultHasher::new();
cmd1.hash(&mut h1);
let mut h2 = DefaultHasher::new();
cmd2.hash(&mut h2);
assert_eq!(h1.finish(), h2.finish());
}
#[test]
fn test_ord_by_canonical() {
let a = Command::builder("alpha").build().unwrap();
let b = Command::builder("beta").build().unwrap();
assert!(a < b);
assert!(b > a);
assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
}
#[test]
fn test_ord_same_canonical_by_summary() {
let a = Command::builder("cmd").summary("Alpha").build().unwrap();
let b = Command::builder("cmd").summary("Beta").build().unwrap();
assert!(a < b);
}
#[test]
fn test_aliases_builder_method() {
let cmd = Command::builder("cmd")
.aliases(["a", "b", "c"])
.build()
.unwrap();
assert_eq!(cmd.aliases, vec!["a", "b", "c"]);
}
#[test]
fn test_spellings_builder_method() {
let cmd = Command::builder("deploy")
.spellings(["DEPLOY", "dploy"])
.build()
.unwrap();
assert_eq!(cmd.spellings, vec!["DEPLOY", "dploy"]);
}
#[test]
fn test_best_practice_and_anti_pattern_builder() {
let cmd = Command::builder("cmd")
.best_practice("Always dry-run first")
.anti_pattern("Deploy on Fridays")
.build()
.unwrap();
assert_eq!(cmd.best_practices, vec!["Always dry-run first"]);
assert_eq!(cmd.anti_patterns, vec!["Deploy on Fridays"]);
}
#[test]
fn test_exclusive_group_too_small() {
use crate::model::BuildError;
let result = Command::builder("cmd")
.flag(Flag::builder("json").build().unwrap())
.exclusive(["json"])
.build();
assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
}
#[test]
fn test_exclusive_group_unknown_flag() {
use crate::model::BuildError;
let result = Command::builder("cmd")
.flag(Flag::builder("json").build().unwrap())
.exclusive(["json", "nonexistent"])
.build();
assert!(matches!(
result,
Err(BuildError::ExclusiveGroupUnknownFlag(_))
));
}
#[test]
fn test_parsed_command_helpers() {
use crate::{Argument, Flag, Parser};
let cmd = Command::builder("serve")
.argument(Argument::builder("host").required().build().unwrap())
.flag(
Flag::builder("port")
.takes_value()
.default_value("8080")
.build()
.unwrap(),
)
.flag(Flag::builder("verbose").build().unwrap())
.build()
.unwrap();
let cmds = vec![cmd];
let parser = Parser::new(&cmds);
let parsed = parser.parse(&["serve", "localhost", "--verbose"]).unwrap();
assert_eq!(parsed.arg("host"), Some("localhost"));
assert_eq!(parsed.arg("missing"), None);
assert_eq!(parsed.flag("port"), Some("8080"));
assert_eq!(parsed.flag("missing"), None);
assert!(parsed.flag_bool("verbose"));
assert!(!parsed.flag_bool("missing"));
assert_eq!(parsed.flag_count("verbose"), 1);
assert_eq!(parsed.flag_count("missing"), 0);
assert_eq!(parsed.flag_values("port"), vec!["8080"]);
assert!(parsed.flag_values("missing").is_empty());
assert!(parsed.has_flag("port"));
assert!(!parsed.has_flag("missing"));
let port: u16 = parsed.flag_as("port").unwrap().unwrap();
assert_eq!(port, 8080);
let port_or: u16 = parsed.flag_as_or("port", 9000);
assert_eq!(port_or, 8080);
let missing_or: u16 = parsed.flag_as_or("missing", 9000);
assert_eq!(missing_or, 9000);
let host: String = parsed.arg_as("host").unwrap().unwrap();
assert_eq!(host, "localhost");
let missing_as: Option<Result<u32, _>> = parsed.arg_as("missing");
assert!(missing_as.is_none());
let arg_or: String = parsed.arg_as_or("host", "default".to_string());
assert_eq!(arg_or, "localhost");
let missing_arg_or: String = parsed.arg_as_or("missing", "default".to_string());
assert_eq!(missing_arg_or, "default");
}
#[test]
fn test_flag_count_false_returns_zero() {
use crate::{Flag, Parser};
let cmd = Command::builder("cmd")
.flag(Flag::builder("verbose").build().unwrap())
.build()
.unwrap();
let cmds = vec![cmd];
let parser = Parser::new(&cmds);
let parsed = parser.parse(&["cmd", "--no-verbose"]).unwrap();
assert_eq!(parsed.flag_count("verbose"), 0);
}
#[test]
fn test_flag_values_json_array() {
use crate::{Flag, Parser};
let cmd = Command::builder("cmd")
.flag(
Flag::builder("tag")
.takes_value()
.repeatable()
.build()
.unwrap(),
)
.build()
.unwrap();
let cmds = vec![cmd];
let parser = Parser::new(&cmds);
let parsed = parser.parse(&["cmd", "--tag=alpha", "--tag=beta"]).unwrap();
let values = parsed.flag_values("tag");
assert_eq!(values, vec!["alpha", "beta"]);
}
#[test]
fn test_mutating_builder_sets_field() {
let non_mutating = Command::builder("list").build().unwrap();
assert!(!non_mutating.mutating, "default should be non-mutating");
let mutating = Command::builder("delete")
.summary("Delete a resource")
.mutating()
.build()
.unwrap();
assert!(mutating.mutating, "mutating() should set mutating to true");
}
#[test]
fn test_mutating_serde_round_trip() {
let cmd = Command::builder("delete")
.summary("Delete a resource")
.mutating()
.build()
.unwrap();
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("\"mutating\":true"), "JSON should include mutating:true");
let de: Command = serde_json::from_str(&json).unwrap();
assert!(de.mutating, "deserialized command should have mutating=true");
}
#[test]
fn test_non_mutating_serde_default() {
let cmd = Command::builder("list").build().unwrap();
let json = serde_json::to_string(&cmd).unwrap();
let de: Command = serde_json::from_str(&json).unwrap();
assert!(!de.mutating, "deserialized non-mutating command should have mutating=false");
}
}