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        self.inner.choices = Some(SpecChoices {
311            choices: choices.into_iter().map(Into::into).collect(),
312        });
313        self
314    }
315
316    /// Build the final SpecArg
317    #[must_use]
318    pub fn build(mut self) -> SpecArg {
319        self.inner.usage = self.inner.usage();
320        self.inner
321    }
322}
323
324/// Builder for SpecCommand
325#[derive(Debug, Default, Clone)]
326pub struct SpecCommandBuilder {
327    inner: SpecCommand,
328}
329
330impl SpecCommandBuilder {
331    /// Create a new SpecCommandBuilder
332    pub fn new() -> Self {
333        Self::default()
334    }
335
336    /// Set the command name
337    pub fn name(mut self, name: impl Into<String>) -> Self {
338        self.inner.name = name.into();
339        self
340    }
341
342    /// Add an alias (can be called multiple times)
343    pub fn alias(mut self, alias: impl Into<String>) -> Self {
344        self.inner.aliases.push(alias.into());
345        self
346    }
347
348    /// Add multiple aliases at once
349    pub fn aliases<I, S>(mut self, aliases: I) -> Self
350    where
351        I: IntoIterator<Item = S>,
352        S: Into<String>,
353    {
354        self.inner
355            .aliases
356            .extend(aliases.into_iter().map(Into::into));
357        self
358    }
359
360    /// Add a hidden alias (can be called multiple times)
361    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
362        self.inner.hidden_aliases.push(alias.into());
363        self
364    }
365
366    /// Add multiple hidden aliases at once
367    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
368    where
369        I: IntoIterator<Item = S>,
370        S: Into<String>,
371    {
372        self.inner
373            .hidden_aliases
374            .extend(aliases.into_iter().map(Into::into));
375        self
376    }
377
378    /// Add a flag to the command
379    pub fn flag(mut self, flag: SpecFlag) -> Self {
380        self.inner.flags.push(flag);
381        self
382    }
383
384    /// Add multiple flags at once
385    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
386        self.inner.flags.extend(flags);
387        self
388    }
389
390    /// Add an argument to the command
391    pub fn arg(mut self, arg: SpecArg) -> Self {
392        self.inner.args.push(arg);
393        self
394    }
395
396    /// Add multiple arguments at once
397    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
398        self.inner.args.extend(args);
399        self
400    }
401
402    /// Set help text
403    pub fn help(mut self, text: impl Into<String>) -> Self {
404        self.inner.help = Some(text.into());
405        self
406    }
407
408    /// Set long help text
409    pub fn help_long(mut self, text: impl Into<String>) -> Self {
410        self.inner.help_long = Some(text.into());
411        self
412    }
413
414    /// Set markdown help text
415    pub fn help_md(mut self, text: impl Into<String>) -> Self {
416        self.inner.help_md = Some(text.into());
417        self
418    }
419
420    /// Set as hidden
421    pub fn hide(mut self, is_hidden: bool) -> Self {
422        self.inner.hide = is_hidden;
423        self
424    }
425
426    /// Set subcommand required
427    pub fn subcommand_required(mut self, required: bool) -> Self {
428        self.inner.subcommand_required = required;
429        self
430    }
431
432    /// Set deprecated message
433    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
434        self.inner.deprecated = Some(msg.into());
435        self
436    }
437
438    /// Set restart token for resetting argument parsing
439    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
440    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
441        self.inner.restart_token = Some(token.into());
442        self
443    }
444
445    /// Add a subcommand (can be called multiple times)
446    pub fn subcommand(mut self, cmd: SpecCommand) -> Self {
447        self.inner.subcommands.insert(cmd.name.clone(), cmd);
448        self
449    }
450
451    /// Add multiple subcommands at once
452    pub fn subcommands(mut self, cmds: impl IntoIterator<Item = SpecCommand>) -> Self {
453        for cmd in cmds {
454            self.inner.subcommands.insert(cmd.name.clone(), cmd);
455        }
456        self
457    }
458
459    /// Set before_help text (displayed before the help message)
460    pub fn before_help(mut self, text: impl Into<String>) -> Self {
461        self.inner.before_help = Some(text.into());
462        self
463    }
464
465    /// Set before_help_long text
466    pub fn before_help_long(mut self, text: impl Into<String>) -> Self {
467        self.inner.before_help_long = Some(text.into());
468        self
469    }
470
471    /// Set before_help markdown text
472    pub fn before_help_md(mut self, text: impl Into<String>) -> Self {
473        self.inner.before_help_md = Some(text.into());
474        self
475    }
476
477    /// Set after_help text (displayed after the help message)
478    pub fn after_help(mut self, text: impl Into<String>) -> Self {
479        self.inner.after_help = Some(text.into());
480        self
481    }
482
483    /// Set after_help_long text
484    pub fn after_help_long(mut self, text: impl Into<String>) -> Self {
485        self.inner.after_help_long = Some(text.into());
486        self
487    }
488
489    /// Set after_help markdown text
490    pub fn after_help_md(mut self, text: impl Into<String>) -> Self {
491        self.inner.after_help_md = Some(text.into());
492        self
493    }
494
495    /// Add an example (can be called multiple times)
496    pub fn example(mut self, code: impl Into<String>) -> Self {
497        self.inner.examples.push(SpecExample::new(code.into()));
498        self
499    }
500
501    /// Add an example with header and help text
502    pub fn example_with_help(
503        mut self,
504        code: impl Into<String>,
505        header: impl Into<String>,
506        help: impl Into<String>,
507    ) -> Self {
508        let mut example = SpecExample::new(code.into());
509        example.header = Some(header.into());
510        example.help = Some(help.into());
511        self.inner.examples.push(example);
512        self
513    }
514
515    /// Build the final SpecCommand
516    #[must_use]
517    pub fn build(mut self) -> SpecCommand {
518        self.inner.usage = self.inner.usage();
519        self.inner
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526
527    #[test]
528    fn test_flag_builder_basic() {
529        let flag = SpecFlagBuilder::new()
530            .name("verbose")
531            .short('v')
532            .long("verbose")
533            .help("Enable verbose output")
534            .build();
535
536        assert_eq!(flag.name, "verbose");
537        assert_eq!(flag.short, vec!['v']);
538        assert_eq!(flag.long, vec!["verbose".to_string()]);
539        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
540    }
541
542    #[test]
543    fn test_flag_builder_multiple_values() {
544        let flag = SpecFlagBuilder::new()
545            .shorts(['v', 'V'])
546            .longs(["verbose", "loud"])
547            .default_values(["info", "warn"])
548            .build();
549
550        assert_eq!(flag.short, vec!['v', 'V']);
551        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
552        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
553        assert!(!flag.required); // Should be false due to defaults
554    }
555
556    #[test]
557    fn test_flag_builder_variadic() {
558        let flag = SpecFlagBuilder::new()
559            .long("file")
560            .var(true)
561            .var_min(1)
562            .var_max(10)
563            .build();
564
565        assert!(flag.var);
566        assert_eq!(flag.var_min, Some(1));
567        assert_eq!(flag.var_max, Some(10));
568    }
569
570    #[test]
571    fn test_flag_builder_name_derivation() {
572        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
573
574        // Name should be derived from long flag
575        assert_eq!(flag.name, "verbose");
576
577        let flag2 = SpecFlagBuilder::new().short('v').build();
578
579        // Name should be derived from short flag if no long
580        assert_eq!(flag2.name, "v");
581    }
582
583    #[test]
584    fn test_arg_builder_basic() {
585        let arg = SpecArgBuilder::new()
586            .name("file")
587            .help("Input file")
588            .required(true)
589            .build();
590
591        assert_eq!(arg.name, "file");
592        assert_eq!(arg.help, Some("Input file".to_string()));
593        assert!(arg.required);
594    }
595
596    #[test]
597    fn test_arg_builder_variadic() {
598        let arg = SpecArgBuilder::new()
599            .name("files")
600            .var(true)
601            .var_min(1)
602            .var_max(10)
603            .help("Input files")
604            .build();
605
606        assert_eq!(arg.name, "files");
607        assert!(arg.var);
608        assert_eq!(arg.var_min, Some(1));
609        assert_eq!(arg.var_max, Some(10));
610    }
611
612    #[test]
613    fn test_arg_builder_defaults() {
614        let arg = SpecArgBuilder::new()
615            .name("file")
616            .default_values(["a.txt", "b.txt"])
617            .build();
618
619        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
620        assert!(!arg.required);
621    }
622
623    #[test]
624    fn test_command_builder_basic() {
625        let cmd = SpecCommandBuilder::new()
626            .name("install")
627            .help("Install packages")
628            .build();
629
630        assert_eq!(cmd.name, "install");
631        assert_eq!(cmd.help, Some("Install packages".to_string()));
632    }
633
634    #[test]
635    fn test_command_builder_aliases() {
636        let cmd = SpecCommandBuilder::new()
637            .name("install")
638            .alias("i")
639            .aliases(["add", "get"])
640            .hidden_aliases(["inst"])
641            .build();
642
643        assert_eq!(
644            cmd.aliases,
645            vec!["i".to_string(), "add".to_string(), "get".to_string()]
646        );
647        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
648    }
649
650    #[test]
651    fn test_command_builder_with_flags_and_args() {
652        let flag = SpecFlagBuilder::new().short('f').long("force").build();
653
654        let arg = SpecArgBuilder::new().name("package").required(true).build();
655
656        let cmd = SpecCommandBuilder::new()
657            .name("install")
658            .flag(flag)
659            .arg(arg)
660            .build();
661
662        assert_eq!(cmd.flags.len(), 1);
663        assert_eq!(cmd.flags[0].name, "force");
664        assert_eq!(cmd.args.len(), 1);
665        assert_eq!(cmd.args[0].name, "package");
666    }
667
668    #[test]
669    fn test_arg_builder_choices() {
670        let arg = SpecArgBuilder::new()
671            .name("format")
672            .choices(["json", "yaml", "toml"])
673            .build();
674
675        assert!(arg.choices.is_some());
676        let choices = arg.choices.unwrap();
677        assert_eq!(
678            choices.choices,
679            vec!["json".to_string(), "yaml".to_string(), "toml".to_string()]
680        );
681    }
682
683    #[test]
684    fn test_command_builder_subcommands() {
685        let sub1 = SpecCommandBuilder::new().name("sub1").build();
686        let sub2 = SpecCommandBuilder::new().name("sub2").build();
687
688        let cmd = SpecCommandBuilder::new()
689            .name("main")
690            .subcommand(sub1)
691            .subcommand(sub2)
692            .build();
693
694        assert_eq!(cmd.subcommands.len(), 2);
695        assert!(cmd.subcommands.contains_key("sub1"));
696        assert!(cmd.subcommands.contains_key("sub2"));
697    }
698
699    #[test]
700    fn test_command_builder_before_after_help() {
701        let cmd = SpecCommandBuilder::new()
702            .name("test")
703            .before_help("Before help text")
704            .before_help_long("Before help long text")
705            .after_help("After help text")
706            .after_help_long("After help long text")
707            .build();
708
709        assert_eq!(cmd.before_help, Some("Before help text".to_string()));
710        assert_eq!(
711            cmd.before_help_long,
712            Some("Before help long text".to_string())
713        );
714        assert_eq!(cmd.after_help, Some("After help text".to_string()));
715        assert_eq!(
716            cmd.after_help_long,
717            Some("After help long text".to_string())
718        );
719    }
720
721    #[test]
722    fn test_command_builder_examples() {
723        let cmd = SpecCommandBuilder::new()
724            .name("test")
725            .example("mycli run")
726            .example_with_help("mycli build", "Build example", "Build the project")
727            .build();
728
729        assert_eq!(cmd.examples.len(), 2);
730        assert_eq!(cmd.examples[0].code, "mycli run");
731        assert_eq!(cmd.examples[1].code, "mycli build");
732        assert_eq!(cmd.examples[1].header, Some("Build example".to_string()));
733        assert_eq!(cmd.examples[1].help, Some("Build the project".to_string()));
734    }
735}