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    SpecAdmonition, SpecArg, SpecChoices, SpecCommand, SpecComplete, SpecDefaultIf, SpecFlag,
37    SpecRequiredIfEq, SpecRequiresIf, spec::arg::SpecDoubleDashChoices,
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            && let Some(arg) = &mut self.inner.arg
312        {
313            arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
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            && let Some(arg) = &mut self.inner.arg
495        {
496            arg.double_dash = crate::spec::arg::SpecDoubleDashChoices::Automatic;
497        }
498        if self.inner.default_missing.is_some()
499            && let Some(arg) = &mut self.inner.arg
500        {
501            arg.required = false;
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    /// Take further choices from the lines a shell command prints (`choices run=`).
788    ///
789    /// The command runs when a value is checked, a word is completed, or a running program
790    /// shows its help; generated docs and code describe it without running it.
791    pub fn choices_run(mut self, run: impl Into<String>) -> Self {
792        let choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
793        choices.run = Some(run.into());
794        self
795    }
796
797    /// Set choices from an environment variable
798    #[cfg(feature = "unstable_choices_env")]
799    pub fn choices_env(mut self, env: impl Into<String>) -> Self {
800        let choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
801        choices.set_env(Some(env.into()));
802        self
803    }
804
805    /// Set the rendered usage string. `build` derives this when unset.
806    pub fn usage(mut self, usage: impl Into<String>) -> Self {
807        self.inner.usage = usage.into();
808        self
809    }
810
811    /// Set the first line of help text. Derived from `help` when unset.
812    pub fn help_first_line(mut self, text: impl Into<String>) -> Self {
813        self.inner.help_first_line = Some(text.into());
814        self
815    }
816
817    /// Raise the command's effect when this argument is supplied.
818    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
819        self.inner.effect = Some(effect);
820        self
821    }
822
823    /// Attach a completer to this argument itself, the builder form of a `complete` node
824    /// written inside `arg`. It wins over a `complete` found by name. `build` sets the
825    /// completer's name to the argument's, lowercased, replacing any name it was given.
826    pub fn complete(mut self, complete: SpecComplete) -> Self {
827        self.inner.complete = Some(complete);
828        self
829    }
830
831    /// Build the final SpecArg
832    #[must_use]
833    pub fn build(mut self) -> SpecArg {
834        if self.inner.validate.is_none() {
835            self.inner.validate_error = None;
836        }
837        // Always the arg's own name, lowercased, whatever the completer was built with: that is
838        // what parsing the KDL form gives it, so the two agree after a round trip.
839        if let Some(complete) = &mut self.inner.complete {
840            complete.name = self.inner.name.to_lowercase();
841        }
842        if self.inner.value_names.len() > 1 {
843            let arity = self.inner.value_names.len();
844            self.inner.var = true;
845            self.inner.var_min = Some(arity);
846            self.inner.var_max = Some(arity);
847        }
848        self.inner.usage = self.inner.usage();
849        self.inner
850    }
851}
852
853/// Builder for SpecCommand
854#[derive(Debug, Default, Clone)]
855pub struct SpecCommandBuilder {
856    inner: SpecCommand,
857}
858
859impl SpecCommandBuilder {
860    /// Label the audience or compatibility surface this command belongs to.
861    pub fn surface(mut self, surface: impl Into<String>) -> Self {
862        self.inner.surface = Some(surface.into());
863        self
864    }
865
866    /// Add a descriptive availability condition.
867    pub fn available_if(mut self, condition: impl Into<String>) -> Self {
868        self.inner.available_if.push(condition.into());
869        self
870    }
871    /// Create a new SpecCommandBuilder
872    pub fn new() -> Self {
873        Self::default()
874    }
875
876    /// Set the command name
877    pub fn name(mut self, name: impl Into<String>) -> Self {
878        self.inner.name = name.into();
879        self
880    }
881
882    /// Add an alias (can be called multiple times)
883    pub fn alias(mut self, alias: impl Into<String>) -> Self {
884        self.inner.aliases.push(alias.into());
885        self
886    }
887
888    /// Add multiple aliases at once
889    pub fn aliases<I, S>(mut self, aliases: I) -> Self
890    where
891        I: IntoIterator<Item = S>,
892        S: Into<String>,
893    {
894        self.inner
895            .aliases
896            .extend(aliases.into_iter().map(Into::into));
897        self
898    }
899
900    /// Add a hidden alias (can be called multiple times)
901    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
902        self.inner.hidden_aliases.push(alias.into());
903        self
904    }
905
906    /// Add multiple hidden aliases at once
907    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
908    where
909        I: IntoIterator<Item = S>,
910        S: Into<String>,
911    {
912        self.inner
913            .hidden_aliases
914            .extend(aliases.into_iter().map(Into::into));
915        self
916    }
917
918    /// Add a flag to the command
919    pub fn flag(mut self, flag: SpecFlag) -> Self {
920        self.inner.flags.push(flag);
921        self
922    }
923
924    /// Add multiple flags at once
925    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
926        self.inner.flags.extend(flags);
927        self
928    }
929
930    /// Add an argument to the command
931    pub fn arg(mut self, arg: SpecArg) -> Self {
932        self.inner.args.push(arg);
933        self
934    }
935
936    /// Add multiple arguments at once
937    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
938        self.inner.args.extend(args);
939        self
940    }
941
942    /// Set help text
943    pub fn help(mut self, text: impl Into<String>) -> Self {
944        self.inner.help = Some(text.into());
945        self
946    }
947
948    /// Set long help text
949    pub fn help_long(mut self, text: impl Into<String>) -> Self {
950        self.inner.help_long = Some(text.into());
951        self
952    }
953
954    /// Set markdown help text
955    pub fn help_md(mut self, text: impl Into<String>) -> Self {
956        self.inner.help_md = Some(text.into());
957        self
958    }
959
960    /// Set as hidden
961    pub fn hide(mut self, is_hidden: bool) -> Self {
962        self.inner.hide = is_hidden;
963        self
964    }
965
966    /// Set subcommand required
967    pub fn subcommand_required(mut self, required: bool) -> Self {
968        self.inner.subcommand_required = required;
969        self
970    }
971
972    /// Set the heading for this command's subcommand list.
973    pub fn subcommand_help_heading(mut self, heading: impl Into<String>) -> Self {
974        self.inner.subcommand_help_heading = Some(heading.into());
975        self
976    }
977
978    /// Set the synopsis placeholder for a subcommand.
979    pub fn subcommand_value_name(mut self, name: impl Into<String>) -> Self {
980        self.inner.subcommand_value_name = Some(name.into());
981        self
982    }
983
984    /// Set a fixed help width. Zero disables wrapping.
985    pub fn term_width(mut self, width: usize) -> Self {
986        self.inner.term_width = Some(width);
987        self
988    }
989
990    /// Cap detected terminal width when no fixed width is set. Zero disables the cap.
991    pub fn max_term_width(mut self, width: usize) -> Self {
992        self.inner.max_term_width = Some(width);
993        self
994    }
995
996    /// Forward an unmatched word as an external command plus the rest of argv
997    pub fn external_subcommand(mut self, enabled: bool) -> Self {
998        self.inner.external_subcommand = enabled;
999        self
1000    }
1001
1002    /// Enable or disable the synthesized `--help` and `-h` flags.
1003    pub fn disable_help_flag(mut self, disabled: bool) -> Self {
1004        self.inner.disable_help_flag = disabled;
1005        self
1006    }
1007
1008    /// Enable or disable the synthesized `help` subcommand route.
1009    pub fn disable_help_subcommand(mut self, disabled: bool) -> Self {
1010        self.inner.disable_help_subcommand = disabled;
1011        self
1012    }
1013
1014    /// Enable or disable the synthesized `--version` and `-V` flags.
1015    pub fn disable_version_flag(mut self, disabled: bool) -> Self {
1016        self.inner.disable_version_flag = disabled;
1017        self
1018    }
1019
1020    /// Set whether a later scalar flag occurrence replaces an earlier one.
1021    ///
1022    /// Enabled by default. Set false to reject duplicate scalar flags.
1023    pub fn args_override_self(mut self, enabled: bool) -> Self {
1024        self.inner.args_override_self = enabled;
1025        self
1026    }
1027
1028    /// Set whether selecting a subcommand suppresses this command's requirements.
1029    pub fn subcommand_negates_reqs(mut self, enabled: bool) -> Self {
1030        self.inner.subcommand_negates_reqs = enabled;
1031        self
1032    }
1033
1034    /// Set whether arguments on this command exclude a later subcommand.
1035    pub fn args_conflicts_with_subcommands(mut self, enabled: bool) -> Self {
1036        self.inner.args_conflicts_with_subcommands = enabled;
1037        self
1038    }
1039
1040    pub fn subcommand_precedence_over_arg(mut self, enabled: bool) -> Self {
1041        self.inner.subcommand_precedence_over_arg = enabled;
1042        self
1043    }
1044
1045    pub fn allow_missing_positional(mut self, enabled: bool) -> Self {
1046        self.inner.allow_missing_positional = enabled;
1047        self
1048    }
1049
1050    /// Set what running this command does to the world
1051    pub fn effect(mut self, effect: SpecCommandEffect) -> Self {
1052        self.inner.effect = Some(effect);
1053        self
1054    }
1055
1056    /// Set deprecated message
1057    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
1058        self.inner.deprecated = Some(msg.into());
1059        self
1060    }
1061
1062    pub fn deprecated_warn_at(mut self, version: impl Into<String>) -> Self {
1063        self.inner.deprecated_warn_at = Some(version.into());
1064        self
1065    }
1066
1067    pub fn deprecated_remove_at(mut self, version: impl Into<String>) -> Self {
1068        self.inner.deprecated_remove_at = Some(version.into());
1069        self
1070    }
1071
1072    /// Set restart token for resetting argument parsing
1073    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
1074    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
1075        self.inner.restart_token = Some(token.into());
1076        self
1077    }
1078
1079    /// Add a subcommand (can be called multiple times)
1080    pub fn subcommand(mut self, cmd: SpecCommand) -> Self {
1081        self.inner.subcommands.insert(cmd.name.clone(), cmd);
1082        self
1083    }
1084
1085    /// Add multiple subcommands at once
1086    pub fn subcommands(mut self, cmds: impl IntoIterator<Item = SpecCommand>) -> Self {
1087        for cmd in cmds {
1088            self.inner.subcommands.insert(cmd.name.clone(), cmd);
1089        }
1090        self
1091    }
1092
1093    /// Set before_help text (displayed before the help message)
1094    pub fn before_help(mut self, text: impl Into<String>) -> Self {
1095        self.inner.before_help = Some(text.into());
1096        self
1097    }
1098
1099    /// Set before_help_long text
1100    pub fn before_help_long(mut self, text: impl Into<String>) -> Self {
1101        self.inner.before_help_long = Some(text.into());
1102        self
1103    }
1104
1105    /// Set before_help markdown text
1106    pub fn before_help_md(mut self, text: impl Into<String>) -> Self {
1107        self.inner.before_help_md = Some(text.into());
1108        self
1109    }
1110
1111    /// Set after_help text (displayed after the help message)
1112    pub fn after_help(mut self, text: impl Into<String>) -> Self {
1113        self.inner.after_help = Some(text.into());
1114        self
1115    }
1116
1117    /// Set after_help_long text
1118    pub fn after_help_long(mut self, text: impl Into<String>) -> Self {
1119        self.inner.after_help_long = Some(text.into());
1120        self
1121    }
1122
1123    /// Set after_help markdown text
1124    pub fn after_help_md(mut self, text: impl Into<String>) -> Self {
1125        self.inner.after_help_md = Some(text.into());
1126        self
1127    }
1128
1129    /// Add an example (can be called multiple times)
1130    pub fn example(mut self, code: impl Into<String>) -> Self {
1131        self.inner.examples.push(SpecExample::new(code.into()));
1132        self
1133    }
1134
1135    /// Add an example with header and help text
1136    pub fn example_with_help(
1137        mut self,
1138        code: impl Into<String>,
1139        header: impl Into<String>,
1140        help: impl Into<String>,
1141    ) -> Self {
1142        let mut example = SpecExample::new(code.into());
1143        example.header = Some(header.into());
1144        example.help = Some(help.into());
1145        self.inner.examples.push(example);
1146        self
1147    }
1148
1149    /// Build the final SpecCommand
1150    #[must_use]
1151    pub fn build(mut self) -> SpecCommand {
1152        self.inner.usage = self.inner.usage();
1153        self.inner
1154    }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160
1161    #[test]
1162    fn test_flag_builder_basic() {
1163        let flag = SpecFlagBuilder::new()
1164            .name("verbose")
1165            .short('v')
1166            .long("verbose")
1167            .help("Enable verbose output")
1168            .build();
1169
1170        assert_eq!(flag.name, "verbose");
1171        assert_eq!(flag.short, vec!['v']);
1172        assert_eq!(flag.long, vec!["verbose".to_string()]);
1173        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
1174    }
1175
1176    #[test]
1177    fn test_flag_builder_multiple_values() {
1178        let flag = SpecFlagBuilder::new()
1179            .shorts(['v', 'V'])
1180            .longs(["verbose", "loud"])
1181            .default_values(["info", "warn"])
1182            .build();
1183
1184        assert_eq!(flag.short, vec!['v', 'V']);
1185        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
1186        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
1187        assert!(!flag.required); // Should be false due to defaults
1188    }
1189
1190    #[test]
1191    fn test_flag_builder_variadic() {
1192        let flag = SpecFlagBuilder::new()
1193            .long("file")
1194            .var(true)
1195            .var_min(1)
1196            .var_max(10)
1197            .build();
1198
1199        assert!(flag.var);
1200        assert_eq!(flag.var_min, Some(1));
1201        assert_eq!(flag.var_max, Some(10));
1202    }
1203
1204    #[test]
1205    fn test_flag_builder_conditional_requirements() {
1206        let flag = SpecFlagBuilder::new()
1207            .long("config")
1208            .requires_if("special.toml", "--key")
1209            .requires_ifs([("remote.toml", "--token"), ("signed.toml", "--identity")])
1210            .build();
1211
1212        assert_eq!(
1213            flag.requires_if,
1214            [
1215                SpecRequiresIf {
1216                    value: "special.toml".into(),
1217                    requires: "--key".into(),
1218                },
1219                SpecRequiresIf {
1220                    value: "remote.toml".into(),
1221                    requires: "--token".into(),
1222                },
1223                SpecRequiresIf {
1224                    value: "signed.toml".into(),
1225                    requires: "--identity".into(),
1226                },
1227            ]
1228        );
1229    }
1230
1231    #[test]
1232    fn test_flag_builder_conditional_defaults() {
1233        let flag = SpecFlagBuilder::new()
1234            .long("bin-names")
1235            .default_if("--json", "true")
1236            .default_if_eq("--output", "json", "pretty")
1237            .build();
1238
1239        assert_eq!(
1240            flag.default_if,
1241            [
1242                SpecDefaultIf {
1243                    selector: "--json".into(),
1244                    when: None,
1245                    value: "true".into(),
1246                },
1247                SpecDefaultIf {
1248                    selector: "--output".into(),
1249                    when: Some("json".into()),
1250                    value: "pretty".into(),
1251                },
1252            ]
1253        );
1254    }
1255
1256    #[test]
1257    fn test_flag_builder_name_derivation() {
1258        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
1259
1260        // Name should be derived from long flag
1261        assert_eq!(flag.name, "verbose");
1262
1263        let flag2 = SpecFlagBuilder::new().short('v').build();
1264
1265        // Name should be derived from short flag if no long
1266        assert_eq!(flag2.name, "v");
1267    }
1268
1269    #[test]
1270    fn test_arg_builder_basic() {
1271        let arg = SpecArgBuilder::new()
1272            .name("file")
1273            .help("Input file")
1274            .required(true)
1275            .build();
1276
1277        assert_eq!(arg.name, "file");
1278        assert_eq!(arg.help, Some("Input file".to_string()));
1279        assert!(arg.required);
1280    }
1281
1282    #[test]
1283    fn test_arg_builder_variadic() {
1284        let arg = SpecArgBuilder::new()
1285            .name("files")
1286            .var(true)
1287            .var_min(1)
1288            .var_max(10)
1289            .help("Input files")
1290            .build();
1291
1292        assert_eq!(arg.name, "files");
1293        assert!(arg.var);
1294        assert_eq!(arg.var_min, Some(1));
1295        assert_eq!(arg.var_max, Some(10));
1296    }
1297
1298    #[test]
1299    fn test_arg_builder_defaults() {
1300        let arg = SpecArgBuilder::new()
1301            .name("file")
1302            .default_values(["a.txt", "b.txt"])
1303            .build();
1304
1305        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
1306        assert!(!arg.required);
1307    }
1308
1309    #[test]
1310    fn test_arg_builder_drops_validation_error_without_expression() {
1311        let arg = SpecArgBuilder::new()
1312            .name("port")
1313            .validate_error("must be a valid port")
1314            .build();
1315
1316        assert!(arg.validate_error.is_none());
1317    }
1318
1319    #[test]
1320    fn test_command_builder_basic() {
1321        let cmd = SpecCommandBuilder::new()
1322            .name("install")
1323            .help("Install packages")
1324            .build();
1325
1326        assert_eq!(cmd.name, "install");
1327        assert_eq!(cmd.help, Some("Install packages".to_string()));
1328    }
1329
1330    #[test]
1331    fn test_command_builder_aliases() {
1332        let cmd = SpecCommandBuilder::new()
1333            .name("install")
1334            .alias("i")
1335            .aliases(["add", "get"])
1336            .hidden_aliases(["inst"])
1337            .build();
1338
1339        assert_eq!(
1340            cmd.aliases,
1341            vec!["i".to_string(), "add".to_string(), "get".to_string()]
1342        );
1343        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
1344    }
1345
1346    #[test]
1347    fn test_command_builder_with_flags_and_args() {
1348        let flag = SpecFlagBuilder::new().short('f').long("force").build();
1349
1350        let arg = SpecArgBuilder::new().name("package").required(true).build();
1351
1352        let cmd = SpecCommandBuilder::new()
1353            .name("install")
1354            .flag(flag)
1355            .arg(arg)
1356            .build();
1357
1358        assert_eq!(cmd.flags.len(), 1);
1359        assert_eq!(cmd.flags[0].name, "force");
1360        assert_eq!(cmd.args.len(), 1);
1361        assert_eq!(cmd.args[0].name, "package");
1362    }
1363
1364    #[test]
1365    fn test_arg_builder_choices() {
1366        let arg = SpecArgBuilder::new()
1367            .name("format")
1368            .choices(["json", "yaml", "toml"])
1369            .build();
1370
1371        assert!(arg.choices.is_some());
1372        let choices = arg.choices.unwrap();
1373        assert_eq!(
1374            choices.choices,
1375            vec!["json".to_string(), "yaml".to_string(), "toml".to_string()]
1376        );
1377        assert_eq!(choices.env(), None);
1378    }
1379
1380    #[test]
1381    fn test_arg_builder_choices_run_keeps_the_declared_values() {
1382        let arg = SpecArgBuilder::new()
1383            .name("service")
1384            .choices_run("printf 'app\\ndatabase\\n'")
1385            .choices(["local"])
1386            .build();
1387        let choices = arg.choices.unwrap();
1388        assert_eq!(choices.choices, vec!["local".to_string()]);
1389        assert_eq!(choices.run(), Some("printf 'app\\ndatabase\\n'"));
1390    }
1391
1392    #[cfg(feature = "unstable_choices_env")]
1393    #[test]
1394    fn test_arg_builder_choices_env() {
1395        let arg = SpecArgBuilder::new()
1396            .name("env")
1397            .choices(["local"])
1398            .choices_env("DEPLOY_ENVS")
1399            .build();
1400
1401        let choices = arg.choices.unwrap();
1402        assert_eq!(choices.choices, vec!["local".to_string()]);
1403        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1404    }
1405
1406    #[cfg(feature = "unstable_choices_env")]
1407    #[test]
1408    fn test_arg_builder_choices_preserves_choices_env() {
1409        let arg = SpecArgBuilder::new()
1410            .name("env")
1411            .choices_env("DEPLOY_ENVS")
1412            .choices(["local"])
1413            .build();
1414
1415        let choices = arg.choices.unwrap();
1416        assert_eq!(choices.choices, vec!["local".to_string()]);
1417        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
1418    }
1419
1420    #[test]
1421    fn test_command_builder_subcommands() {
1422        let sub1 = SpecCommandBuilder::new().name("sub1").build();
1423        let sub2 = SpecCommandBuilder::new().name("sub2").build();
1424
1425        let cmd = SpecCommandBuilder::new()
1426            .name("main")
1427            .subcommand(sub1)
1428            .subcommand(sub2)
1429            .build();
1430
1431        assert_eq!(cmd.subcommands.len(), 2);
1432        assert!(cmd.subcommands.contains_key("sub1"));
1433        assert!(cmd.subcommands.contains_key("sub2"));
1434    }
1435
1436    #[test]
1437    fn test_command_builder_before_after_help() {
1438        let cmd = SpecCommandBuilder::new()
1439            .name("test")
1440            .before_help("Before help text")
1441            .before_help_long("Before help long text")
1442            .after_help("After help text")
1443            .after_help_long("After help long text")
1444            .build();
1445
1446        assert_eq!(cmd.before_help, Some("Before help text".to_string()));
1447        assert_eq!(
1448            cmd.before_help_long,
1449            Some("Before help long text".to_string())
1450        );
1451        assert_eq!(cmd.after_help, Some("After help text".to_string()));
1452        assert_eq!(
1453            cmd.after_help_long,
1454            Some("After help long text".to_string())
1455        );
1456    }
1457
1458    #[test]
1459    fn test_command_builder_examples() {
1460        let cmd = SpecCommandBuilder::new()
1461            .name("test")
1462            .example("mycli run")
1463            .example_with_help("mycli build", "Build example", "Build the project")
1464            .build();
1465
1466        assert_eq!(cmd.examples.len(), 2);
1467        assert_eq!(cmd.examples[0].code, "mycli run");
1468        assert_eq!(cmd.examples[1].code, "mycli build");
1469        assert_eq!(cmd.examples[1].header, Some("Build example".to_string()));
1470        assert_eq!(cmd.examples[1].help, Some("Build the project".to_string()));
1471    }
1472}