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