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    /// Classify this positional by a leading token prefix and strip it on binding.
547    pub fn sigil(mut self, sigil: impl Into<String>) -> Self {
548        self.inner.sigil = Some(sigil.into());
549        self
550    }
551
552    /// Set the ordered placeholders for a fixed-arity value.
553    pub fn value_names<I, S>(mut self, names: I) -> Self
554    where
555        I: IntoIterator<Item = S>,
556        S: Into<String>,
557    {
558        self.inner.value_names = names.into_iter().map(Into::into).collect();
559        if let Some(first) = self.inner.value_names.first() {
560            self.inner.name.clone_from(first);
561        }
562        if self.inner.value_names.len() > 1 {
563            let arity = self.inner.value_names.len();
564            self.inner.var = true;
565            self.inner.var_min = Some(arity);
566            self.inner.var_max = Some(arity);
567        }
568        self
569    }
570
571    /// Add a default value (can be called multiple times for var args)
572    pub fn default_value(mut self, value: impl Into<String>) -> Self {
573        self.inner.default.push(value.into());
574        self.inner.required = false;
575        self
576    }
577
578    /// Add multiple default values at once
579    pub fn default_values<I, S>(mut self, values: I) -> Self
580    where
581        I: IntoIterator<Item = S>,
582        S: Into<String>,
583    {
584        self.inner
585            .default
586            .extend(values.into_iter().map(Into::into));
587        if !self.inner.default.is_empty() {
588            self.inner.required = false;
589        }
590        self
591    }
592
593    /// Set help text
594    pub fn help(mut self, text: impl Into<String>) -> Self {
595        self.inner.help = Some(text.into());
596        self
597    }
598
599    /// Set long help text
600    pub fn help_long(mut self, text: impl Into<String>) -> Self {
601        self.inner.help_long = Some(text.into());
602        self
603    }
604
605    /// Set markdown help text
606    pub fn help_md(mut self, text: impl Into<String>) -> Self {
607        self.inner.help_md = Some(text.into());
608        self
609    }
610
611    /// Add a semantic note rendered in help and generated documentation.
612    pub fn note(mut self, text: impl Into<String>) -> Self {
613        self.inner.admonitions.push(SpecAdmonition::note(text));
614        self
615    }
616
617    /// Add a semantic warning rendered in help and generated documentation.
618    pub fn warning(mut self, text: impl Into<String>) -> Self {
619        self.inner.admonitions.push(SpecAdmonition::warning(text));
620        self
621    }
622
623    /// Set as variadic (accepts multiple values)
624    pub fn var(mut self, is_var: bool) -> Self {
625        self.inner.var = is_var;
626        self
627    }
628
629    /// Set minimum count for variadic argument
630    pub fn var_min(mut self, min: usize) -> Self {
631        self.inner.var_min = Some(min);
632        self
633    }
634
635    /// Set maximum count for variadic argument
636    pub fn var_max(mut self, max: usize) -> Self {
637        self.inner.var_max = Some(max);
638        self
639    }
640
641    /// Set as required
642    pub fn required(mut self, is_required: bool) -> Self {
643        self.inner.required = is_required;
644        self
645    }
646
647    /// Add arguments that must be satisfied when this positional is present.
648    pub fn requires<I, S>(mut self, selectors: I) -> Self
649    where
650        I: IntoIterator<Item = S>,
651        S: Into<String>,
652    {
653        self.inner
654            .requires
655            .extend(selectors.into_iter().map(Into::into));
656        self
657    }
658
659    /// Add selectors whose presence makes this positional required.
660    pub fn required_if_any<I, S>(mut self, selectors: I) -> Self
661    where
662        I: IntoIterator<Item = S>,
663        S: Into<String>,
664    {
665        self.inner
666            .required_if
667            .extend(selectors.into_iter().map(Into::into));
668        self
669    }
670
671    /// Add a selector/value condition that makes this positional required.
672    pub fn required_if_eq(mut self, selector: impl Into<String>, value: impl Into<String>) -> Self {
673        self.inner.required_if_eq.push(SpecRequiredIfEq {
674            selector: selector.into(),
675            value: value.into(),
676        });
677        self
678    }
679
680    /// Set selector/value conditions which must all match to require this positional.
681    pub fn required_if_eq_all<I, S, V>(mut self, conditions: I) -> Self
682    where
683        I: IntoIterator<Item = (S, V)>,
684        S: Into<String>,
685        V: Into<String>,
686    {
687        self.inner
688            .required_if_eq_all
689            .extend(
690                conditions
691                    .into_iter()
692                    .map(|(selector, value)| SpecRequiredIfEq {
693                        selector: selector.into(),
694                        value: value.into(),
695                    }),
696            );
697        self
698    }
699
700    /// Add selectors where any presence waives this positional's requirement.
701    pub fn required_unless_any<I, S>(mut self, selectors: I) -> Self
702    where
703        I: IntoIterator<Item = S>,
704        S: Into<String>,
705    {
706        self.inner
707            .required_unless
708            .extend(selectors.into_iter().map(Into::into));
709        self
710    }
711
712    /// Add selectors which must all be present to waive this positional's requirement.
713    pub fn required_unless_all<I, S>(mut self, selectors: I) -> Self
714    where
715        I: IntoIterator<Item = S>,
716        S: Into<String>,
717    {
718        self.inner
719            .required_unless_all
720            .extend(selectors.into_iter().map(Into::into));
721        self
722    }
723
724    /// Set as hidden
725    pub fn hide(mut self, is_hidden: bool) -> Self {
726        self.inner.hide = is_hidden;
727        self
728    }
729
730    /// Set environment variable name
731    /// Heading to list this under in help output.
732    pub fn help_heading(mut self, help_heading: impl Into<String>) -> Self {
733        self.inner.help_heading = Some(help_heading.into());
734        self
735    }
736
737    pub fn env(mut self, env: impl Into<String>) -> Self {
738        self.inner.env = Some(env.into());
739        self
740    }
741
742    /// Add an environment fallback, consulted after the canonical variable.
743    pub fn env_fallback(mut self, env: impl Into<String>) -> Self {
744        self.inner.env_fallback.push(env.into());
745        self
746    }
747
748    /// Add a deprecated environment alias, consulted after ordinary fallbacks.
749    pub fn deprecated_env(mut self, env: impl Into<String>) -> Self {
750        self.inner.deprecated_env.push(env.into());
751        self
752    }
753
754    /// Set the double-dash behavior
755    pub fn double_dash(mut self, behavior: SpecDoubleDashChoices) -> Self {
756        self.inner.double_dash = behavior;
757        self
758    }
759
760    /// Set choices for this argument
761    pub fn choices<I, S>(mut self, choices: I) -> Self
762    where
763        I: IntoIterator<Item = S>,
764        S: Into<String>,
765    {
766        let spec_choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
767        #[cfg(feature = "unstable_choices_env")]
768        let env = spec_choices.env().map(ToString::to_string);
769        spec_choices.choices = choices.into_iter().map(Into::into).collect();
770        #[cfg(feature = "unstable_choices_env")]
771        spec_choices.set_env(env);
772        self
773    }
774
775    /// Set a portable expr expression that must accept each raw value.
776    pub fn validate(mut self, expression: impl Into<String>) -> Self {
777        self.inner.validate = Some(expression.into());
778        self
779    }
780
781    /// Set the message reported when validation returns false.
782    pub fn validate_error(mut self, message: impl Into<String>) -> Self {
783        self.inner.validate_error = Some(message.into());
784        self
785    }
786
787    /// Set choices from an environment variable
788    #[cfg(feature = "unstable_choices_env")]
789    pub fn choices_env(mut self, env: impl Into<String>) -> Self {
790        let choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
791        choices.set_env(Some(env.into()));
792        self
793    }
794
795    /// Set the rendered usage string. `build` derives this when unset.
796    pub fn usage(mut self, usage: impl Into<String>) -> Self {
797        self.inner.usage = usage.into();
798        self
799    }
800
801    /// Set the first line of help text. Derived from `help` when unset.
802    pub fn help_first_line(mut self, text: impl Into<String>) -> Self {
803        self.inner.help_first_line = Some(text.into());
804        self
805    }
806
807    /// Raise the command's effect when this argument is supplied.
808    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
809        self.inner.effect = Some(effect);
810        self
811    }
812
813    /// Build the final SpecArg
814    #[must_use]
815    pub fn build(mut self) -> SpecArg {
816        if self.inner.validate.is_none() {
817            self.inner.validate_error = None;
818        }
819        if self.inner.value_names.len() > 1 {
820            let arity = self.inner.value_names.len();
821            self.inner.var = true;
822            self.inner.var_min = Some(arity);
823            self.inner.var_max = Some(arity);
824        }
825        self.inner.usage = self.inner.usage();
826        self.inner
827    }
828}
829
830/// Builder for SpecCommand
831#[derive(Debug, Default, Clone)]
832pub struct SpecCommandBuilder {
833    inner: SpecCommand,
834}
835
836impl SpecCommandBuilder {
837    /// Label the audience or compatibility surface this command belongs to.
838    pub fn surface(mut self, surface: impl Into<String>) -> Self {
839        self.inner.surface = Some(surface.into());
840        self
841    }
842
843    /// Add a descriptive availability condition.
844    pub fn available_if(mut self, condition: impl Into<String>) -> Self {
845        self.inner.available_if.push(condition.into());
846        self
847    }
848    /// Create a new SpecCommandBuilder
849    pub fn new() -> Self {
850        Self::default()
851    }
852
853    /// Set the command name
854    pub fn name(mut self, name: impl Into<String>) -> Self {
855        self.inner.name = name.into();
856        self
857    }
858
859    /// Add an alias (can be called multiple times)
860    pub fn alias(mut self, alias: impl Into<String>) -> Self {
861        self.inner.aliases.push(alias.into());
862        self
863    }
864
865    /// Add multiple aliases at once
866    pub fn aliases<I, S>(mut self, aliases: I) -> Self
867    where
868        I: IntoIterator<Item = S>,
869        S: Into<String>,
870    {
871        self.inner
872            .aliases
873            .extend(aliases.into_iter().map(Into::into));
874        self
875    }
876
877    /// Add a hidden alias (can be called multiple times)
878    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
879        self.inner.hidden_aliases.push(alias.into());
880        self
881    }
882
883    /// Add multiple hidden aliases at once
884    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
885    where
886        I: IntoIterator<Item = S>,
887        S: Into<String>,
888    {
889        self.inner
890            .hidden_aliases
891            .extend(aliases.into_iter().map(Into::into));
892        self
893    }
894
895    /// Add a flag to the command
896    pub fn flag(mut self, flag: SpecFlag) -> Self {
897        self.inner.flags.push(flag);
898        self
899    }
900
901    /// Add multiple flags at once
902    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
903        self.inner.flags.extend(flags);
904        self
905    }
906
907    /// Add an argument to the command
908    pub fn arg(mut self, arg: SpecArg) -> Self {
909        self.inner.args.push(arg);
910        self
911    }
912
913    /// Add multiple arguments at once
914    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
915        self.inner.args.extend(args);
916        self
917    }
918
919    /// Set help text
920    pub fn help(mut self, text: impl Into<String>) -> Self {
921        self.inner.help = Some(text.into());
922        self
923    }
924
925    /// Set long help text
926    pub fn help_long(mut self, text: impl Into<String>) -> Self {
927        self.inner.help_long = Some(text.into());
928        self
929    }
930
931    /// Set markdown help text
932    pub fn help_md(mut self, text: impl Into<String>) -> Self {
933        self.inner.help_md = Some(text.into());
934        self
935    }
936
937    /// Set as hidden
938    pub fn hide(mut self, is_hidden: bool) -> Self {
939        self.inner.hide = is_hidden;
940        self
941    }
942
943    /// Set subcommand required
944    pub fn subcommand_required(mut self, required: bool) -> Self {
945        self.inner.subcommand_required = required;
946        self
947    }
948
949    /// Set the heading for this command's subcommand list.
950    pub fn subcommand_help_heading(mut self, heading: impl Into<String>) -> Self {
951        self.inner.subcommand_help_heading = Some(heading.into());
952        self
953    }
954
955    /// Set the synopsis placeholder for a subcommand.
956    pub fn subcommand_value_name(mut self, name: impl Into<String>) -> Self {
957        self.inner.subcommand_value_name = Some(name.into());
958        self
959    }
960
961    /// Set a fixed help width. Zero disables wrapping.
962    pub fn term_width(mut self, width: usize) -> Self {
963        self.inner.term_width = Some(width);
964        self
965    }
966
967    /// Cap detected terminal width when no fixed width is set. Zero disables the cap.
968    pub fn max_term_width(mut self, width: usize) -> Self {
969        self.inner.max_term_width = Some(width);
970        self
971    }
972
973    /// Forward an unmatched word as an external command plus the rest of argv
974    pub fn external_subcommand(mut self, enabled: bool) -> Self {
975        self.inner.external_subcommand = enabled;
976        self
977    }
978
979    /// Enable or disable the synthesized `--help` and `-h` flags.
980    pub fn disable_help_flag(mut self, disabled: bool) -> Self {
981        self.inner.disable_help_flag = disabled;
982        self
983    }
984
985    /// Enable or disable the synthesized `help` subcommand route.
986    pub fn disable_help_subcommand(mut self, disabled: bool) -> Self {
987        self.inner.disable_help_subcommand = disabled;
988        self
989    }
990
991    /// Enable or disable the synthesized `--version` and `-V` flags.
992    pub fn disable_version_flag(mut self, disabled: bool) -> Self {
993        self.inner.disable_version_flag = disabled;
994        self
995    }
996
997    /// Set whether a later scalar flag occurrence replaces an earlier one.
998    ///
999    /// Enabled by default. Set false to reject duplicate scalar flags.
1000    pub fn args_override_self(mut self, enabled: bool) -> Self {
1001        self.inner.args_override_self = enabled;
1002        self
1003    }
1004
1005    /// Set whether selecting a subcommand suppresses this command's requirements.
1006    pub fn subcommand_negates_reqs(mut self, enabled: bool) -> Self {
1007        self.inner.subcommand_negates_reqs = enabled;
1008        self
1009    }
1010
1011    /// Set whether arguments on this command exclude a later subcommand.
1012    pub fn args_conflicts_with_subcommands(mut self, enabled: bool) -> Self {
1013        self.inner.args_conflicts_with_subcommands = enabled;
1014        self
1015    }
1016
1017    pub fn subcommand_precedence_over_arg(mut self, enabled: bool) -> Self {
1018        self.inner.subcommand_precedence_over_arg = enabled;
1019        self
1020    }
1021
1022    pub fn allow_missing_positional(mut self, enabled: bool) -> Self {
1023        self.inner.allow_missing_positional = enabled;
1024        self
1025    }
1026
1027    /// Set what running this command does to the world
1028    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
1029        self.inner.effect = Some(effect);
1030        self
1031    }
1032
1033    /// Set deprecated message
1034    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
1035        self.inner.deprecated = Some(msg.into());
1036        self
1037    }
1038
1039    pub fn deprecated_warn_at(mut self, version: impl Into<String>) -> Self {
1040        self.inner.deprecated_warn_at = Some(version.into());
1041        self
1042    }
1043
1044    pub fn deprecated_remove_at(mut self, version: impl Into<String>) -> Self {
1045        self.inner.deprecated_remove_at = Some(version.into());
1046        self
1047    }
1048
1049    /// Set restart token for resetting argument parsing
1050    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1051    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
1052        self.inner.restart_token = Some(token.into());
1053        self
1054    }
1055
1056    /// Add a subcommand (can be called multiple times)
1057    pub fn subcommand(mut self, cmd: SpecCommand) -> Self {
1058        self.inner.subcommands.insert(cmd.name.clone(), cmd);
1059        self
1060    }
1061
1062    /// Add multiple subcommands at once
1063    pub fn subcommands(mut self, cmds: impl IntoIterator<Item = SpecCommand>) -> Self {
1064        for cmd in cmds {
1065            self.inner.subcommands.insert(cmd.name.clone(), cmd);
1066        }
1067        self
1068    }
1069
1070    /// Set before_help text (displayed before the help message)
1071    pub fn before_help(mut self, text: impl Into<String>) -> Self {
1072        self.inner.before_help = Some(text.into());
1073        self
1074    }
1075
1076    /// Set before_help_long text
1077    pub fn before_help_long(mut self, text: impl Into<String>) -> Self {
1078        self.inner.before_help_long = Some(text.into());
1079        self
1080    }
1081
1082    /// Set before_help markdown text
1083    pub fn before_help_md(mut self, text: impl Into<String>) -> Self {
1084        self.inner.before_help_md = Some(text.into());
1085        self
1086    }
1087
1088    /// Set after_help text (displayed after the help message)
1089    pub fn after_help(mut self, text: impl Into<String>) -> Self {
1090        self.inner.after_help = Some(text.into());
1091        self
1092    }
1093
1094    /// Set after_help_long text
1095    pub fn after_help_long(mut self, text: impl Into<String>) -> Self {
1096        self.inner.after_help_long = Some(text.into());
1097        self
1098    }
1099
1100    /// Set after_help markdown text
1101    pub fn after_help_md(mut self, text: impl Into<String>) -> Self {
1102        self.inner.after_help_md = Some(text.into());
1103        self
1104    }
1105
1106    /// Add an example (can be called multiple times)
1107    pub fn example(mut self, code: impl Into<String>) -> Self {
1108        self.inner.examples.push(SpecExample::new(code.into()));
1109        self
1110    }
1111
1112    /// Add an example with header and help text
1113    pub fn example_with_help(
1114        mut self,
1115        code: impl Into<String>,
1116        header: impl Into<String>,
1117        help: impl Into<String>,
1118    ) -> Self {
1119        let mut example = SpecExample::new(code.into());
1120        example.header = Some(header.into());
1121        example.help = Some(help.into());
1122        self.inner.examples.push(example);
1123        self
1124    }
1125
1126    /// Build the final SpecCommand
1127    #[must_use]
1128    pub fn build(mut self) -> SpecCommand {
1129        self.inner.usage = self.inner.usage();
1130        self.inner
1131    }
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137
1138    #[test]
1139    fn test_flag_builder_basic() {
1140        let flag = SpecFlagBuilder::new()
1141            .name("verbose")
1142            .short('v')
1143            .long("verbose")
1144            .help("Enable verbose output")
1145            .build();
1146
1147        assert_eq!(flag.name, "verbose");
1148        assert_eq!(flag.short, vec!['v']);
1149        assert_eq!(flag.long, vec!["verbose".to_string()]);
1150        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
1151    }
1152
1153    #[test]
1154    fn test_flag_builder_multiple_values() {
1155        let flag = SpecFlagBuilder::new()
1156            .shorts(['v', 'V'])
1157            .longs(["verbose", "loud"])
1158            .default_values(["info", "warn"])
1159            .build();
1160
1161        assert_eq!(flag.short, vec!['v', 'V']);
1162        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
1163        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
1164        assert!(!flag.required); // Should be false due to defaults
1165    }
1166
1167    #[test]
1168    fn test_flag_builder_variadic() {
1169        let flag = SpecFlagBuilder::new()
1170            .long("file")
1171            .var(true)
1172            .var_min(1)
1173            .var_max(10)
1174            .build();
1175
1176        assert!(flag.var);
1177        assert_eq!(flag.var_min, Some(1));
1178        assert_eq!(flag.var_max, Some(10));
1179    }
1180
1181    #[test]
1182    fn test_flag_builder_conditional_requirements() {
1183        let flag = SpecFlagBuilder::new()
1184            .long("config")
1185            .requires_if("special.toml", "--key")
1186            .requires_ifs([("remote.toml", "--token"), ("signed.toml", "--identity")])
1187            .build();
1188
1189        assert_eq!(
1190            flag.requires_if,
1191            [
1192                SpecRequiresIf {
1193                    value: "special.toml".into(),
1194                    requires: "--key".into(),
1195                },
1196                SpecRequiresIf {
1197                    value: "remote.toml".into(),
1198                    requires: "--token".into(),
1199                },
1200                SpecRequiresIf {
1201                    value: "signed.toml".into(),
1202                    requires: "--identity".into(),
1203                },
1204            ]
1205        );
1206    }
1207
1208    #[test]
1209    fn test_flag_builder_conditional_defaults() {
1210        let flag = SpecFlagBuilder::new()
1211            .long("bin-names")
1212            .default_if("--json", "true")
1213            .default_if_eq("--output", "json", "pretty")
1214            .build();
1215
1216        assert_eq!(
1217            flag.default_if,
1218            [
1219                SpecDefaultIf {
1220                    selector: "--json".into(),
1221                    when: None,
1222                    value: "true".into(),
1223                },
1224                SpecDefaultIf {
1225                    selector: "--output".into(),
1226                    when: Some("json".into()),
1227                    value: "pretty".into(),
1228                },
1229            ]
1230        );
1231    }
1232
1233    #[test]
1234    fn test_flag_builder_name_derivation() {
1235        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
1236
1237        // Name should be derived from long flag
1238        assert_eq!(flag.name, "verbose");
1239
1240        let flag2 = SpecFlagBuilder::new().short('v').build();
1241
1242        // Name should be derived from short flag if no long
1243        assert_eq!(flag2.name, "v");
1244    }
1245
1246    #[test]
1247    fn test_arg_builder_basic() {
1248        let arg = SpecArgBuilder::new()
1249            .name("file")
1250            .help("Input file")
1251            .required(true)
1252            .build();
1253
1254        assert_eq!(arg.name, "file");
1255        assert_eq!(arg.help, Some("Input file".to_string()));
1256        assert!(arg.required);
1257    }
1258
1259    #[test]
1260    fn test_arg_builder_variadic() {
1261        let arg = SpecArgBuilder::new()
1262            .name("files")
1263            .var(true)
1264            .var_min(1)
1265            .var_max(10)
1266            .help("Input files")
1267            .build();
1268
1269        assert_eq!(arg.name, "files");
1270        assert!(arg.var);
1271        assert_eq!(arg.var_min, Some(1));
1272        assert_eq!(arg.var_max, Some(10));
1273    }
1274
1275    #[test]
1276    fn test_arg_builder_defaults() {
1277        let arg = SpecArgBuilder::new()
1278            .name("file")
1279            .default_values(["a.txt", "b.txt"])
1280            .build();
1281
1282        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
1283        assert!(!arg.required);
1284    }
1285
1286    #[test]
1287    fn test_arg_builder_drops_validation_error_without_expression() {
1288        let arg = SpecArgBuilder::new()
1289            .name("port")
1290            .validate_error("must be a valid port")
1291            .build();
1292
1293        assert!(arg.validate_error.is_none());
1294    }
1295
1296    #[test]
1297    fn test_command_builder_basic() {
1298        let cmd = SpecCommandBuilder::new()
1299            .name("install")
1300            .help("Install packages")
1301            .build();
1302
1303        assert_eq!(cmd.name, "install");
1304        assert_eq!(cmd.help, Some("Install packages".to_string()));
1305    }
1306
1307    #[test]
1308    fn test_command_builder_aliases() {
1309        let cmd = SpecCommandBuilder::new()
1310            .name("install")
1311            .alias("i")
1312            .aliases(["add", "get"])
1313            .hidden_aliases(["inst"])
1314            .build();
1315
1316        assert_eq!(
1317            cmd.aliases,
1318            vec!["i".to_string(), "add".to_string(), "get".to_string()]
1319        );
1320        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
1321    }
1322
1323    #[test]
1324    fn test_command_builder_with_flags_and_args() {
1325        let flag = SpecFlagBuilder::new().short('f').long("force").build();
1326
1327        let arg = SpecArgBuilder::new().name("package").required(true).build();
1328
1329        let cmd = SpecCommandBuilder::new()
1330            .name("install")
1331            .flag(flag)
1332            .arg(arg)
1333            .build();
1334
1335        assert_eq!(cmd.flags.len(), 1);
1336        assert_eq!(cmd.flags[0].name, "force");
1337        assert_eq!(cmd.args.len(), 1);
1338        assert_eq!(cmd.args[0].name, "package");
1339    }
1340
1341    #[test]
1342    fn test_arg_builder_choices() {
1343        let arg = SpecArgBuilder::new()
1344            .name("format")
1345            .choices(["json", "yaml", "toml"])
1346            .build();
1347
1348        assert!(arg.choices.is_some());
1349        let choices = arg.choices.unwrap();
1350        assert_eq!(
1351            choices.choices,
1352            vec!["json".to_string(), "yaml".to_string(), "toml".to_string()]
1353        );
1354        assert_eq!(choices.env(), None);
1355    }
1356
1357    #[cfg(feature = "unstable_choices_env")]
1358    #[test]
1359    fn test_arg_builder_choices_env() {
1360        let arg = SpecArgBuilder::new()
1361            .name("env")
1362            .choices(["local"])
1363            .choices_env("DEPLOY_ENVS")
1364            .build();
1365
1366        let choices = arg.choices.unwrap();
1367        assert_eq!(choices.choices, vec!["local".to_string()]);
1368        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1369    }
1370
1371    #[cfg(feature = "unstable_choices_env")]
1372    #[test]
1373    fn test_arg_builder_choices_preserves_choices_env() {
1374        let arg = SpecArgBuilder::new()
1375            .name("env")
1376            .choices_env("DEPLOY_ENVS")
1377            .choices(["local"])
1378            .build();
1379
1380        let choices = arg.choices.unwrap();
1381        assert_eq!(choices.choices, vec!["local".to_string()]);
1382        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1383    }
1384
1385    #[test]
1386    fn test_command_builder_subcommands() {
1387        let sub1 = SpecCommandBuilder::new().name("sub1").build();
1388        let sub2 = SpecCommandBuilder::new().name("sub2").build();
1389
1390        let cmd = SpecCommandBuilder::new()
1391            .name("main")
1392            .subcommand(sub1)
1393            .subcommand(sub2)
1394            .build();
1395
1396        assert_eq!(cmd.subcommands.len(), 2);
1397        assert!(cmd.subcommands.contains_key("sub1"));
1398        assert!(cmd.subcommands.contains_key("sub2"));
1399    }
1400
1401    #[test]
1402    fn test_command_builder_before_after_help() {
1403        let cmd = SpecCommandBuilder::new()
1404            .name("test")
1405            .before_help("Before help text")
1406            .before_help_long("Before help long text")
1407            .after_help("After help text")
1408            .after_help_long("After help long text")
1409            .build();
1410
1411        assert_eq!(cmd.before_help, Some("Before help text".to_string()));
1412        assert_eq!(
1413            cmd.before_help_long,
1414            Some("Before help long text".to_string())
1415        );
1416        assert_eq!(cmd.after_help, Some("After help text".to_string()));
1417        assert_eq!(
1418            cmd.after_help_long,
1419            Some("After help long text".to_string())
1420        );
1421    }
1422
1423    #[test]
1424    fn test_command_builder_examples() {
1425        let cmd = SpecCommandBuilder::new()
1426            .name("test")
1427            .example("mycli run")
1428            .example_with_help("mycli build", "Build example", "Build the project")
1429            .build();
1430
1431        assert_eq!(cmd.examples.len(), 2);
1432        assert_eq!(cmd.examples[0].code, "mycli run");
1433        assert_eq!(cmd.examples[1].code, "mycli build");
1434        assert_eq!(cmd.examples[1].header, Some("Build example".to_string()));
1435        assert_eq!(cmd.examples[1].help, Some("Build the project".to_string()));
1436    }
1437}