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