Skip to main content

usage/spec/
builder.rs

1//! Builder patterns for ergonomic spec construction
2//!
3//! These builders allow constructing specs without manual Vec allocation,
4//! using variadic-friendly methods.
5//!
6//! # Examples
7//!
8//! ```
9//! use usage::{SpecFlagBuilder, SpecArgBuilder, SpecCommandBuilder};
10//!
11//! let flag = SpecFlagBuilder::new()
12//!     .name("verbose")
13//!     .short('v')
14//!     .long("verbose")
15//!     .help("Enable verbose output")
16//!     .build();
17//!
18//! let arg = SpecArgBuilder::new()
19//!     .name("files")
20//!     .var(true)
21//!     .var_min(1)
22//!     .help("Input files")
23//!     .build();
24//!
25//! let cmd = SpecCommandBuilder::new()
26//!     .name("install")
27//!     .aliases(["i", "add"])
28//!     .flag(flag)
29//!     .arg(arg)
30//!     .build();
31//! ```
32
33use crate::spec::cmd::SpecExample;
34use crate::spec::effect::SpecCommandEffect;
35use crate::{
36    spec::arg::SpecDoubleDashChoices, SpecArg, SpecChoices, SpecCommand, SpecDefaultIf, SpecFlag,
37    SpecRequiredIfEq, SpecRequiresIf,
38};
39
40/// Builder for SpecFlag
41#[derive(Debug, Default, Clone)]
42pub struct SpecFlagBuilder {
43    inner: SpecFlag,
44    allow_hyphen_values: bool,
45}
46
47impl SpecFlagBuilder {
48    /// Create a new SpecFlagBuilder
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Set the flag name
54    pub fn name(mut self, name: impl Into<String>) -> Self {
55        self.inner.name = name.into();
56        self
57    }
58
59    /// Add a short flag character (can be called multiple times)
60    pub fn short(mut self, c: char) -> Self {
61        self.inner.short.push(c);
62        self
63    }
64
65    /// Add multiple short flags at once
66    pub fn shorts(mut self, chars: impl IntoIterator<Item = char>) -> Self {
67        self.inner.short.extend(chars);
68        self
69    }
70
71    /// Add a long flag name (can be called multiple times)
72    pub fn long(mut self, name: impl Into<String>) -> Self {
73        self.inner.long.push(name.into());
74        self
75    }
76
77    /// Add multiple long flags at once
78    pub fn longs<I, S>(mut self, names: I) -> Self
79    where
80        I: IntoIterator<Item = S>,
81        S: Into<String>,
82    {
83        self.inner.long.extend(names.into_iter().map(Into::into));
84        self
85    }
86
87    /// Add a default value (can be called multiple times for var flags)
88    pub fn default_value(mut self, value: impl Into<String>) -> Self {
89        self.inner.default.push(value.into());
90        self.inner.required = false;
91        self
92    }
93
94    /// Add multiple default values at once
95    pub fn default_values<I, S>(mut self, values: I) -> Self
96    where
97        I: IntoIterator<Item = S>,
98        S: Into<String>,
99    {
100        self.inner
101            .default
102            .extend(values.into_iter().map(Into::into));
103        if !self.inner.default.is_empty() {
104            self.inner.required = false;
105        }
106        self
107    }
108
109    /// Set help text
110    pub fn help(mut self, text: impl Into<String>) -> Self {
111        self.inner.help = Some(text.into());
112        self
113    }
114
115    /// Set long help text
116    pub fn help_long(mut self, text: impl Into<String>) -> Self {
117        self.inner.help_long = Some(text.into());
118        self
119    }
120
121    /// Set markdown help text
122    pub fn help_md(mut self, text: impl Into<String>) -> Self {
123        self.inner.help_md = Some(text.into());
124        self
125    }
126
127    /// Set as variadic (can be specified multiple times)
128    pub fn var(mut self, is_var: bool) -> Self {
129        self.inner.var = is_var;
130        self
131    }
132
133    /// Set minimum count for variadic flag
134    pub fn var_min(mut self, min: usize) -> Self {
135        self.inner.var_min = Some(min);
136        self
137    }
138
139    /// Set maximum count for variadic flag
140    pub fn var_max(mut self, max: usize) -> Self {
141        self.inner.var_max = Some(max);
142        self
143    }
144
145    /// Set as required
146    pub fn required(mut self, is_required: bool) -> Self {
147        self.inner.required = is_required;
148        self
149    }
150
151    /// Add a flag whose presence makes this flag required
152    pub fn required_if(mut self, flag: impl Into<String>) -> Self {
153        self.inner.required_if.push(flag.into());
154        self
155    }
156
157    /// Add flags whose presence makes this flag required
158    pub fn required_if_any<I, S>(mut self, flags: I) -> Self
159    where
160        I: IntoIterator<Item = S>,
161        S: Into<String>,
162    {
163        self.inner
164            .required_if
165            .extend(flags.into_iter().map(Into::into));
166        self
167    }
168
169    /// Add a selector/value condition that makes this flag required.
170    pub fn required_if_eq(mut self, selector: impl Into<String>, value: impl Into<String>) -> Self {
171        self.inner.required_if_eq.push(SpecRequiredIfEq {
172            selector: selector.into(),
173            value: value.into(),
174        });
175        self
176    }
177
178    /// Set selector/value conditions which must all match to require this flag.
179    pub fn required_if_eq_all<I, S, V>(mut self, conditions: I) -> Self
180    where
181        I: IntoIterator<Item = (S, V)>,
182        S: Into<String>,
183        V: Into<String>,
184    {
185        self.inner
186            .required_if_eq_all
187            .extend(
188                conditions
189                    .into_iter()
190                    .map(|(selector, value)| SpecRequiredIfEq {
191                        selector: selector.into(),
192                        value: value.into(),
193                    }),
194            );
195        self
196    }
197
198    /// Add a flag whose absence makes this flag required
199    pub fn required_unless(mut self, flag: impl Into<String>) -> Self {
200        self.inner.required_unless.push(flag.into());
201        self
202    }
203
204    /// Add flags where the absence of all of them makes this flag required
205    pub fn required_unless_any<I, S>(mut self, flags: I) -> Self
206    where
207        I: IntoIterator<Item = S>,
208        S: Into<String>,
209    {
210        self.inner
211            .required_unless
212            .extend(flags.into_iter().map(Into::into));
213        self
214    }
215
216    /// Add selectors which must all be present to waive this flag's requirement.
217    pub fn required_unless_all<I, S>(mut self, flags: I) -> Self
218    where
219        I: IntoIterator<Item = S>,
220        S: Into<String>,
221    {
222        self.inner
223            .required_unless_all
224            .extend(flags.into_iter().map(Into::into));
225        self
226    }
227
228    /// Set as global (available to subcommands)
229    pub fn global(mut self, is_global: bool) -> Self {
230        self.inner.global = is_global;
231        self
232    }
233
234    /// Set as hidden
235    pub fn hide(mut self, is_hidden: bool) -> Self {
236        self.inner.hide = is_hidden;
237        self
238    }
239
240    /// Set as count flag
241    pub fn count(mut self, is_count: bool) -> Self {
242        self.inner.count = is_count;
243        self
244    }
245
246    /// Allow this flag's value to start with `-`
247    pub fn allow_hyphen_values(mut self, allow: bool) -> Self {
248        self.allow_hyphen_values = allow;
249        if let Some(arg) = &mut self.inner.arg {
250            arg.double_dash = if allow {
251                crate::spec::arg::SpecDoubleDashChoices::Automatic
252            } else {
253                crate::spec::arg::SpecDoubleDashChoices::Optional
254            };
255        }
256        self
257    }
258
259    /// Require `--flag=value` and refuse `--flag value`.
260    pub fn require_equals(mut self, require: bool) -> Self {
261        self.inner.require_equals = require;
262        self
263    }
264
265    /// Allow this flag to be present without a value.
266    pub fn value_optional(mut self, optional: bool) -> Self {
267        self.inner.value_optional = optional;
268        self
269    }
270
271    /// Allow `--flag=true` and `--flag=false` on a boolean switch.
272    pub fn bool_value(mut self, enabled: bool) -> Self {
273        self.inner.bool_value = enabled;
274        self
275    }
276
277    /// Value used when the flag is present but no value is given.
278    pub fn default_missing(mut self, value: impl Into<String>) -> Self {
279        self.inner.default_missing = Some(value.into());
280        self
281    }
282
283    /// Set the argument spec for flags that take values
284    pub fn arg(mut self, arg: SpecArg) -> Self {
285        self.inner.arg = Some(arg);
286        if self.allow_hyphen_values {
287            if let Some(arg) = &mut self.inner.arg {
288                arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
289            }
290        }
291        self
292    }
293
294    /// Set negate string
295    pub fn negate(mut self, negate: impl Into<String>) -> Self {
296        self.inner.negate = Some(negate.into());
297        self
298    }
299
300    /// Add a flag that this flag mutually overrides
301    pub fn override_with(mut self, flag: impl Into<String>) -> Self {
302        self.inner.overrides.push(flag.into());
303        self
304    }
305
306    /// Add flags that this flag mutually overrides
307    pub fn overrides_with<I, S>(mut self, flags: I) -> Self
308    where
309        I: IntoIterator<Item = S>,
310        S: Into<String>,
311    {
312        self.inner
313            .overrides
314            .extend(flags.into_iter().map(Into::into));
315        self
316    }
317
318    /// Add a flag that must also be given when this one is
319    pub fn require(mut self, flag: impl Into<String>) -> Self {
320        self.inner.requires.push(flag.into());
321        self
322    }
323
324    /// Add flags that must also be given when this one is
325    pub fn requires<I, S>(mut self, flags: I) -> Self
326    where
327        I: IntoIterator<Item = S>,
328        S: Into<String>,
329    {
330        self.inner
331            .requires
332            .extend(flags.into_iter().map(Into::into));
333        self
334    }
335
336    /// Add a flag required when this flag is explicitly given `value`
337    pub fn requires_if(mut self, value: impl Into<String>, flag: impl Into<String>) -> Self {
338        self.inner.requires_if.push(SpecRequiresIf {
339            value: value.into(),
340            requires: flag.into(),
341        });
342        self
343    }
344
345    /// Add value-conditional flag requirements
346    pub fn requires_ifs<I, V, S>(mut self, requirements: I) -> Self
347    where
348        I: IntoIterator<Item = (V, S)>,
349        V: Into<String>,
350        S: Into<String>,
351    {
352        self.inner
353            .requires_if
354            .extend(
355                requirements
356                    .into_iter()
357                    .map(|(value, requires)| SpecRequiresIf {
358                        value: value.into(),
359                        requires: requires.into(),
360                    }),
361            );
362        self
363    }
364
365    /// Bind `value` on this flag when `selector` is present.
366    ///
367    /// clap's `default_value_if(id, ArgPredicate::IsPresent, value)`.
368    pub fn default_if(mut self, selector: impl Into<String>, value: impl Into<String>) -> Self {
369        self.inner.default_if.push(SpecDefaultIf {
370            selector: selector.into(),
371            when: None,
372            value: value.into(),
373        });
374        self
375    }
376
377    /// Bind `value` on this flag when `selector` is explicitly `when`.
378    ///
379    /// clap's `default_value_if(id, ArgPredicate::Equals(when), value)`.
380    pub fn default_if_eq(
381        mut self,
382        selector: impl Into<String>,
383        when: impl Into<String>,
384        value: impl Into<String>,
385    ) -> Self {
386        self.inner.default_if.push(SpecDefaultIf {
387            selector: selector.into(),
388            when: Some(when.into()),
389            value: value.into(),
390        });
391        self
392    }
393
394    /// Add several conditional defaults, in first-match-wins order.
395    pub fn default_ifs<I>(mut self, conditions: I) -> Self
396    where
397        I: IntoIterator<Item = SpecDefaultIf>,
398    {
399        self.inner.default_if.extend(conditions);
400        self
401    }
402
403    /// Heading to list this under in help output.
404    pub fn help_heading(mut self, help_heading: impl Into<String>) -> Self {
405        self.inner.help_heading = Some(help_heading.into());
406        self
407    }
408
409    /// Make this flag request help or version output instead of binding a value.
410    pub fn action(mut self, action: crate::SpecFlagAction) -> Self {
411        self.inner.action = action;
412        self
413    }
414
415    pub fn env(mut self, env: impl Into<String>) -> Self {
416        self.inner.env = Some(env.into());
417        self
418    }
419
420    /// Add an environment variable fallback, consulted in declaration order.
421    pub fn env_fallback(mut self, env: impl Into<String>) -> Self {
422        self.inner.env_fallback.push(env.into());
423        self
424    }
425
426    /// Add a deprecated environment variable alias.
427    pub fn deprecated_env(mut self, env: impl Into<String>) -> Self {
428        self.inner.deprecated_env.push(env.into());
429        self
430    }
431
432    /// Set deprecated message
433    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
434        self.inner.deprecated = Some(msg.into());
435        self
436    }
437
438    pub fn deprecated_warn_at(mut self, version: impl Into<String>) -> Self {
439        self.inner.deprecated_warn_at = Some(version.into());
440        self
441    }
442
443    pub fn deprecated_remove_at(mut self, version: impl Into<String>) -> Self {
444        self.inner.deprecated_remove_at = Some(version.into());
445        self
446    }
447
448    /// Set the rendered usage string. `build` derives this when unset.
449    pub fn usage(mut self, usage: impl Into<String>) -> Self {
450        self.inner.usage = usage.into();
451        self
452    }
453
454    /// Set the first line of help text. Derived from `help` when unset.
455    pub fn help_first_line(mut self, text: impl Into<String>) -> Self {
456        self.inner.help_first_line = Some(text.into());
457        self
458    }
459
460    /// Raise the command's effect when this flag is supplied.
461    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
462        self.inner.effect = Some(effect);
463        self
464    }
465
466    /// Build the final SpecFlag
467    #[must_use]
468    pub fn build(mut self) -> SpecFlag {
469        if self.allow_hyphen_values {
470            if let Some(arg) = &mut self.inner.arg {
471                arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
472            }
473        }
474        if self.inner.default_missing.is_some() {
475            if let Some(arg) = &mut self.inner.arg {
476                arg.required = false;
477            }
478        }
479        self.inner.usage = self.inner.usage();
480        if self.inner.name.is_empty() {
481            // Derive name from long or short flags
482            if let Some(long) = self.inner.long.first() {
483                self.inner.name = long.clone();
484            } else if let Some(short) = self.inner.short.first() {
485                self.inner.name = short.to_string();
486            }
487        }
488        self.inner
489    }
490}
491
492/// Builder for SpecArg
493#[derive(Debug, Default, Clone)]
494pub struct SpecArgBuilder {
495    inner: SpecArg,
496}
497
498impl SpecArgBuilder {
499    /// Create a new SpecArgBuilder
500    pub fn new() -> Self {
501        Self::default()
502    }
503
504    /// Set the argument name
505    pub fn name(mut self, name: impl Into<String>) -> Self {
506        self.inner.name = name.into();
507        self
508    }
509
510    /// Set the ordered placeholders for a fixed-arity value.
511    pub fn value_names<I, S>(mut self, names: I) -> Self
512    where
513        I: IntoIterator<Item = S>,
514        S: Into<String>,
515    {
516        self.inner.value_names = names.into_iter().map(Into::into).collect();
517        if let Some(first) = self.inner.value_names.first() {
518            self.inner.name.clone_from(first);
519        }
520        if self.inner.value_names.len() > 1 {
521            let arity = self.inner.value_names.len();
522            self.inner.var = true;
523            self.inner.var_min = Some(arity);
524            self.inner.var_max = Some(arity);
525        }
526        self
527    }
528
529    /// Add a default value (can be called multiple times for var args)
530    pub fn default_value(mut self, value: impl Into<String>) -> Self {
531        self.inner.default.push(value.into());
532        self.inner.required = false;
533        self
534    }
535
536    /// Add multiple default values at once
537    pub fn default_values<I, S>(mut self, values: I) -> Self
538    where
539        I: IntoIterator<Item = S>,
540        S: Into<String>,
541    {
542        self.inner
543            .default
544            .extend(values.into_iter().map(Into::into));
545        if !self.inner.default.is_empty() {
546            self.inner.required = false;
547        }
548        self
549    }
550
551    /// Set help text
552    pub fn help(mut self, text: impl Into<String>) -> Self {
553        self.inner.help = Some(text.into());
554        self
555    }
556
557    /// Set long help text
558    pub fn help_long(mut self, text: impl Into<String>) -> Self {
559        self.inner.help_long = Some(text.into());
560        self
561    }
562
563    /// Set markdown help text
564    pub fn help_md(mut self, text: impl Into<String>) -> Self {
565        self.inner.help_md = Some(text.into());
566        self
567    }
568
569    /// Set as variadic (accepts multiple values)
570    pub fn var(mut self, is_var: bool) -> Self {
571        self.inner.var = is_var;
572        self
573    }
574
575    /// Set minimum count for variadic argument
576    pub fn var_min(mut self, min: usize) -> Self {
577        self.inner.var_min = Some(min);
578        self
579    }
580
581    /// Set maximum count for variadic argument
582    pub fn var_max(mut self, max: usize) -> Self {
583        self.inner.var_max = Some(max);
584        self
585    }
586
587    /// Set as required
588    pub fn required(mut self, is_required: bool) -> Self {
589        self.inner.required = is_required;
590        self
591    }
592
593    /// Add arguments that must be satisfied when this positional is present.
594    pub fn requires<I, S>(mut self, selectors: I) -> Self
595    where
596        I: IntoIterator<Item = S>,
597        S: Into<String>,
598    {
599        self.inner
600            .requires
601            .extend(selectors.into_iter().map(Into::into));
602        self
603    }
604
605    /// Add selectors whose presence makes this positional required.
606    pub fn required_if_any<I, S>(mut self, selectors: I) -> Self
607    where
608        I: IntoIterator<Item = S>,
609        S: Into<String>,
610    {
611        self.inner
612            .required_if
613            .extend(selectors.into_iter().map(Into::into));
614        self
615    }
616
617    /// Add a selector/value condition that makes this positional required.
618    pub fn required_if_eq(mut self, selector: impl Into<String>, value: impl Into<String>) -> Self {
619        self.inner.required_if_eq.push(SpecRequiredIfEq {
620            selector: selector.into(),
621            value: value.into(),
622        });
623        self
624    }
625
626    /// Set selector/value conditions which must all match to require this positional.
627    pub fn required_if_eq_all<I, S, V>(mut self, conditions: I) -> Self
628    where
629        I: IntoIterator<Item = (S, V)>,
630        S: Into<String>,
631        V: Into<String>,
632    {
633        self.inner
634            .required_if_eq_all
635            .extend(
636                conditions
637                    .into_iter()
638                    .map(|(selector, value)| SpecRequiredIfEq {
639                        selector: selector.into(),
640                        value: value.into(),
641                    }),
642            );
643        self
644    }
645
646    /// Add selectors where any presence waives this positional's requirement.
647    pub fn required_unless_any<I, S>(mut self, selectors: I) -> Self
648    where
649        I: IntoIterator<Item = S>,
650        S: Into<String>,
651    {
652        self.inner
653            .required_unless
654            .extend(selectors.into_iter().map(Into::into));
655        self
656    }
657
658    /// Add selectors which must all be present to waive this positional's requirement.
659    pub fn required_unless_all<I, S>(mut self, selectors: I) -> Self
660    where
661        I: IntoIterator<Item = S>,
662        S: Into<String>,
663    {
664        self.inner
665            .required_unless_all
666            .extend(selectors.into_iter().map(Into::into));
667        self
668    }
669
670    /// Set as hidden
671    pub fn hide(mut self, is_hidden: bool) -> Self {
672        self.inner.hide = is_hidden;
673        self
674    }
675
676    /// Set environment variable name
677    /// Heading to list this under in help output.
678    pub fn help_heading(mut self, help_heading: impl Into<String>) -> Self {
679        self.inner.help_heading = Some(help_heading.into());
680        self
681    }
682
683    pub fn env(mut self, env: impl Into<String>) -> Self {
684        self.inner.env = Some(env.into());
685        self
686    }
687
688    /// Add an environment fallback, consulted after the canonical variable.
689    pub fn env_fallback(mut self, env: impl Into<String>) -> Self {
690        self.inner.env_fallback.push(env.into());
691        self
692    }
693
694    /// Add a deprecated environment alias, consulted after ordinary fallbacks.
695    pub fn deprecated_env(mut self, env: impl Into<String>) -> Self {
696        self.inner.deprecated_env.push(env.into());
697        self
698    }
699
700    /// Set the double-dash behavior
701    pub fn double_dash(mut self, behavior: SpecDoubleDashChoices) -> Self {
702        self.inner.double_dash = behavior;
703        self
704    }
705
706    /// Set choices for this argument
707    pub fn choices<I, S>(mut self, choices: I) -> Self
708    where
709        I: IntoIterator<Item = S>,
710        S: Into<String>,
711    {
712        let spec_choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
713        #[cfg(feature = "unstable_choices_env")]
714        let env = spec_choices.env().map(ToString::to_string);
715        spec_choices.choices = choices.into_iter().map(Into::into).collect();
716        #[cfg(feature = "unstable_choices_env")]
717        spec_choices.set_env(env);
718        self
719    }
720
721    /// Set a portable expr expression that must accept each raw value.
722    pub fn validate(mut self, expression: impl Into<String>) -> Self {
723        self.inner.validate = Some(expression.into());
724        self
725    }
726
727    /// Set the message reported when validation returns false.
728    pub fn validate_error(mut self, message: impl Into<String>) -> Self {
729        self.inner.validate_error = Some(message.into());
730        self
731    }
732
733    /// Set choices from an environment variable
734    #[cfg(feature = "unstable_choices_env")]
735    pub fn choices_env(mut self, env: impl Into<String>) -> Self {
736        let choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
737        choices.set_env(Some(env.into()));
738        self
739    }
740
741    /// Set the rendered usage string. `build` derives this when unset.
742    pub fn usage(mut self, usage: impl Into<String>) -> Self {
743        self.inner.usage = usage.into();
744        self
745    }
746
747    /// Set the first line of help text. Derived from `help` when unset.
748    pub fn help_first_line(mut self, text: impl Into<String>) -> Self {
749        self.inner.help_first_line = Some(text.into());
750        self
751    }
752
753    /// Raise the command's effect when this argument is supplied.
754    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
755        self.inner.effect = Some(effect);
756        self
757    }
758
759    /// Build the final SpecArg
760    #[must_use]
761    pub fn build(mut self) -> SpecArg {
762        if self.inner.validate.is_none() {
763            self.inner.validate_error = None;
764        }
765        if self.inner.value_names.len() > 1 {
766            let arity = self.inner.value_names.len();
767            self.inner.var = true;
768            self.inner.var_min = Some(arity);
769            self.inner.var_max = Some(arity);
770        }
771        self.inner.usage = self.inner.usage();
772        self.inner
773    }
774}
775
776/// Builder for SpecCommand
777#[derive(Debug, Default, Clone)]
778pub struct SpecCommandBuilder {
779    inner: SpecCommand,
780}
781
782impl SpecCommandBuilder {
783    /// Create a new SpecCommandBuilder
784    pub fn new() -> Self {
785        Self::default()
786    }
787
788    /// Set the command name
789    pub fn name(mut self, name: impl Into<String>) -> Self {
790        self.inner.name = name.into();
791        self
792    }
793
794    /// Add an alias (can be called multiple times)
795    pub fn alias(mut self, alias: impl Into<String>) -> Self {
796        self.inner.aliases.push(alias.into());
797        self
798    }
799
800    /// Add multiple aliases at once
801    pub fn aliases<I, S>(mut self, aliases: I) -> Self
802    where
803        I: IntoIterator<Item = S>,
804        S: Into<String>,
805    {
806        self.inner
807            .aliases
808            .extend(aliases.into_iter().map(Into::into));
809        self
810    }
811
812    /// Add a hidden alias (can be called multiple times)
813    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
814        self.inner.hidden_aliases.push(alias.into());
815        self
816    }
817
818    /// Add multiple hidden aliases at once
819    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
820    where
821        I: IntoIterator<Item = S>,
822        S: Into<String>,
823    {
824        self.inner
825            .hidden_aliases
826            .extend(aliases.into_iter().map(Into::into));
827        self
828    }
829
830    /// Add a flag to the command
831    pub fn flag(mut self, flag: SpecFlag) -> Self {
832        self.inner.flags.push(flag);
833        self
834    }
835
836    /// Add multiple flags at once
837    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
838        self.inner.flags.extend(flags);
839        self
840    }
841
842    /// Add an argument to the command
843    pub fn arg(mut self, arg: SpecArg) -> Self {
844        self.inner.args.push(arg);
845        self
846    }
847
848    /// Add multiple arguments at once
849    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
850        self.inner.args.extend(args);
851        self
852    }
853
854    /// Set help text
855    pub fn help(mut self, text: impl Into<String>) -> Self {
856        self.inner.help = Some(text.into());
857        self
858    }
859
860    /// Set long help text
861    pub fn help_long(mut self, text: impl Into<String>) -> Self {
862        self.inner.help_long = Some(text.into());
863        self
864    }
865
866    /// Set markdown help text
867    pub fn help_md(mut self, text: impl Into<String>) -> Self {
868        self.inner.help_md = Some(text.into());
869        self
870    }
871
872    /// Set as hidden
873    pub fn hide(mut self, is_hidden: bool) -> Self {
874        self.inner.hide = is_hidden;
875        self
876    }
877
878    /// Set subcommand required
879    pub fn subcommand_required(mut self, required: bool) -> Self {
880        self.inner.subcommand_required = required;
881        self
882    }
883
884    /// Set the heading for this command's subcommand list.
885    pub fn subcommand_help_heading(mut self, heading: impl Into<String>) -> Self {
886        self.inner.subcommand_help_heading = Some(heading.into());
887        self
888    }
889
890    /// Set the synopsis placeholder for a subcommand.
891    pub fn subcommand_value_name(mut self, name: impl Into<String>) -> Self {
892        self.inner.subcommand_value_name = Some(name.into());
893        self
894    }
895
896    /// Set a fixed help width. Zero disables wrapping.
897    pub fn term_width(mut self, width: usize) -> Self {
898        self.inner.term_width = Some(width);
899        self
900    }
901
902    /// Cap detected terminal width when no fixed width is set. Zero disables the cap.
903    pub fn max_term_width(mut self, width: usize) -> Self {
904        self.inner.max_term_width = Some(width);
905        self
906    }
907
908    /// Forward an unmatched word as an external command plus the rest of argv
909    pub fn external_subcommand(mut self, enabled: bool) -> Self {
910        self.inner.external_subcommand = enabled;
911        self
912    }
913
914    /// Enable or disable the synthesized `--help` and `-h` flags.
915    pub fn disable_help_flag(mut self, disabled: bool) -> Self {
916        self.inner.disable_help_flag = disabled;
917        self
918    }
919
920    /// Enable or disable the synthesized `help` subcommand route.
921    pub fn disable_help_subcommand(mut self, disabled: bool) -> Self {
922        self.inner.disable_help_subcommand = disabled;
923        self
924    }
925
926    /// Enable or disable the synthesized `--version` and `-V` flags.
927    pub fn disable_version_flag(mut self, disabled: bool) -> Self {
928        self.inner.disable_version_flag = disabled;
929        self
930    }
931
932    /// Set whether a later scalar flag occurrence replaces an earlier one.
933    ///
934    /// Enabled by default. Set false to reject duplicate scalar flags.
935    pub fn args_override_self(mut self, enabled: bool) -> Self {
936        self.inner.args_override_self = enabled;
937        self
938    }
939
940    /// Set whether selecting a subcommand suppresses this command's requirements.
941    pub fn subcommand_negates_reqs(mut self, enabled: bool) -> Self {
942        self.inner.subcommand_negates_reqs = enabled;
943        self
944    }
945
946    /// Set whether arguments on this command exclude a later subcommand.
947    pub fn args_conflicts_with_subcommands(mut self, enabled: bool) -> Self {
948        self.inner.args_conflicts_with_subcommands = enabled;
949        self
950    }
951
952    pub fn subcommand_precedence_over_arg(mut self, enabled: bool) -> Self {
953        self.inner.subcommand_precedence_over_arg = enabled;
954        self
955    }
956
957    pub fn allow_missing_positional(mut self, enabled: bool) -> Self {
958        self.inner.allow_missing_positional = enabled;
959        self
960    }
961
962    /// Set what running this command does to the world
963    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
964        self.inner.effect = Some(effect);
965        self
966    }
967
968    /// Set deprecated message
969    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
970        self.inner.deprecated = Some(msg.into());
971        self
972    }
973
974    pub fn deprecated_warn_at(mut self, version: impl Into<String>) -> Self {
975        self.inner.deprecated_warn_at = Some(version.into());
976        self
977    }
978
979    pub fn deprecated_remove_at(mut self, version: impl Into<String>) -> Self {
980        self.inner.deprecated_remove_at = Some(version.into());
981        self
982    }
983
984    /// Set restart token for resetting argument parsing
985    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
986    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
987        self.inner.restart_token = Some(token.into());
988        self
989    }
990
991    /// Add a subcommand (can be called multiple times)
992    pub fn subcommand(mut self, cmd: SpecCommand) -> Self {
993        self.inner.subcommands.insert(cmd.name.clone(), cmd);
994        self
995    }
996
997    /// Add multiple subcommands at once
998    pub fn subcommands(mut self, cmds: impl IntoIterator<Item = SpecCommand>) -> Self {
999        for cmd in cmds {
1000            self.inner.subcommands.insert(cmd.name.clone(), cmd);
1001        }
1002        self
1003    }
1004
1005    /// Set before_help text (displayed before the help message)
1006    pub fn before_help(mut self, text: impl Into<String>) -> Self {
1007        self.inner.before_help = Some(text.into());
1008        self
1009    }
1010
1011    /// Set before_help_long text
1012    pub fn before_help_long(mut self, text: impl Into<String>) -> Self {
1013        self.inner.before_help_long = Some(text.into());
1014        self
1015    }
1016
1017    /// Set before_help markdown text
1018    pub fn before_help_md(mut self, text: impl Into<String>) -> Self {
1019        self.inner.before_help_md = Some(text.into());
1020        self
1021    }
1022
1023    /// Set after_help text (displayed after the help message)
1024    pub fn after_help(mut self, text: impl Into<String>) -> Self {
1025        self.inner.after_help = Some(text.into());
1026        self
1027    }
1028
1029    /// Set after_help_long text
1030    pub fn after_help_long(mut self, text: impl Into<String>) -> Self {
1031        self.inner.after_help_long = Some(text.into());
1032        self
1033    }
1034
1035    /// Set after_help markdown text
1036    pub fn after_help_md(mut self, text: impl Into<String>) -> Self {
1037        self.inner.after_help_md = Some(text.into());
1038        self
1039    }
1040
1041    /// Add an example (can be called multiple times)
1042    pub fn example(mut self, code: impl Into<String>) -> Self {
1043        self.inner.examples.push(SpecExample::new(code.into()));
1044        self
1045    }
1046
1047    /// Add an example with header and help text
1048    pub fn example_with_help(
1049        mut self,
1050        code: impl Into<String>,
1051        header: impl Into<String>,
1052        help: impl Into<String>,
1053    ) -> Self {
1054        let mut example = SpecExample::new(code.into());
1055        example.header = Some(header.into());
1056        example.help = Some(help.into());
1057        self.inner.examples.push(example);
1058        self
1059    }
1060
1061    /// Build the final SpecCommand
1062    #[must_use]
1063    pub fn build(mut self) -> SpecCommand {
1064        self.inner.usage = self.inner.usage();
1065        self.inner
1066    }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071    use super::*;
1072
1073    #[test]
1074    fn test_flag_builder_basic() {
1075        let flag = SpecFlagBuilder::new()
1076            .name("verbose")
1077            .short('v')
1078            .long("verbose")
1079            .help("Enable verbose output")
1080            .build();
1081
1082        assert_eq!(flag.name, "verbose");
1083        assert_eq!(flag.short, vec!['v']);
1084        assert_eq!(flag.long, vec!["verbose".to_string()]);
1085        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
1086    }
1087
1088    #[test]
1089    fn test_flag_builder_multiple_values() {
1090        let flag = SpecFlagBuilder::new()
1091            .shorts(['v', 'V'])
1092            .longs(["verbose", "loud"])
1093            .default_values(["info", "warn"])
1094            .build();
1095
1096        assert_eq!(flag.short, vec!['v', 'V']);
1097        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
1098        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
1099        assert!(!flag.required); // Should be false due to defaults
1100    }
1101
1102    #[test]
1103    fn test_flag_builder_variadic() {
1104        let flag = SpecFlagBuilder::new()
1105            .long("file")
1106            .var(true)
1107            .var_min(1)
1108            .var_max(10)
1109            .build();
1110
1111        assert!(flag.var);
1112        assert_eq!(flag.var_min, Some(1));
1113        assert_eq!(flag.var_max, Some(10));
1114    }
1115
1116    #[test]
1117    fn test_flag_builder_conditional_requirements() {
1118        let flag = SpecFlagBuilder::new()
1119            .long("config")
1120            .requires_if("special.toml", "--key")
1121            .requires_ifs([("remote.toml", "--token"), ("signed.toml", "--identity")])
1122            .build();
1123
1124        assert_eq!(
1125            flag.requires_if,
1126            [
1127                SpecRequiresIf {
1128                    value: "special.toml".into(),
1129                    requires: "--key".into(),
1130                },
1131                SpecRequiresIf {
1132                    value: "remote.toml".into(),
1133                    requires: "--token".into(),
1134                },
1135                SpecRequiresIf {
1136                    value: "signed.toml".into(),
1137                    requires: "--identity".into(),
1138                },
1139            ]
1140        );
1141    }
1142
1143    #[test]
1144    fn test_flag_builder_conditional_defaults() {
1145        let flag = SpecFlagBuilder::new()
1146            .long("bin-names")
1147            .default_if("--json", "true")
1148            .default_if_eq("--output", "json", "pretty")
1149            .build();
1150
1151        assert_eq!(
1152            flag.default_if,
1153            [
1154                SpecDefaultIf {
1155                    selector: "--json".into(),
1156                    when: None,
1157                    value: "true".into(),
1158                },
1159                SpecDefaultIf {
1160                    selector: "--output".into(),
1161                    when: Some("json".into()),
1162                    value: "pretty".into(),
1163                },
1164            ]
1165        );
1166    }
1167
1168    #[test]
1169    fn test_flag_builder_name_derivation() {
1170        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
1171
1172        // Name should be derived from long flag
1173        assert_eq!(flag.name, "verbose");
1174
1175        let flag2 = SpecFlagBuilder::new().short('v').build();
1176
1177        // Name should be derived from short flag if no long
1178        assert_eq!(flag2.name, "v");
1179    }
1180
1181    #[test]
1182    fn test_arg_builder_basic() {
1183        let arg = SpecArgBuilder::new()
1184            .name("file")
1185            .help("Input file")
1186            .required(true)
1187            .build();
1188
1189        assert_eq!(arg.name, "file");
1190        assert_eq!(arg.help, Some("Input file".to_string()));
1191        assert!(arg.required);
1192    }
1193
1194    #[test]
1195    fn test_arg_builder_variadic() {
1196        let arg = SpecArgBuilder::new()
1197            .name("files")
1198            .var(true)
1199            .var_min(1)
1200            .var_max(10)
1201            .help("Input files")
1202            .build();
1203
1204        assert_eq!(arg.name, "files");
1205        assert!(arg.var);
1206        assert_eq!(arg.var_min, Some(1));
1207        assert_eq!(arg.var_max, Some(10));
1208    }
1209
1210    #[test]
1211    fn test_arg_builder_defaults() {
1212        let arg = SpecArgBuilder::new()
1213            .name("file")
1214            .default_values(["a.txt", "b.txt"])
1215            .build();
1216
1217        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
1218        assert!(!arg.required);
1219    }
1220
1221    #[test]
1222    fn test_arg_builder_drops_validation_error_without_expression() {
1223        let arg = SpecArgBuilder::new()
1224            .name("port")
1225            .validate_error("must be a valid port")
1226            .build();
1227
1228        assert!(arg.validate_error.is_none());
1229    }
1230
1231    #[test]
1232    fn test_command_builder_basic() {
1233        let cmd = SpecCommandBuilder::new()
1234            .name("install")
1235            .help("Install packages")
1236            .build();
1237
1238        assert_eq!(cmd.name, "install");
1239        assert_eq!(cmd.help, Some("Install packages".to_string()));
1240    }
1241
1242    #[test]
1243    fn test_command_builder_aliases() {
1244        let cmd = SpecCommandBuilder::new()
1245            .name("install")
1246            .alias("i")
1247            .aliases(["add", "get"])
1248            .hidden_aliases(["inst"])
1249            .build();
1250
1251        assert_eq!(
1252            cmd.aliases,
1253            vec!["i".to_string(), "add".to_string(), "get".to_string()]
1254        );
1255        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
1256    }
1257
1258    #[test]
1259    fn test_command_builder_with_flags_and_args() {
1260        let flag = SpecFlagBuilder::new().short('f').long("force").build();
1261
1262        let arg = SpecArgBuilder::new().name("package").required(true).build();
1263
1264        let cmd = SpecCommandBuilder::new()
1265            .name("install")
1266            .flag(flag)
1267            .arg(arg)
1268            .build();
1269
1270        assert_eq!(cmd.flags.len(), 1);
1271        assert_eq!(cmd.flags[0].name, "force");
1272        assert_eq!(cmd.args.len(), 1);
1273        assert_eq!(cmd.args[0].name, "package");
1274    }
1275
1276    #[test]
1277    fn test_arg_builder_choices() {
1278        let arg = SpecArgBuilder::new()
1279            .name("format")
1280            .choices(["json", "yaml", "toml"])
1281            .build();
1282
1283        assert!(arg.choices.is_some());
1284        let choices = arg.choices.unwrap();
1285        assert_eq!(
1286            choices.choices,
1287            vec!["json".to_string(), "yaml".to_string(), "toml".to_string()]
1288        );
1289        assert_eq!(choices.env(), None);
1290    }
1291
1292    #[cfg(feature = "unstable_choices_env")]
1293    #[test]
1294    fn test_arg_builder_choices_env() {
1295        let arg = SpecArgBuilder::new()
1296            .name("env")
1297            .choices(["local"])
1298            .choices_env("DEPLOY_ENVS")
1299            .build();
1300
1301        let choices = arg.choices.unwrap();
1302        assert_eq!(choices.choices, vec!["local".to_string()]);
1303        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1304    }
1305
1306    #[cfg(feature = "unstable_choices_env")]
1307    #[test]
1308    fn test_arg_builder_choices_preserves_choices_env() {
1309        let arg = SpecArgBuilder::new()
1310            .name("env")
1311            .choices_env("DEPLOY_ENVS")
1312            .choices(["local"])
1313            .build();
1314
1315        let choices = arg.choices.unwrap();
1316        assert_eq!(choices.choices, vec!["local".to_string()]);
1317        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1318    }
1319
1320    #[test]
1321    fn test_command_builder_subcommands() {
1322        let sub1 = SpecCommandBuilder::new().name("sub1").build();
1323        let sub2 = SpecCommandBuilder::new().name("sub2").build();
1324
1325        let cmd = SpecCommandBuilder::new()
1326            .name("main")
1327            .subcommand(sub1)
1328            .subcommand(sub2)
1329            .build();
1330
1331        assert_eq!(cmd.subcommands.len(), 2);
1332        assert!(cmd.subcommands.contains_key("sub1"));
1333        assert!(cmd.subcommands.contains_key("sub2"));
1334    }
1335
1336    #[test]
1337    fn test_command_builder_before_after_help() {
1338        let cmd = SpecCommandBuilder::new()
1339            .name("test")
1340            .before_help("Before help text")
1341            .before_help_long("Before help long text")
1342            .after_help("After help text")
1343            .after_help_long("After help long text")
1344            .build();
1345
1346        assert_eq!(cmd.before_help, Some("Before help text".to_string()));
1347        assert_eq!(
1348            cmd.before_help_long,
1349            Some("Before help long text".to_string())
1350        );
1351        assert_eq!(cmd.after_help, Some("After help text".to_string()));
1352        assert_eq!(
1353            cmd.after_help_long,
1354            Some("After help long text".to_string())
1355        );
1356    }
1357
1358    #[test]
1359    fn test_command_builder_examples() {
1360        let cmd = SpecCommandBuilder::new()
1361            .name("test")
1362            .example("mycli run")
1363            .example_with_help("mycli build", "Build example", "Build the project")
1364            .build();
1365
1366        assert_eq!(cmd.examples.len(), 2);
1367        assert_eq!(cmd.examples[0].code, "mycli run");
1368        assert_eq!(cmd.examples[1].code, "mycli build");
1369        assert_eq!(cmd.examples[1].header, Some("Build example".to_string()));
1370        assert_eq!(cmd.examples[1].help, Some("Build the project".to_string()));
1371    }
1372}