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::arg::SpecDoubleDashChoices, SpecArg, SpecChoices, SpecCommand, SpecFlag};
35
36/// Builder for SpecFlag
37#[derive(Debug, Default, Clone)]
38pub struct SpecFlagBuilder {
39    inner: SpecFlag,
40}
41
42impl SpecFlagBuilder {
43    /// Create a new SpecFlagBuilder
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Set the flag name
49    pub fn name(mut self, name: impl Into<String>) -> Self {
50        self.inner.name = name.into();
51        self
52    }
53
54    /// Add a short flag character (can be called multiple times)
55    pub fn short(mut self, c: char) -> Self {
56        self.inner.short.push(c);
57        self
58    }
59
60    /// Add multiple short flags at once
61    pub fn shorts(mut self, chars: impl IntoIterator<Item = char>) -> Self {
62        self.inner.short.extend(chars);
63        self
64    }
65
66    /// Add a long flag name (can be called multiple times)
67    pub fn long(mut self, name: impl Into<String>) -> Self {
68        self.inner.long.push(name.into());
69        self
70    }
71
72    /// Add multiple long flags at once
73    pub fn longs<I, S>(mut self, names: I) -> Self
74    where
75        I: IntoIterator<Item = S>,
76        S: Into<String>,
77    {
78        self.inner.long.extend(names.into_iter().map(Into::into));
79        self
80    }
81
82    /// Add a default value (can be called multiple times for var flags)
83    pub fn default_value(mut self, value: impl Into<String>) -> Self {
84        self.inner.default.push(value.into());
85        self.inner.required = false;
86        self
87    }
88
89    /// Add multiple default values at once
90    pub fn default_values<I, S>(mut self, values: I) -> Self
91    where
92        I: IntoIterator<Item = S>,
93        S: Into<String>,
94    {
95        self.inner
96            .default
97            .extend(values.into_iter().map(Into::into));
98        if !self.inner.default.is_empty() {
99            self.inner.required = false;
100        }
101        self
102    }
103
104    /// Set help text
105    pub fn help(mut self, text: impl Into<String>) -> Self {
106        self.inner.help = Some(text.into());
107        self
108    }
109
110    /// Set long help text
111    pub fn help_long(mut self, text: impl Into<String>) -> Self {
112        self.inner.help_long = Some(text.into());
113        self
114    }
115
116    /// Set markdown help text
117    pub fn help_md(mut self, text: impl Into<String>) -> Self {
118        self.inner.help_md = Some(text.into());
119        self
120    }
121
122    /// Set as variadic (can be specified multiple times)
123    pub fn var(mut self, is_var: bool) -> Self {
124        self.inner.var = is_var;
125        self
126    }
127
128    /// Set minimum count for variadic flag
129    pub fn var_min(mut self, min: usize) -> Self {
130        self.inner.var_min = Some(min);
131        self
132    }
133
134    /// Set maximum count for variadic flag
135    pub fn var_max(mut self, max: usize) -> Self {
136        self.inner.var_max = Some(max);
137        self
138    }
139
140    /// Set as required
141    pub fn required(mut self, is_required: bool) -> Self {
142        self.inner.required = is_required;
143        self
144    }
145
146    /// Set as global (available to subcommands)
147    pub fn global(mut self, is_global: bool) -> Self {
148        self.inner.global = is_global;
149        self
150    }
151
152    /// Set as hidden
153    pub fn hide(mut self, is_hidden: bool) -> Self {
154        self.inner.hide = is_hidden;
155        self
156    }
157
158    /// Set as count flag
159    pub fn count(mut self, is_count: bool) -> Self {
160        self.inner.count = is_count;
161        self
162    }
163
164    /// Set the argument spec for flags that take values
165    pub fn arg(mut self, arg: SpecArg) -> Self {
166        self.inner.arg = Some(arg);
167        self
168    }
169
170    /// Set negate string
171    pub fn negate(mut self, negate: impl Into<String>) -> Self {
172        self.inner.negate = Some(negate.into());
173        self
174    }
175
176    /// Set environment variable name
177    pub fn env(mut self, env: impl Into<String>) -> Self {
178        self.inner.env = Some(env.into());
179        self
180    }
181
182    /// Set deprecated message
183    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
184        self.inner.deprecated = Some(msg.into());
185        self
186    }
187
188    /// Build the final SpecFlag
189    #[must_use]
190    pub fn build(mut self) -> SpecFlag {
191        self.inner.usage = self.inner.usage();
192        if self.inner.name.is_empty() {
193            // Derive name from long or short flags
194            if let Some(long) = self.inner.long.first() {
195                self.inner.name = long.clone();
196            } else if let Some(short) = self.inner.short.first() {
197                self.inner.name = short.to_string();
198            }
199        }
200        self.inner
201    }
202}
203
204/// Builder for SpecArg
205#[derive(Debug, Default, Clone)]
206pub struct SpecArgBuilder {
207    inner: SpecArg,
208}
209
210impl SpecArgBuilder {
211    /// Create a new SpecArgBuilder
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    /// Set the argument name
217    pub fn name(mut self, name: impl Into<String>) -> Self {
218        self.inner.name = name.into();
219        self
220    }
221
222    /// Add a default value (can be called multiple times for var args)
223    pub fn default_value(mut self, value: impl Into<String>) -> Self {
224        self.inner.default.push(value.into());
225        self.inner.required = false;
226        self
227    }
228
229    /// Add multiple default values at once
230    pub fn default_values<I, S>(mut self, values: I) -> Self
231    where
232        I: IntoIterator<Item = S>,
233        S: Into<String>,
234    {
235        self.inner
236            .default
237            .extend(values.into_iter().map(Into::into));
238        if !self.inner.default.is_empty() {
239            self.inner.required = false;
240        }
241        self
242    }
243
244    /// Set help text
245    pub fn help(mut self, text: impl Into<String>) -> Self {
246        self.inner.help = Some(text.into());
247        self
248    }
249
250    /// Set long help text
251    pub fn help_long(mut self, text: impl Into<String>) -> Self {
252        self.inner.help_long = Some(text.into());
253        self
254    }
255
256    /// Set markdown help text
257    pub fn help_md(mut self, text: impl Into<String>) -> Self {
258        self.inner.help_md = Some(text.into());
259        self
260    }
261
262    /// Set as variadic (accepts multiple values)
263    pub fn var(mut self, is_var: bool) -> Self {
264        self.inner.var = is_var;
265        self
266    }
267
268    /// Set minimum count for variadic argument
269    pub fn var_min(mut self, min: usize) -> Self {
270        self.inner.var_min = Some(min);
271        self
272    }
273
274    /// Set maximum count for variadic argument
275    pub fn var_max(mut self, max: usize) -> Self {
276        self.inner.var_max = Some(max);
277        self
278    }
279
280    /// Set as required
281    pub fn required(mut self, is_required: bool) -> Self {
282        self.inner.required = is_required;
283        self
284    }
285
286    /// Set as hidden
287    pub fn hide(mut self, is_hidden: bool) -> Self {
288        self.inner.hide = is_hidden;
289        self
290    }
291
292    /// Set environment variable name
293    pub fn env(mut self, env: impl Into<String>) -> Self {
294        self.inner.env = Some(env.into());
295        self
296    }
297
298    /// Set the double-dash behavior
299    pub fn double_dash(mut self, behavior: SpecDoubleDashChoices) -> Self {
300        self.inner.double_dash = behavior;
301        self
302    }
303
304    /// Set choices for this argument
305    pub fn choices<I, S>(mut self, choices: I) -> Self
306    where
307        I: IntoIterator<Item = S>,
308        S: Into<String>,
309    {
310        let spec_choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
311        #[cfg(feature = "unstable_choices_env")]
312        let env = spec_choices.env().map(ToString::to_string);
313        spec_choices.choices = choices.into_iter().map(Into::into).collect();
314        #[cfg(feature = "unstable_choices_env")]
315        spec_choices.set_env(env);
316        self
317    }
318
319    /// Set choices from an environment variable
320    #[cfg(feature = "unstable_choices_env")]
321    pub fn choices_env(mut self, env: impl Into<String>) -> Self {
322        let choices = self.inner.choices.get_or_insert_with(SpecChoices::default);
323        choices.set_env(Some(env.into()));
324        self
325    }
326
327    /// Build the final SpecArg
328    #[must_use]
329    pub fn build(mut self) -> SpecArg {
330        self.inner.usage = self.inner.usage();
331        self.inner
332    }
333}
334
335/// Builder for SpecCommand
336#[derive(Debug, Default, Clone)]
337pub struct SpecCommandBuilder {
338    inner: SpecCommand,
339}
340
341impl SpecCommandBuilder {
342    /// Create a new SpecCommandBuilder
343    pub fn new() -> Self {
344        Self::default()
345    }
346
347    /// Set the command name
348    pub fn name(mut self, name: impl Into<String>) -> Self {
349        self.inner.name = name.into();
350        self
351    }
352
353    /// Add an alias (can be called multiple times)
354    pub fn alias(mut self, alias: impl Into<String>) -> Self {
355        self.inner.aliases.push(alias.into());
356        self
357    }
358
359    /// Add multiple aliases at once
360    pub fn aliases<I, S>(mut self, aliases: I) -> Self
361    where
362        I: IntoIterator<Item = S>,
363        S: Into<String>,
364    {
365        self.inner
366            .aliases
367            .extend(aliases.into_iter().map(Into::into));
368        self
369    }
370
371    /// Add a hidden alias (can be called multiple times)
372    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
373        self.inner.hidden_aliases.push(alias.into());
374        self
375    }
376
377    /// Add multiple hidden aliases at once
378    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
379    where
380        I: IntoIterator<Item = S>,
381        S: Into<String>,
382    {
383        self.inner
384            .hidden_aliases
385            .extend(aliases.into_iter().map(Into::into));
386        self
387    }
388
389    /// Add a flag to the command
390    pub fn flag(mut self, flag: SpecFlag) -> Self {
391        self.inner.flags.push(flag);
392        self
393    }
394
395    /// Add multiple flags at once
396    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
397        self.inner.flags.extend(flags);
398        self
399    }
400
401    /// Add an argument to the command
402    pub fn arg(mut self, arg: SpecArg) -> Self {
403        self.inner.args.push(arg);
404        self
405    }
406
407    /// Add multiple arguments at once
408    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
409        self.inner.args.extend(args);
410        self
411    }
412
413    /// Set help text
414    pub fn help(mut self, text: impl Into<String>) -> Self {
415        self.inner.help = Some(text.into());
416        self
417    }
418
419    /// Set long help text
420    pub fn help_long(mut self, text: impl Into<String>) -> Self {
421        self.inner.help_long = Some(text.into());
422        self
423    }
424
425    /// Set markdown help text
426    pub fn help_md(mut self, text: impl Into<String>) -> Self {
427        self.inner.help_md = Some(text.into());
428        self
429    }
430
431    /// Set as hidden
432    pub fn hide(mut self, is_hidden: bool) -> Self {
433        self.inner.hide = is_hidden;
434        self
435    }
436
437    /// Set subcommand required
438    pub fn subcommand_required(mut self, required: bool) -> Self {
439        self.inner.subcommand_required = required;
440        self
441    }
442
443    /// Set deprecated message
444    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
445        self.inner.deprecated = Some(msg.into());
446        self
447    }
448
449    /// Set restart token for resetting argument parsing
450    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
451    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
452        self.inner.restart_token = Some(token.into());
453        self
454    }
455
456    /// Add a subcommand (can be called multiple times)
457    pub fn subcommand(mut self, cmd: SpecCommand) -> Self {
458        self.inner.subcommands.insert(cmd.name.clone(), cmd);
459        self
460    }
461
462    /// Add multiple subcommands at once
463    pub fn subcommands(mut self, cmds: impl IntoIterator<Item = SpecCommand>) -> Self {
464        for cmd in cmds {
465            self.inner.subcommands.insert(cmd.name.clone(), cmd);
466        }
467        self
468    }
469
470    /// Set before_help text (displayed before the help message)
471    pub fn before_help(mut self, text: impl Into<String>) -> Self {
472        self.inner.before_help = Some(text.into());
473        self
474    }
475
476    /// Set before_help_long text
477    pub fn before_help_long(mut self, text: impl Into<String>) -> Self {
478        self.inner.before_help_long = Some(text.into());
479        self
480    }
481
482    /// Set before_help markdown text
483    pub fn before_help_md(mut self, text: impl Into<String>) -> Self {
484        self.inner.before_help_md = Some(text.into());
485        self
486    }
487
488    /// Set after_help text (displayed after the help message)
489    pub fn after_help(mut self, text: impl Into<String>) -> Self {
490        self.inner.after_help = Some(text.into());
491        self
492    }
493
494    /// Set after_help_long text
495    pub fn after_help_long(mut self, text: impl Into<String>) -> Self {
496        self.inner.after_help_long = Some(text.into());
497        self
498    }
499
500    /// Set after_help markdown text
501    pub fn after_help_md(mut self, text: impl Into<String>) -> Self {
502        self.inner.after_help_md = Some(text.into());
503        self
504    }
505
506    /// Add an example (can be called multiple times)
507    pub fn example(mut self, code: impl Into<String>) -> Self {
508        self.inner.examples.push(SpecExample::new(code.into()));
509        self
510    }
511
512    /// Add an example with header and help text
513    pub fn example_with_help(
514        mut self,
515        code: impl Into<String>,
516        header: impl Into<String>,
517        help: impl Into<String>,
518    ) -> Self {
519        let mut example = SpecExample::new(code.into());
520        example.header = Some(header.into());
521        example.help = Some(help.into());
522        self.inner.examples.push(example);
523        self
524    }
525
526    /// Build the final SpecCommand
527    #[must_use]
528    pub fn build(mut self) -> SpecCommand {
529        self.inner.usage = self.inner.usage();
530        self.inner
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_flag_builder_basic() {
540        let flag = SpecFlagBuilder::new()
541            .name("verbose")
542            .short('v')
543            .long("verbose")
544            .help("Enable verbose output")
545            .build();
546
547        assert_eq!(flag.name, "verbose");
548        assert_eq!(flag.short, vec!['v']);
549        assert_eq!(flag.long, vec!["verbose".to_string()]);
550        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
551    }
552
553    #[test]
554    fn test_flag_builder_multiple_values() {
555        let flag = SpecFlagBuilder::new()
556            .shorts(['v', 'V'])
557            .longs(["verbose", "loud"])
558            .default_values(["info", "warn"])
559            .build();
560
561        assert_eq!(flag.short, vec!['v', 'V']);
562        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
563        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
564        assert!(!flag.required); // Should be false due to defaults
565    }
566
567    #[test]
568    fn test_flag_builder_variadic() {
569        let flag = SpecFlagBuilder::new()
570            .long("file")
571            .var(true)
572            .var_min(1)
573            .var_max(10)
574            .build();
575
576        assert!(flag.var);
577        assert_eq!(flag.var_min, Some(1));
578        assert_eq!(flag.var_max, Some(10));
579    }
580
581    #[test]
582    fn test_flag_builder_name_derivation() {
583        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
584
585        // Name should be derived from long flag
586        assert_eq!(flag.name, "verbose");
587
588        let flag2 = SpecFlagBuilder::new().short('v').build();
589
590        // Name should be derived from short flag if no long
591        assert_eq!(flag2.name, "v");
592    }
593
594    #[test]
595    fn test_arg_builder_basic() {
596        let arg = SpecArgBuilder::new()
597            .name("file")
598            .help("Input file")
599            .required(true)
600            .build();
601
602        assert_eq!(arg.name, "file");
603        assert_eq!(arg.help, Some("Input file".to_string()));
604        assert!(arg.required);
605    }
606
607    #[test]
608    fn test_arg_builder_variadic() {
609        let arg = SpecArgBuilder::new()
610            .name("files")
611            .var(true)
612            .var_min(1)
613            .var_max(10)
614            .help("Input files")
615            .build();
616
617        assert_eq!(arg.name, "files");
618        assert!(arg.var);
619        assert_eq!(arg.var_min, Some(1));
620        assert_eq!(arg.var_max, Some(10));
621    }
622
623    #[test]
624    fn test_arg_builder_defaults() {
625        let arg = SpecArgBuilder::new()
626            .name("file")
627            .default_values(["a.txt", "b.txt"])
628            .build();
629
630        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
631        assert!(!arg.required);
632    }
633
634    #[test]
635    fn test_command_builder_basic() {
636        let cmd = SpecCommandBuilder::new()
637            .name("install")
638            .help("Install packages")
639            .build();
640
641        assert_eq!(cmd.name, "install");
642        assert_eq!(cmd.help, Some("Install packages".to_string()));
643    }
644
645    #[test]
646    fn test_command_builder_aliases() {
647        let cmd = SpecCommandBuilder::new()
648            .name("install")
649            .alias("i")
650            .aliases(["add", "get"])
651            .hidden_aliases(["inst"])
652            .build();
653
654        assert_eq!(
655            cmd.aliases,
656            vec!["i".to_string(), "add".to_string(), "get".to_string()]
657        );
658        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
659    }
660
661    #[test]
662    fn test_command_builder_with_flags_and_args() {
663        let flag = SpecFlagBuilder::new().short('f').long("force").build();
664
665        let arg = SpecArgBuilder::new().name("package").required(true).build();
666
667        let cmd = SpecCommandBuilder::new()
668            .name("install")
669            .flag(flag)
670            .arg(arg)
671            .build();
672
673        assert_eq!(cmd.flags.len(), 1);
674        assert_eq!(cmd.flags[0].name, "force");
675        assert_eq!(cmd.args.len(), 1);
676        assert_eq!(cmd.args[0].name, "package");
677    }
678
679    #[test]
680    fn test_arg_builder_choices() {
681        let arg = SpecArgBuilder::new()
682            .name("format")
683            .choices(["json", "yaml", "toml"])
684            .build();
685
686        assert!(arg.choices.is_some());
687        let choices = arg.choices.unwrap();
688        assert_eq!(
689            choices.choices,
690            vec!["json".to_string(), "yaml".to_string(), "toml".to_string()]
691        );
692        assert_eq!(choices.env(), None);
693    }
694
695    #[cfg(feature = "unstable_choices_env")]
696    #[test]
697    fn test_arg_builder_choices_env() {
698        let arg = SpecArgBuilder::new()
699            .name("env")
700            .choices(["local"])
701            .choices_env("DEPLOY_ENVS")
702            .build();
703
704        let choices = arg.choices.unwrap();
705        assert_eq!(choices.choices, vec!["local".to_string()]);
706        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
707    }
708
709    #[cfg(feature = "unstable_choices_env")]
710    #[test]
711    fn test_arg_builder_choices_preserves_choices_env() {
712        let arg = SpecArgBuilder::new()
713            .name("env")
714            .choices_env("DEPLOY_ENVS")
715            .choices(["local"])
716            .build();
717
718        let choices = arg.choices.unwrap();
719        assert_eq!(choices.choices, vec!["local".to_string()]);
720        assert_eq!(choices.env(), Some("DEPLOY_ENVS"));
721    }
722
723    #[test]
724    fn test_command_builder_subcommands() {
725        let sub1 = SpecCommandBuilder::new().name("sub1").build();
726        let sub2 = SpecCommandBuilder::new().name("sub2").build();
727
728        let cmd = SpecCommandBuilder::new()
729            .name("main")
730            .subcommand(sub1)
731            .subcommand(sub2)
732            .build();
733
734        assert_eq!(cmd.subcommands.len(), 2);
735        assert!(cmd.subcommands.contains_key("sub1"));
736        assert!(cmd.subcommands.contains_key("sub2"));
737    }
738
739    #[test]
740    fn test_command_builder_before_after_help() {
741        let cmd = SpecCommandBuilder::new()
742            .name("test")
743            .before_help("Before help text")
744            .before_help_long("Before help long text")
745            .after_help("After help text")
746            .after_help_long("After help long text")
747            .build();
748
749        assert_eq!(cmd.before_help, Some("Before help text".to_string()));
750        assert_eq!(
751            cmd.before_help_long,
752            Some("Before help long text".to_string())
753        );
754        assert_eq!(cmd.after_help, Some("After help text".to_string()));
755        assert_eq!(
756            cmd.after_help_long,
757            Some("After help long text".to_string())
758        );
759    }
760
761    #[test]
762    fn test_command_builder_examples() {
763        let cmd = SpecCommandBuilder::new()
764            .name("test")
765            .example("mycli run")
766            .example_with_help("mycli build", "Build example", "Build the project")
767            .build();
768
769        assert_eq!(cmd.examples.len(), 2);
770        assert_eq!(cmd.examples[0].code, "mycli run");
771        assert_eq!(cmd.examples[1].code, "mycli build");
772        assert_eq!(cmd.examples[1].header, Some("Build example".to_string()));
773        assert_eq!(cmd.examples[1].help, Some("Build the project".to_string()));
774    }
775}