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