Skip to main content

argot_cmd/model/
command.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5
6use super::{Argument, BuildError, Example, Flag};
7
8/// A handler function that can be registered on a [`Command`].
9///
10/// The function is stored in an [`Arc`] so that [`Command::clone`] only
11/// increments a reference count rather than copying the closure. The
12/// higher-ranked trait bound (`for<'a>`) allows the handler to be called with
13/// a [`ParsedCommand`] of any lifetime, which is required because the parsed
14/// command borrows from the command tree at call time.
15///
16/// # Examples
17///
18/// ```
19/// # use std::sync::Arc;
20/// # use argot_cmd::HandlerFn;
21/// let handler: HandlerFn = Arc::new(|parsed| {
22///     println!("running command: {}", parsed.command.canonical);
23///     Ok(())
24/// });
25/// ```
26pub type HandlerFn =
27    Arc<dyn for<'a> Fn(&ParsedCommand<'a>) -> Result<(), Box<dyn std::error::Error>> + Send + Sync>;
28
29/// An async handler function that can be registered on a [`Command`].
30///
31/// The function receives a [`ParsedCommand`] reference and returns a
32/// pinned, boxed future. Use [`Command::builder`] → [`CommandBuilder::async_handler`]
33/// to register one.
34///
35/// # Feature
36///
37/// Requires the `async` feature flag.
38///
39/// # Examples
40///
41/// ```
42/// # #[cfg(feature = "async")] {
43/// use std::sync::Arc;
44/// use argot_cmd::AsyncHandlerFn;
45///
46/// let handler: AsyncHandlerFn = Arc::new(|parsed| Box::pin(async move {
47///     println!("async command: {}", parsed.command.canonical);
48///     Ok(())
49/// }));
50/// # }
51/// ```
52#[cfg(feature = "async")]
53pub type AsyncHandlerFn = std::sync::Arc<
54    dyn for<'a> Fn(
55            &'a ParsedCommand<'a>,
56        ) -> std::pin::Pin<
57            Box<
58                dyn std::future::Future<
59                        Output = Result<(), Box<dyn std::error::Error + Send + Sync>>,
60                    > + Send
61                    + 'a,
62            >,
63        > + Send
64        + Sync,
65>;
66
67/// The result of successfully parsing an invocation against a [`Command`].
68///
69/// `ParsedCommand` borrows the matched [`Command`] from the registry (lifetime
70/// `'a`) and owns the resolved argument and flag value maps. Keys in both maps
71/// are the canonical names of the argument/flag definitions.
72///
73/// # Examples
74///
75/// ```
76/// # use argot_cmd::{Command, Argument, Parser};
77/// let cmd = Command::builder("get")
78///     .argument(
79///         Argument::builder("id")
80///             .required()
81///             .build()
82///             .unwrap(),
83///     )
84///     .build()
85///     .unwrap();
86///
87/// let cmds = vec![cmd];
88/// let parser = Parser::new(&cmds);
89/// let parsed = parser.parse(&["get", "42"]).unwrap();
90///
91/// assert_eq!(parsed.command.canonical, "get");
92/// assert_eq!(parsed.args["id"], "42");
93/// ```
94#[derive(Debug)]
95pub struct ParsedCommand<'a> {
96    /// The matched [`Command`] definition, borrowed from the registry.
97    pub command: &'a Command,
98    /// Resolved positional argument values, keyed by argument name.
99    pub args: HashMap<String, String>,
100    /// Resolved flag values, keyed by flag name.
101    ///
102    /// Boolean flags (those without `takes_value`) are stored as `"true"`.
103    /// Flags that were not provided but have a `default` value are included
104    /// with that default.
105    pub flags: HashMap<String, String>,
106}
107
108impl<'a> ParsedCommand<'a> {
109    /// Return a positional argument value by name, or `None` if absent.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// # use argot_cmd::{Argument, Command, Parser};
115    /// let cmd = Command::builder("get")
116    ///     .argument(Argument::builder("id").required().build().unwrap())
117    ///     .build().unwrap();
118    /// let cmds = vec![cmd];
119    /// let parsed = Parser::new(&cmds).parse(&["get", "42"]).unwrap();
120    /// assert_eq!(parsed.arg("id"), Some("42"));
121    /// assert_eq!(parsed.arg("missing"), None);
122    /// ```
123    pub fn arg(&self, name: &str) -> Option<&str> {
124        self.args.get(name).map(String::as_str)
125    }
126
127    /// Return a flag value by name, or `None` if absent.
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// # use argot_cmd::{Command, Flag, Parser};
133    /// let cmd = Command::builder("run")
134    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
135    ///     .build().unwrap();
136    /// let cmds = vec![cmd];
137    /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
138    /// assert_eq!(parsed.flag("output"), Some("json"));
139    /// ```
140    pub fn flag(&self, name: &str) -> Option<&str> {
141        self.flags.get(name).map(String::as_str)
142    }
143
144    /// Return `true` if a boolean flag is present and set to `"true"`, `false` otherwise.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use argot_cmd::{Command, Flag, Parser};
150    /// let cmd = Command::builder("build")
151    ///     .flag(Flag::builder("release").build().unwrap())
152    ///     .build().unwrap();
153    /// let cmds = vec![cmd];
154    /// let parsed = Parser::new(&cmds).parse(&["build", "--release"]).unwrap();
155    /// assert!(parsed.flag_bool("release"));
156    /// assert!(!parsed.flag_bool("missing"));
157    /// ```
158    pub fn flag_bool(&self, name: &str) -> bool {
159        self.flags.get(name).map(|v| v == "true").unwrap_or(false)
160    }
161
162    /// Return the occurrence count for a repeatable boolean flag (stored as a numeric string).
163    /// Returns `0` if the flag was not provided.
164    ///
165    /// For flags stored as `"true"` (non-repeatable), returns `1`.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// # use argot_cmd::{Command, Flag, Parser};
171    /// // With a repeatable flag (see Flag::repeatable), -v -v -v → flag_count("verbose") == 3
172    /// // With a normal flag, --verbose → flag_count("verbose") == 1 (stored as "true")
173    /// let cmd = Command::builder("run")
174    ///     .flag(Flag::builder("verbose").short('v').build().unwrap())
175    ///     .build().unwrap();
176    /// let cmds = vec![cmd];
177    /// let parsed = Parser::new(&cmds).parse(&["run", "-v"]).unwrap();
178    /// // Non-repeatable flag stores "true"; flag_count returns 1
179    /// assert_eq!(parsed.flag_count("verbose"), 1);
180    /// assert_eq!(parsed.flag_count("missing"), 0);
181    /// ```
182    pub fn flag_count(&self, name: &str) -> u64 {
183        match self.flags.get(name) {
184            None => 0,
185            Some(v) if v == "true" => 1,
186            Some(v) if v == "false" => 0,
187            Some(v) => v.parse().unwrap_or(0),
188        }
189    }
190
191    /// Return all values for a repeatable value-taking flag as a `Vec<String>`.
192    ///
193    /// - If the flag was provided multiple times (repeatable), the stored JSON array is
194    ///   deserialized into a `Vec`.
195    /// - If the flag was provided once (non-repeatable), returns a single-element `Vec`.
196    /// - If the flag was not provided, returns an empty `Vec`.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// # use argot_cmd::{Command, Flag, Parser};
202    /// let cmd = Command::builder("run")
203    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
204    ///     .build().unwrap();
205    /// let cmds = vec![cmd];
206    /// let parsed = Parser::new(&cmds).parse(&["run", "--output=json"]).unwrap();
207    /// assert_eq!(parsed.flag_values("output"), vec!["json"]);
208    /// assert!(parsed.flag_values("missing").is_empty());
209    /// ```
210    pub fn flag_values(&self, name: &str) -> Vec<String> {
211        match self.flags.get(name) {
212            None => vec![],
213            Some(v) => serde_json::from_str::<Vec<String>>(v).unwrap_or_else(|_| vec![v.clone()]),
214        }
215    }
216
217    /// Return `true` if `name` is present in the parsed flag map.
218    ///
219    /// A flag is present when it was explicitly provided on the command line
220    /// **or** when the parser inserted a default or env-var value.
221    /// To distinguish explicit from default, use [`ParsedCommand::flag`] and
222    /// compare with the flag definition's default.
223    ///
224    /// # Examples
225    ///
226    /// ```
227    /// # use argot_cmd::{Command, Flag, Parser};
228    /// let cmd = Command::builder("run")
229    ///     .flag(Flag::builder("verbose").build().unwrap())
230    ///     .flag(Flag::builder("output").takes_value().default_value("text").build().unwrap())
231    ///     .build().unwrap();
232    /// let cmds = vec![cmd];
233    /// let parsed = Parser::new(&cmds).parse(&["run", "--verbose"]).unwrap();
234    /// assert!(parsed.has_flag("verbose"));
235    /// assert!(parsed.has_flag("output"));  // present via default
236    /// assert!(!parsed.has_flag("missing"));
237    /// ```
238    pub fn has_flag(&self, name: &str) -> bool {
239        self.flags.contains_key(name)
240    }
241
242    /// Parse a positional argument as type `T` using [`std::str::FromStr`].
243    ///
244    /// Returns:
245    /// - `None` if the argument was not provided (absent from the map)
246    /// - `Some(Ok(value))` if parsing succeeded
247    /// - `Some(Err(e))` if the string was present but could not be parsed
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// # use argot_cmd::{Argument, Command, Parser};
253    /// let cmd = Command::builder("resize")
254    ///     .argument(Argument::builder("width").required().build().unwrap())
255    ///     .build().unwrap();
256    /// let cmds = vec![cmd];
257    /// let parsed = Parser::new(&cmds).parse(&["resize", "1920"]).unwrap();
258    ///
259    /// let width: u32 = parsed.arg_as("width").unwrap().unwrap();
260    /// assert_eq!(width, 1920u32);
261    ///
262    /// assert!(parsed.arg_as::<u32>("missing").is_none());
263    /// ```
264    pub fn arg_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
265        self.args.get(name).map(|v| v.parse())
266    }
267
268    /// Parse a flag value as type `T` using [`std::str::FromStr`].
269    ///
270    /// Returns:
271    /// - `None` if the flag was not set (and has no default)
272    /// - `Some(Ok(value))` if parsing succeeded
273    /// - `Some(Err(e))` if the string was present but could not be parsed
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// # use argot_cmd::{Command, Flag, Parser};
279    /// let cmd = Command::builder("serve")
280    ///     .flag(Flag::builder("port").takes_value().default_value("8080").build().unwrap())
281    ///     .build().unwrap();
282    /// let cmds = vec![cmd];
283    /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
284    ///
285    /// let port: u16 = parsed.flag_as("port").unwrap().unwrap();
286    /// assert_eq!(port, 8080u16);
287    /// ```
288    pub fn flag_as<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
289        self.flags.get(name).map(|v| v.parse())
290    }
291
292    /// Parse a positional argument as type `T`, returning a default if absent or unparseable.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// # use argot_cmd::{Argument, Command, Parser};
298    /// let cmd = Command::builder("run")
299    ///     .argument(Argument::builder("count").build().unwrap())
300    ///     .build().unwrap();
301    /// let cmds = vec![cmd];
302    /// let parsed = Parser::new(&cmds).parse(&["run"]).unwrap();
303    ///
304    /// assert_eq!(parsed.arg_as_or("count", 1u32), 1u32);
305    /// ```
306    pub fn arg_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
307        self.arg_as(name).and_then(|r| r.ok()).unwrap_or(default)
308    }
309
310    /// Parse a flag value as type `T`, returning a default if absent or unparseable.
311    ///
312    /// # Examples
313    ///
314    /// ```
315    /// # use argot_cmd::{Command, Flag, Parser};
316    /// let cmd = Command::builder("serve")
317    ///     .flag(Flag::builder("workers").takes_value().build().unwrap())
318    ///     .build().unwrap();
319    /// let cmds = vec![cmd];
320    /// let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
321    ///
322    /// assert_eq!(parsed.flag_as_or("workers", 4u32), 4u32);
323    /// ```
324    pub fn flag_as_or<T: std::str::FromStr>(&self, name: &str, default: T) -> T {
325        self.flag_as(name).and_then(|r| r.ok()).unwrap_or(default)
326    }
327}
328
329/// A command in the registry, potentially with subcommands.
330///
331/// Commands are the central unit of argot. Each command has a canonical name,
332/// optional aliases and alternate spellings, human-readable documentation,
333/// typed positional arguments, named flags, usage examples, and an optional
334/// handler closure. Commands can be nested arbitrarily deep via
335/// [`Command::subcommands`].
336///
337/// Use [`Command::builder`] to construct instances — direct struct
338/// construction is intentionally not exposed.
339///
340/// # Serialization
341///
342/// `Command` implements `serde::Serialize` / `Deserialize`. The [`handler`]
343/// field is skipped during serialization (it cannot be represented in JSON)
344/// and will be `None` after deserialization.
345///
346/// # Examples
347///
348/// ```
349/// # use argot_cmd::{Command, Argument, Flag, Example};
350/// let cmd = Command::builder("deploy")
351///     .alias("d")
352///     .summary("Deploy the app")
353///     .description("Deploys to the specified environment.")
354///     .argument(
355///         Argument::builder("env")
356///             .description("Target environment")
357///             .required()
358///             .build()
359///             .unwrap(),
360///     )
361///     .flag(
362///         Flag::builder("dry-run")
363///             .short('n')
364///             .description("Simulate only")
365///             .build()
366///             .unwrap(),
367///     )
368///     .example(Example::new("deploy to prod", "myapp deploy production"))
369///     .build()
370///     .unwrap();
371///
372/// assert_eq!(cmd.canonical, "deploy");
373/// assert_eq!(cmd.aliases, vec!["d"]);
374/// ```
375///
376/// [`handler`]: Command::handler
377#[derive(Clone, Serialize, Deserialize)]
378pub struct Command {
379    /// The primary, canonical name used to invoke this command.
380    pub canonical: String,
381    /// Alternative names that resolve to this command (e.g. `"ls"` for `"list"`).
382    pub aliases: Vec<String>,
383    /// Alternate capitalizations or spellings (e.g. `"LIST"` for `"list"`).
384    ///
385    /// Spellings differ from aliases semantically: they represent the same
386    /// word written differently rather than a short-form abbreviation.
387    pub spellings: Vec<String>,
388    /// One-line description shown in command listings.
389    pub summary: String,
390    /// Full prose description shown in detailed help output.
391    pub description: String,
392    /// Ordered list of positional arguments accepted by this command.
393    pub arguments: Vec<Argument>,
394    /// Named flags (long and/or short) accepted by this command.
395    pub flags: Vec<Flag>,
396    /// Usage examples shown in help and Markdown documentation.
397    pub examples: Vec<Example>,
398    /// Nested sub-commands (e.g. `remote add`, `remote remove`).
399    pub subcommands: Vec<Command>,
400    /// Prose tips about correct usage, surfaced to AI agents.
401    pub best_practices: Vec<String>,
402    /// Prose warnings about incorrect usage, surfaced to AI agents.
403    pub anti_patterns: Vec<String>,
404    /// Natural-language phrases describing what this command does.
405    ///
406    /// Used for intent-based discovery (e.g. [`crate::query::Registry::match_intent`])
407    /// but intentionally excluded from normal help output.
408    pub semantic_aliases: Vec<String>,
409    /// Optional runtime handler invoked by [`crate::cli::Cli::run`].
410    ///
411    /// Skipped during JSON serialization/deserialization.
412    #[serde(skip)]
413    pub handler: Option<HandlerFn>,
414    /// Optional async runtime handler (feature: `async`).
415    ///
416    /// Skipped during JSON serialization.
417    #[cfg(feature = "async")]
418    #[serde(skip)]
419    pub async_handler: Option<AsyncHandlerFn>,
420    /// Arbitrary application-defined metadata.
421    ///
422    /// Use this to attach structured data that is not covered by the built-in
423    /// fields (e.g., permission requirements, category tags, telemetry labels).
424    ///
425    /// Serialized to JSON as an object; absent from output when empty.
426    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
427    pub extra: HashMap<String, serde_json::Value>,
428    /// Groups of mutually exclusive flag names.
429    ///
430    /// At most one flag in each group may be provided in a single invocation.
431    /// Validated at build time (flags must exist) and enforced at parse time.
432    #[serde(default, skip_serializing_if = "Vec::is_empty")]
433    pub exclusive_groups: Vec<Vec<String>>,
434}
435
436impl std::fmt::Debug for Command {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        let mut ds = f.debug_struct("Command");
439        ds.field("canonical", &self.canonical)
440            .field("aliases", &self.aliases)
441            .field("spellings", &self.spellings)
442            .field("summary", &self.summary)
443            .field("description", &self.description)
444            .field("arguments", &self.arguments)
445            .field("flags", &self.flags)
446            .field("examples", &self.examples)
447            .field("subcommands", &self.subcommands)
448            .field("best_practices", &self.best_practices)
449            .field("anti_patterns", &self.anti_patterns)
450            .field("semantic_aliases", &self.semantic_aliases)
451            .field("handler", &self.handler.as_ref().map(|_| "<handler>"));
452        #[cfg(feature = "async")]
453        ds.field(
454            "async_handler",
455            &self.async_handler.as_ref().map(|_| "<async_handler>"),
456        );
457        ds.field("extra", &self.extra)
458            .field("exclusive_groups", &self.exclusive_groups)
459            .finish()
460    }
461}
462
463impl PartialEq for Command {
464    fn eq(&self, other: &Self) -> bool {
465        self.canonical == other.canonical
466            && self.aliases == other.aliases
467            && self.spellings == other.spellings
468            && self.summary == other.summary
469            && self.description == other.description
470            && self.arguments == other.arguments
471            && self.flags == other.flags
472            && self.examples == other.examples
473            && self.subcommands == other.subcommands
474            && self.best_practices == other.best_practices
475            && self.anti_patterns == other.anti_patterns
476            && self.semantic_aliases == other.semantic_aliases
477            && self.extra == other.extra
478            && self.exclusive_groups == other.exclusive_groups
479    }
480}
481
482impl Eq for Command {}
483
484impl std::hash::Hash for Command {
485    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
486        self.canonical.hash(state);
487        self.aliases.hash(state);
488        self.spellings.hash(state);
489        self.summary.hash(state);
490        self.description.hash(state);
491        self.arguments.hash(state);
492        self.flags.hash(state);
493        self.examples.hash(state);
494        self.subcommands.hash(state);
495        self.best_practices.hash(state);
496        self.anti_patterns.hash(state);
497        self.semantic_aliases.hash(state);
498        // handler is intentionally excluded (not hashable)
499        // extra: hash keys in sorted order, with their JSON string representation
500        {
501            let mut keys: Vec<&String> = self.extra.keys().collect();
502            keys.sort();
503            for k in keys {
504                k.hash(state);
505                self.extra[k].to_string().hash(state);
506            }
507        }
508        self.exclusive_groups.hash(state);
509    }
510}
511
512impl PartialOrd for Command {
513    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
514        Some(self.cmp(other))
515    }
516}
517
518/// Commands are ordered by canonical name, then by their full field contents.
519impl Ord for Command {
520    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
521        self.canonical
522            .cmp(&other.canonical)
523            .then_with(|| self.summary.cmp(&other.summary))
524            .then_with(|| self.aliases.cmp(&other.aliases))
525    }
526}
527
528impl Command {
529    /// Create a new [`CommandBuilder`] with the given canonical name.
530    ///
531    /// # Arguments
532    ///
533    /// - `canonical` — The primary command name. Must be non-empty after
534    ///   trimming (enforced by [`CommandBuilder::build`]).
535    ///
536    /// # Examples
537    ///
538    /// ```
539    /// # use argot_cmd::Command;
540    /// let cmd = Command::builder("list").build().unwrap();
541    /// assert_eq!(cmd.canonical, "list");
542    /// ```
543    pub fn builder(canonical: impl Into<String>) -> CommandBuilder {
544        CommandBuilder {
545            canonical: canonical.into(),
546            aliases: Vec::new(),
547            spellings: Vec::new(),
548            summary: String::new(),
549            description: String::new(),
550            arguments: Vec::new(),
551            flags: Vec::new(),
552            examples: Vec::new(),
553            subcommands: Vec::new(),
554            best_practices: Vec::new(),
555            anti_patterns: Vec::new(),
556            semantic_aliases: Vec::new(),
557            handler: None,
558            #[cfg(feature = "async")]
559            async_handler: None,
560            extra: HashMap::new(),
561            exclusive_groups: Vec::new(),
562        }
563    }
564
565    /// All strings this command can be matched by for exact lookup
566    /// (canonical + aliases + spellings), lowercased.
567    pub(crate) fn matchable_strings(&self) -> Vec<String> {
568        let mut v = vec![self.canonical.to_lowercase()];
569        v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
570        v.extend(self.spellings.iter().map(|s| s.to_lowercase()));
571        v
572    }
573
574    /// Strings eligible for prefix matching (canonical + aliases only), lowercased.
575    ///
576    /// Spellings are intentionally excluded: they are exact-match-only so they
577    /// do not contribute to prefix ambiguity resolution and never appear in
578    /// "did you mean?" / ambiguous-candidate output.
579    pub(crate) fn prefix_matchable_strings(&self) -> Vec<String> {
580        let mut v = vec![self.canonical.to_lowercase()];
581        v.extend(self.aliases.iter().map(|s| s.to_lowercase()));
582        v
583    }
584}
585
586/// Consuming builder for [`Command`].
587///
588/// Obtain an instance via [`Command::builder`]. All setter methods consume and
589/// return `self` to allow method chaining. Call [`CommandBuilder::build`] to
590/// produce a [`Command`].
591///
592/// # Examples
593///
594/// ```
595/// # use argot_cmd::{Command, Flag};
596/// let cmd = Command::builder("run")
597///     .alias("r")
598///     .summary("Run the pipeline")
599///     .flag(Flag::builder("verbose").short('v').build().unwrap())
600///     .build()
601///     .unwrap();
602///
603/// assert_eq!(cmd.canonical, "run");
604/// assert_eq!(cmd.aliases, vec!["r"]);
605/// ```
606pub struct CommandBuilder {
607    canonical: String,
608    aliases: Vec<String>,
609    spellings: Vec<String>,
610    summary: String,
611    description: String,
612    arguments: Vec<Argument>,
613    flags: Vec<Flag>,
614    examples: Vec<Example>,
615    subcommands: Vec<Command>,
616    best_practices: Vec<String>,
617    anti_patterns: Vec<String>,
618    semantic_aliases: Vec<String>,
619    handler: Option<HandlerFn>,
620    #[cfg(feature = "async")]
621    async_handler: Option<AsyncHandlerFn>,
622    extra: HashMap<String, serde_json::Value>,
623    exclusive_groups: Vec<Vec<String>>,
624}
625
626impl CommandBuilder {
627    /// Replace the entire alias list with the given collection.
628    ///
629    /// To add a single alias use [`CommandBuilder::alias`].
630    pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
631        self.aliases = aliases.into_iter().map(Into::into).collect();
632        self
633    }
634
635    /// Append a single alias.
636    pub fn alias(mut self, alias: impl Into<String>) -> Self {
637        self.aliases.push(alias.into());
638        self
639    }
640
641    /// Replace the entire spelling list with the given collection.
642    ///
643    /// Spellings are silent typo-corrections or abbreviated forms that the
644    /// resolver accepts but does **not** advertise in help text. They differ
645    /// from aliases in that they will never appear in the `--help` aliases
646    /// line, and they are excluded from prefix-match ambiguity candidates.
647    ///
648    /// To add a single spelling use [`CommandBuilder::spelling`].
649    pub fn spellings(mut self, spellings: impl IntoIterator<Item = impl Into<String>>) -> Self {
650        self.spellings = spellings.into_iter().map(Into::into).collect();
651        self
652    }
653
654    /// Register a common misspelling or abbreviated form that resolves to this command.
655    ///
656    /// Unlike [`CommandBuilder::alias`], spellings are **not shown in help text**.
657    /// They serve as silent typo-correction or shorthand that resolves
658    /// to the canonical command name without advertising the alternative.
659    ///
660    /// # Difference from `alias`
661    ///
662    /// | Feature | `alias` | `spelling` |
663    /// |---------|---------|------------|
664    /// | Shown in `--help` | Yes | No |
665    /// | Resolver exact match | Yes | Yes |
666    /// | Resolver prefix match | Yes | No |
667    /// | Appears in `aliases` field | Yes | No (goes in `spellings`) |
668    ///
669    /// # Examples
670    ///
671    /// ```
672    /// # use argot_cmd::Command;
673    /// let cmd = Command::builder("deploy")
674    ///     .alias("release")           // shown in help: "deploy (release)"
675    ///     .alias("ship")              // shown in help
676    ///     .spelling("deply")          // silent typo correction
677    ///     .spelling("delpoy")         // silent typo correction
678    ///     .build().unwrap();
679    ///
680    /// assert!(cmd.aliases.contains(&"release".to_string()));
681    /// assert!(cmd.spellings.contains(&"deply".to_string()));
682    /// ```
683    pub fn spelling(mut self, s: impl Into<String>) -> Self {
684        self.spellings.push(s.into());
685        self
686    }
687
688    /// Set the one-line summary shown in command listings.
689    pub fn summary(mut self, s: impl Into<String>) -> Self {
690        self.summary = s.into();
691        self
692    }
693
694    /// Set the full prose description shown in detailed help output.
695    pub fn description(mut self, d: impl Into<String>) -> Self {
696        self.description = d.into();
697        self
698    }
699
700    /// Append a positional [`Argument`] definition.
701    ///
702    /// Arguments are bound in declaration order when the command is parsed.
703    pub fn argument(mut self, arg: Argument) -> Self {
704        self.arguments.push(arg);
705        self
706    }
707
708    /// Append a [`Flag`] definition.
709    pub fn flag(mut self, flag: Flag) -> Self {
710        self.flags.push(flag);
711        self
712    }
713
714    /// Append a usage [`Example`].
715    pub fn example(mut self, example: Example) -> Self {
716        self.examples.push(example);
717        self
718    }
719
720    /// Append a subcommand.
721    ///
722    /// Subcommands are resolved before positional arguments during parsing, so
723    /// a token that matches a subcommand name is consumed as the subcommand
724    /// selector, not as a positional value.
725    pub fn subcommand(mut self, cmd: Command) -> Self {
726        self.subcommands.push(cmd);
727        self
728    }
729
730    /// Append a best-practice tip surfaced to AI agents.
731    pub fn best_practice(mut self, bp: impl Into<String>) -> Self {
732        self.best_practices.push(bp.into());
733        self
734    }
735
736    /// Append an anti-pattern warning surfaced to AI agents.
737    pub fn anti_pattern(mut self, ap: impl Into<String>) -> Self {
738        self.anti_patterns.push(ap.into());
739        self
740    }
741
742    /// Replace the entire semantic alias list with the given collection.
743    ///
744    /// Semantic aliases are natural-language phrases used for intent-based
745    /// discovery but are **not shown in help text**.
746    ///
747    /// To add a single semantic alias use [`CommandBuilder::semantic_alias`].
748    pub fn semantic_aliases(
749        mut self,
750        aliases: impl IntoIterator<Item = impl Into<String>>,
751    ) -> Self {
752        self.semantic_aliases = aliases.into_iter().map(Into::into).collect();
753        self
754    }
755
756    /// Append a single natural-language phrase for intent-based discovery.
757    ///
758    /// Unlike [`CommandBuilder::alias`], semantic aliases are natural-language
759    /// phrases describing what the command does (e.g. `"release to production"`,
760    /// `"push to environment"`). They are used by
761    /// [`crate::query::Registry::match_intent`] but are **not shown in help text**.
762    ///
763    /// # Examples
764    ///
765    /// ```
766    /// # use argot_cmd::Command;
767    /// let cmd = Command::builder("deploy")
768    ///     .semantic_alias("release to production")
769    ///     .semantic_alias("push to environment")
770    ///     .build().unwrap();
771    ///
772    /// assert_eq!(cmd.semantic_aliases, vec!["release to production", "push to environment"]);
773    /// ```
774    pub fn semantic_alias(mut self, s: impl Into<String>) -> Self {
775        self.semantic_aliases.push(s.into());
776        self
777    }
778
779    /// Set the runtime handler invoked when this command is dispatched.
780    ///
781    /// The handler receives a [`ParsedCommand`] and should return `Ok(())`
782    /// on success or a boxed error on failure.
783    pub fn handler(mut self, h: HandlerFn) -> Self {
784        self.handler = Some(h);
785        self
786    }
787
788    /// Register an async handler for this command (feature: `async`).
789    ///
790    /// The handler receives a [`ParsedCommand`] and returns a boxed future.
791    /// Use [`crate::Cli::run_async`] to dispatch async handlers.
792    ///
793    /// # Examples
794    ///
795    /// ```
796    /// # #[cfg(feature = "async")] {
797    /// use std::sync::Arc;
798    /// use argot_cmd::Command;
799    ///
800    /// let cmd = Command::builder("deploy")
801    ///     .async_handler(Arc::new(|parsed| Box::pin(async move {
802    ///         println!("deployed!");
803    ///         Ok(())
804    ///     })))
805    ///     .build()
806    ///     .unwrap();
807    /// # }
808    /// ```
809    #[cfg(feature = "async")]
810    pub fn async_handler(mut self, h: AsyncHandlerFn) -> Self {
811        self.async_handler = Some(h);
812        self
813    }
814
815    /// Set an arbitrary metadata entry on this command.
816    ///
817    /// # Examples
818    ///
819    /// ```
820    /// # use argot_cmd::Command;
821    /// # use serde_json::json;
822    /// let cmd = Command::builder("deploy")
823    ///     .meta("category", json!("infrastructure"))
824    ///     .meta("min_role", json!("ops"))
825    ///     .build()
826    ///     .unwrap();
827    ///
828    /// assert_eq!(cmd.extra["category"], json!("infrastructure"));
829    /// ```
830    pub fn meta(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
831        self.extra.insert(key.into(), value);
832        self
833    }
834
835    /// Declare a group of mutually exclusive flags.
836    ///
837    /// At most one flag from `flags` may be provided in a single invocation.
838    /// The parser returns [`crate::ParseError::MutuallyExclusive`] if two or
839    /// more flags from the same group are present.
840    ///
841    /// # Panics (at build time via [`BuildError`])
842    ///
843    /// [`CommandBuilder::build`] returns an error if:
844    /// - The group has fewer than 2 flags.
845    /// - Any flag name in the group is not defined on this command.
846    ///
847    /// # Examples
848    ///
849    /// ```
850    /// # use argot_cmd::{Command, Flag};
851    /// let cmd = Command::builder("export")
852    ///     .flag(Flag::builder("json").build().unwrap())
853    ///     .flag(Flag::builder("yaml").build().unwrap())
854    ///     .flag(Flag::builder("csv").build().unwrap())
855    ///     .exclusive(["json", "yaml", "csv"])
856    ///     .build()
857    ///     .unwrap();
858    /// assert_eq!(cmd.exclusive_groups.len(), 1);
859    /// ```
860    pub fn exclusive(mut self, flags: impl IntoIterator<Item = impl Into<String>>) -> Self {
861        self.exclusive_groups
862            .push(flags.into_iter().map(Into::into).collect());
863        self
864    }
865
866    /// Consume the builder and return a [`Command`].
867    ///
868    /// # Errors
869    ///
870    /// - [`BuildError::EmptyCanonical`] — canonical name is empty or whitespace.
871    /// - [`BuildError::AliasEqualsCanonical`] — an alias is identical to the
872    ///   canonical name (case-insensitive).
873    /// - [`BuildError::DuplicateAlias`] — two aliases share the same string
874    ///   (case-insensitive comparison).
875    /// - [`BuildError::DuplicateFlagName`] — two flags share the same long name.
876    /// - [`BuildError::DuplicateShortFlag`] — two flags share the same short
877    ///   character.
878    /// - [`BuildError::DuplicateArgumentName`] — two positional arguments share
879    ///   the same name.
880    /// - [`BuildError::DuplicateSubcommandName`] — two subcommands share the
881    ///   same canonical name.
882    /// - [`BuildError::VariadicNotLast`] — a variadic argument is not the last
883    ///   argument in the list.
884    ///
885    /// # Examples
886    ///
887    /// ```
888    /// # use argot_cmd::{Command, BuildError};
889    /// assert!(Command::builder("ok").build().is_ok());
890    /// assert_eq!(Command::builder("").build().unwrap_err(), BuildError::EmptyCanonical);
891    /// ```
892    pub fn build(self) -> Result<Command, BuildError> {
893        if self.canonical.trim().is_empty() {
894            return Err(BuildError::EmptyCanonical);
895        }
896
897        // 1. Alias equals canonical
898        let canonical_lower = self.canonical.to_lowercase();
899        for alias in &self.aliases {
900            if alias.to_lowercase() == canonical_lower {
901                return Err(BuildError::AliasEqualsCanonical(alias.clone()));
902            }
903        }
904
905        // 2. Duplicate aliases
906        let mut seen_aliases = std::collections::HashSet::new();
907        for alias in &self.aliases {
908            let key = alias.to_lowercase();
909            if !seen_aliases.insert(key) {
910                return Err(BuildError::DuplicateAlias(alias.clone()));
911            }
912        }
913
914        // 3. Duplicate flag names (long)
915        let mut seen_flag_names = std::collections::HashSet::new();
916        for flag in &self.flags {
917            if !seen_flag_names.insert(flag.name.clone()) {
918                return Err(BuildError::DuplicateFlagName(flag.name.clone()));
919            }
920        }
921
922        // 4. Duplicate short flags
923        let mut seen_short_flags = std::collections::HashSet::new();
924        for flag in &self.flags {
925            if let Some(c) = flag.short {
926                if !seen_short_flags.insert(c) {
927                    return Err(BuildError::DuplicateShortFlag(c));
928                }
929            }
930        }
931
932        // 5. Duplicate argument names
933        let mut seen_arg_names = std::collections::HashSet::new();
934        for arg in &self.arguments {
935            if !seen_arg_names.insert(arg.name.clone()) {
936                return Err(BuildError::DuplicateArgumentName(arg.name.clone()));
937            }
938        }
939
940        // 6. Duplicate subcommand canonical names
941        let mut seen_sub_names = std::collections::HashSet::new();
942        for sub in &self.subcommands {
943            if !seen_sub_names.insert(sub.canonical.clone()) {
944                return Err(BuildError::DuplicateSubcommandName(sub.canonical.clone()));
945            }
946        }
947
948        // 7. Variadic argument must be last
949        for (i, arg) in self.arguments.iter().enumerate() {
950            if arg.variadic && i != self.arguments.len() - 1 {
951                return Err(BuildError::VariadicNotLast(arg.name.clone()));
952            }
953        }
954
955        // 8. Flags with choices must have a non-empty choices list
956        for flag in &self.flags {
957            if let Some(choices) = &flag.choices {
958                if choices.is_empty() {
959                    return Err(BuildError::EmptyChoices(flag.name.clone()));
960                }
961            }
962        }
963
964        // 9. Mutual exclusivity group validation
965        for group in &self.exclusive_groups {
966            if group.len() < 2 {
967                return Err(BuildError::ExclusiveGroupTooSmall);
968            }
969            for flag_name in group {
970                if !self.flags.iter().any(|f| &f.name == flag_name) {
971                    return Err(BuildError::ExclusiveGroupUnknownFlag(flag_name.clone()));
972                }
973            }
974        }
975
976        Ok(Command {
977            canonical: self.canonical,
978            aliases: self.aliases,
979            spellings: self.spellings,
980            summary: self.summary,
981            description: self.description,
982            arguments: self.arguments,
983            flags: self.flags,
984            examples: self.examples,
985            subcommands: self.subcommands,
986            best_practices: self.best_practices,
987            anti_patterns: self.anti_patterns,
988            semantic_aliases: self.semantic_aliases,
989            handler: self.handler,
990            #[cfg(feature = "async")]
991            async_handler: self.async_handler,
992            extra: self.extra,
993            exclusive_groups: self.exclusive_groups,
994        })
995    }
996}
997
998#[cfg(test)]
999mod tests {
1000    use super::*;
1001    use crate::model::{Argument, Flag};
1002
1003    fn make_simple_cmd() -> Command {
1004        Command::builder("run")
1005            .alias("r")
1006            .spelling("RUN")
1007            .summary("Run something")
1008            .description("Runs the thing.")
1009            .build()
1010            .unwrap()
1011    }
1012
1013    #[test]
1014    fn test_builder_happy_path() {
1015        let cmd = make_simple_cmd();
1016        assert_eq!(cmd.canonical, "run");
1017        assert_eq!(cmd.aliases, vec!["r"]);
1018        assert_eq!(cmd.spellings, vec!["RUN"]);
1019    }
1020
1021    #[test]
1022    fn test_builder_empty_canonical() {
1023        assert_eq!(
1024            Command::builder("").build().unwrap_err(),
1025            BuildError::EmptyCanonical
1026        );
1027        assert_eq!(
1028            Command::builder("   ").build().unwrap_err(),
1029            BuildError::EmptyCanonical
1030        );
1031    }
1032
1033    #[test]
1034    fn test_partial_eq_ignores_handler() {
1035        let cmd1 = Command::builder("run").build().unwrap();
1036        let mut cmd2 = cmd1.clone();
1037        cmd2.handler = Some(Arc::new(|_| Ok(())));
1038        assert_eq!(cmd1, cmd2);
1039    }
1040
1041    #[test]
1042    fn test_serde_round_trip_skips_handler() {
1043        let cmd = Command::builder("deploy")
1044            .summary("Deploy the app")
1045            .argument(
1046                Argument::builder("env")
1047                    .description("target env")
1048                    .required()
1049                    .build()
1050                    .unwrap(),
1051            )
1052            .flag(
1053                Flag::builder("dry-run")
1054                    .description("dry run mode")
1055                    .build()
1056                    .unwrap(),
1057            )
1058            .handler(Arc::new(|_| Ok(())))
1059            .build()
1060            .unwrap();
1061
1062        let json = serde_json::to_string(&cmd).unwrap();
1063        let de: Command = serde_json::from_str(&json).unwrap();
1064        assert_eq!(cmd, de);
1065        assert!(de.handler.is_none());
1066    }
1067
1068    #[test]
1069    fn test_matchable_strings() {
1070        let cmd = Command::builder("Git")
1071            .alias("g")
1072            .spelling("GIT")
1073            .build()
1074            .unwrap();
1075        let matchables = cmd.matchable_strings();
1076        assert!(matchables.contains(&"git".to_string()));
1077        assert!(matchables.contains(&"g".to_string()));
1078        assert!(matchables.contains(&"git".to_string())); // spelling lowercased
1079    }
1080
1081    #[test]
1082    fn test_clone_shares_handler() {
1083        let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1084        let called_clone = called.clone();
1085        let cmd = Command::builder("x")
1086            .handler(Arc::new(move |_| {
1087                called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
1088                Ok(())
1089            }))
1090            .build()
1091            .unwrap();
1092        let cmd2 = cmd.clone();
1093        assert!(std::sync::Arc::ptr_eq(
1094            cmd.handler.as_ref().unwrap(),
1095            cmd2.handler.as_ref().unwrap()
1096        ));
1097    }
1098
1099    #[test]
1100    fn test_duplicate_alias_rejected() {
1101        let err = Command::builder("cmd")
1102            .alias("a")
1103            .alias("a")
1104            .build()
1105            .unwrap_err();
1106        assert!(matches!(err, BuildError::DuplicateAlias(_)));
1107    }
1108
1109    #[test]
1110    fn test_alias_equals_canonical_rejected() {
1111        let err = Command::builder("deploy")
1112            .alias("deploy")
1113            .build()
1114            .unwrap_err();
1115        assert!(matches!(err, BuildError::AliasEqualsCanonical(_)));
1116    }
1117
1118    #[test]
1119    fn test_duplicate_flag_name_rejected() {
1120        let flag = Flag::builder("verbose").build().unwrap();
1121        let err = Command::builder("cmd")
1122            .flag(flag.clone())
1123            .flag(flag)
1124            .build()
1125            .unwrap_err();
1126        assert!(matches!(err, BuildError::DuplicateFlagName(_)));
1127    }
1128
1129    #[test]
1130    fn test_duplicate_short_flag_rejected() {
1131        let f1 = Flag::builder("verbose").short('v').build().unwrap();
1132        let f2 = Flag::builder("version").short('v').build().unwrap();
1133        let err = Command::builder("cmd")
1134            .flag(f1)
1135            .flag(f2)
1136            .build()
1137            .unwrap_err();
1138        assert!(matches!(err, BuildError::DuplicateShortFlag('v')));
1139    }
1140
1141    #[test]
1142    fn test_duplicate_argument_name_rejected() {
1143        let arg = Argument::builder("env").build().unwrap();
1144        let err = Command::builder("cmd")
1145            .argument(arg.clone())
1146            .argument(arg)
1147            .build()
1148            .unwrap_err();
1149        assert!(matches!(err, BuildError::DuplicateArgumentName(_)));
1150    }
1151
1152    #[test]
1153    fn test_duplicate_subcommand_name_rejected() {
1154        let sub = Command::builder("add").build().unwrap();
1155        let err = Command::builder("remote")
1156            .subcommand(sub.clone())
1157            .subcommand(sub)
1158            .build()
1159            .unwrap_err();
1160        assert!(matches!(err, BuildError::DuplicateSubcommandName(_)));
1161    }
1162
1163    #[test]
1164    fn test_variadic_must_be_last() {
1165        let variadic = Argument::builder("files").variadic().build().unwrap();
1166        let after = Argument::builder("extra").build().unwrap();
1167        let err = Command::builder("cmd")
1168            .argument(variadic)
1169            .argument(after)
1170            .build()
1171            .unwrap_err();
1172        assert!(matches!(err, BuildError::VariadicNotLast(_)));
1173    }
1174
1175    #[test]
1176    fn test_meta_field_serde() {
1177        let cmd = Command::builder("x")
1178            .meta("role", serde_json::json!("admin"))
1179            .build()
1180            .unwrap();
1181        let json = serde_json::to_string(&cmd).unwrap();
1182        assert!(json.contains("admin"));
1183        let de: Command = serde_json::from_str(&json).unwrap();
1184        assert_eq!(de.extra["role"], serde_json::json!("admin"));
1185    }
1186
1187    #[test]
1188    fn test_meta_empty_not_serialized() {
1189        let cmd = Command::builder("x").build().unwrap();
1190        let json = serde_json::to_string(&cmd).unwrap();
1191        assert!(!json.contains("extra"));
1192    }
1193
1194    #[test]
1195    fn test_semantic_alias_builder() {
1196        let cmd = Command::builder("deploy")
1197            .semantic_alias("release to production")
1198            .semantic_alias("push to environment")
1199            .build()
1200            .unwrap();
1201        assert_eq!(
1202            cmd.semantic_aliases,
1203            vec!["release to production", "push to environment"]
1204        );
1205    }
1206
1207    #[test]
1208    fn test_semantic_aliases_bulk_builder() {
1209        let cmd = Command::builder("deploy")
1210            .semantic_aliases(["release to production", "push to environment"])
1211            .build()
1212            .unwrap();
1213        assert_eq!(
1214            cmd.semantic_aliases,
1215            vec!["release to production", "push to environment"]
1216        );
1217    }
1218
1219    #[test]
1220    fn test_semantic_alias_not_in_canonical_aliases() {
1221        let cmd = Command::builder("deploy")
1222            .alias("d")
1223            .semantic_alias("release to production")
1224            .build()
1225            .unwrap();
1226        // semantic_aliases and aliases are distinct fields
1227        assert_eq!(cmd.aliases, vec!["d"]);
1228        assert_eq!(cmd.semantic_aliases, vec!["release to production"]);
1229        assert!(!cmd.aliases.contains(&"release to production".to_string()));
1230        assert!(!cmd.semantic_aliases.contains(&"d".to_string()));
1231    }
1232
1233    #[test]
1234    fn test_debug_impl_includes_fields() {
1235        let cmd = Command::builder("debug-test")
1236            .alias("dt")
1237            .spelling("DEBUGTEST")
1238            .summary("Test debug")
1239            .description("A debug test command")
1240            .handler(Arc::new(|_| Ok(())))
1241            .build()
1242            .unwrap();
1243        let s = format!("{:?}", cmd);
1244        assert!(s.contains("debug-test"));
1245        assert!(s.contains("dt"));
1246        assert!(s.contains("Test debug"));
1247        // Handler should appear as "<handler>" not a raw pointer
1248        assert!(s.contains("<handler>"));
1249        // spellings should also be shown
1250        assert!(s.contains("DEBUGTEST"));
1251    }
1252
1253    #[test]
1254    fn test_hash_deterministic() {
1255        use std::collections::hash_map::DefaultHasher;
1256        use std::hash::{Hash, Hasher};
1257
1258        let cmd1 = Command::builder("hash-test")
1259            .summary("Hash")
1260            .meta("key", serde_json::json!("value"))
1261            .build()
1262            .unwrap();
1263        let cmd2 = Command::builder("hash-test")
1264            .summary("Hash")
1265            .meta("key", serde_json::json!("value"))
1266            .build()
1267            .unwrap();
1268
1269        let mut h1 = DefaultHasher::new();
1270        cmd1.hash(&mut h1);
1271        let mut h2 = DefaultHasher::new();
1272        cmd2.hash(&mut h2);
1273        assert_eq!(h1.finish(), h2.finish());
1274    }
1275
1276    #[test]
1277    fn test_ord_by_canonical() {
1278        let a = Command::builder("alpha").build().unwrap();
1279        let b = Command::builder("beta").build().unwrap();
1280        assert!(a < b);
1281        assert!(b > a);
1282        assert_eq!(a.partial_cmp(&a), Some(std::cmp::Ordering::Equal));
1283    }
1284
1285    #[test]
1286    fn test_ord_same_canonical_by_summary() {
1287        let a = Command::builder("cmd").summary("Alpha").build().unwrap();
1288        let b = Command::builder("cmd").summary("Beta").build().unwrap();
1289        assert!(a < b);
1290    }
1291
1292    #[test]
1293    fn test_aliases_builder_method() {
1294        let cmd = Command::builder("cmd")
1295            .aliases(["a", "b", "c"])
1296            .build()
1297            .unwrap();
1298        assert_eq!(cmd.aliases, vec!["a", "b", "c"]);
1299    }
1300
1301    #[test]
1302    fn test_spellings_builder_method() {
1303        let cmd = Command::builder("deploy")
1304            .spellings(["DEPLOY", "dploy"])
1305            .build()
1306            .unwrap();
1307        assert_eq!(cmd.spellings, vec!["DEPLOY", "dploy"]);
1308    }
1309
1310    #[test]
1311    fn test_best_practice_and_anti_pattern_builder() {
1312        let cmd = Command::builder("cmd")
1313            .best_practice("Always dry-run first")
1314            .anti_pattern("Deploy on Fridays")
1315            .build()
1316            .unwrap();
1317        assert_eq!(cmd.best_practices, vec!["Always dry-run first"]);
1318        assert_eq!(cmd.anti_patterns, vec!["Deploy on Fridays"]);
1319    }
1320
1321    #[test]
1322    fn test_exclusive_group_too_small() {
1323        use crate::model::BuildError;
1324        let result = Command::builder("cmd")
1325            .flag(Flag::builder("json").build().unwrap())
1326            .exclusive(["json"])
1327            .build();
1328        assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
1329    }
1330
1331    #[test]
1332    fn test_exclusive_group_unknown_flag() {
1333        use crate::model::BuildError;
1334        let result = Command::builder("cmd")
1335            .flag(Flag::builder("json").build().unwrap())
1336            .exclusive(["json", "nonexistent"])
1337            .build();
1338        assert!(matches!(
1339            result,
1340            Err(BuildError::ExclusiveGroupUnknownFlag(_))
1341        ));
1342    }
1343
1344    #[test]
1345    fn test_parsed_command_helpers() {
1346        use crate::{Argument, Flag, Parser};
1347        let cmd = Command::builder("serve")
1348            .argument(Argument::builder("host").required().build().unwrap())
1349            .flag(
1350                Flag::builder("port")
1351                    .takes_value()
1352                    .default_value("8080")
1353                    .build()
1354                    .unwrap(),
1355            )
1356            .flag(Flag::builder("verbose").build().unwrap())
1357            .build()
1358            .unwrap();
1359        let cmds = vec![cmd];
1360        let parser = Parser::new(&cmds);
1361        let parsed = parser.parse(&["serve", "localhost", "--verbose"]).unwrap();
1362
1363        assert_eq!(parsed.arg("host"), Some("localhost"));
1364        assert_eq!(parsed.arg("missing"), None);
1365        assert_eq!(parsed.flag("port"), Some("8080"));
1366        assert_eq!(parsed.flag("missing"), None);
1367        assert!(parsed.flag_bool("verbose"));
1368        assert!(!parsed.flag_bool("missing"));
1369        assert_eq!(parsed.flag_count("verbose"), 1);
1370        assert_eq!(parsed.flag_count("missing"), 0);
1371        assert_eq!(parsed.flag_values("port"), vec!["8080"]);
1372        assert!(parsed.flag_values("missing").is_empty());
1373        assert!(parsed.has_flag("port"));
1374        assert!(!parsed.has_flag("missing"));
1375
1376        let port: u16 = parsed.flag_as("port").unwrap().unwrap();
1377        assert_eq!(port, 8080);
1378
1379        let port_or: u16 = parsed.flag_as_or("port", 9000);
1380        assert_eq!(port_or, 8080);
1381
1382        let missing_or: u16 = parsed.flag_as_or("missing", 9000);
1383        assert_eq!(missing_or, 9000);
1384
1385        let host: String = parsed.arg_as("host").unwrap().unwrap();
1386        assert_eq!(host, "localhost");
1387
1388        let missing_as: Option<Result<u32, _>> = parsed.arg_as("missing");
1389        assert!(missing_as.is_none());
1390
1391        let arg_or: String = parsed.arg_as_or("host", "default".to_string());
1392        assert_eq!(arg_or, "localhost");
1393
1394        let missing_arg_or: String = parsed.arg_as_or("missing", "default".to_string());
1395        assert_eq!(missing_arg_or, "default");
1396    }
1397
1398    #[test]
1399    fn test_flag_count_false_returns_zero() {
1400        use crate::{Flag, Parser};
1401        let cmd = Command::builder("cmd")
1402            .flag(Flag::builder("verbose").build().unwrap())
1403            .build()
1404            .unwrap();
1405        let cmds = vec![cmd];
1406        let parser = Parser::new(&cmds);
1407        let parsed = parser.parse(&["cmd", "--no-verbose"]).unwrap();
1408        assert_eq!(parsed.flag_count("verbose"), 0);
1409    }
1410
1411    #[test]
1412    fn test_flag_values_json_array() {
1413        use crate::{Flag, Parser};
1414        let cmd = Command::builder("cmd")
1415            .flag(
1416                Flag::builder("tag")
1417                    .takes_value()
1418                    .repeatable()
1419                    .build()
1420                    .unwrap(),
1421            )
1422            .build()
1423            .unwrap();
1424        let cmds = vec![cmd];
1425        let parser = Parser::new(&cmds);
1426        let parsed = parser.parse(&["cmd", "--tag=alpha", "--tag=beta"]).unwrap();
1427        let values = parsed.flag_values("tag");
1428        assert_eq!(values, vec!["alpha", "beta"]);
1429    }
1430}