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::arg::SpecDoubleDashChoices, SpecArg, SpecCommand, SpecFlag};
34
35/// Builder for SpecFlag
36#[derive(Debug, Default, Clone)]
37pub struct SpecFlagBuilder {
38    inner: SpecFlag,
39}
40
41impl SpecFlagBuilder {
42    /// Create a new SpecFlagBuilder
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Set the flag name
48    pub fn name(mut self, name: impl Into<String>) -> Self {
49        self.inner.name = name.into();
50        self
51    }
52
53    /// Add a short flag character (can be called multiple times)
54    pub fn short(mut self, c: char) -> Self {
55        self.inner.short.push(c);
56        self
57    }
58
59    /// Add multiple short flags at once
60    pub fn shorts(mut self, chars: impl IntoIterator<Item = char>) -> Self {
61        self.inner.short.extend(chars);
62        self
63    }
64
65    /// Add a long flag name (can be called multiple times)
66    pub fn long(mut self, name: impl Into<String>) -> Self {
67        self.inner.long.push(name.into());
68        self
69    }
70
71    /// Add multiple long flags at once
72    pub fn longs<I, S>(mut self, names: I) -> Self
73    where
74        I: IntoIterator<Item = S>,
75        S: Into<String>,
76    {
77        self.inner.long.extend(names.into_iter().map(Into::into));
78        self
79    }
80
81    /// Add a default value (can be called multiple times for var flags)
82    pub fn default_value(mut self, value: impl Into<String>) -> Self {
83        self.inner.default.push(value.into());
84        self.inner.required = false;
85        self
86    }
87
88    /// Add multiple default values at once
89    pub fn default_values<I, S>(mut self, values: I) -> Self
90    where
91        I: IntoIterator<Item = S>,
92        S: Into<String>,
93    {
94        self.inner
95            .default
96            .extend(values.into_iter().map(Into::into));
97        if !self.inner.default.is_empty() {
98            self.inner.required = false;
99        }
100        self
101    }
102
103    /// Set help text
104    pub fn help(mut self, text: impl Into<String>) -> Self {
105        self.inner.help = Some(text.into());
106        self
107    }
108
109    /// Set long help text
110    pub fn help_long(mut self, text: impl Into<String>) -> Self {
111        self.inner.help_long = Some(text.into());
112        self
113    }
114
115    /// Set markdown help text
116    pub fn help_md(mut self, text: impl Into<String>) -> Self {
117        self.inner.help_md = Some(text.into());
118        self
119    }
120
121    /// Set as variadic (can be specified multiple times)
122    pub fn var(mut self, is_var: bool) -> Self {
123        self.inner.var = is_var;
124        self
125    }
126
127    /// Set minimum count for variadic flag
128    pub fn var_min(mut self, min: usize) -> Self {
129        self.inner.var_min = Some(min);
130        self
131    }
132
133    /// Set maximum count for variadic flag
134    pub fn var_max(mut self, max: usize) -> Self {
135        self.inner.var_max = Some(max);
136        self
137    }
138
139    /// Set as required
140    pub fn required(mut self, is_required: bool) -> Self {
141        self.inner.required = is_required;
142        self
143    }
144
145    /// Set as global (available to subcommands)
146    pub fn global(mut self, is_global: bool) -> Self {
147        self.inner.global = is_global;
148        self
149    }
150
151    /// Set as hidden
152    pub fn hide(mut self, is_hidden: bool) -> Self {
153        self.inner.hide = is_hidden;
154        self
155    }
156
157    /// Set as count flag
158    pub fn count(mut self, is_count: bool) -> Self {
159        self.inner.count = is_count;
160        self
161    }
162
163    /// Set the argument spec for flags that take values
164    pub fn arg(mut self, arg: SpecArg) -> Self {
165        self.inner.arg = Some(arg);
166        self
167    }
168
169    /// Set negate string
170    pub fn negate(mut self, negate: impl Into<String>) -> Self {
171        self.inner.negate = Some(negate.into());
172        self
173    }
174
175    /// Set environment variable name
176    pub fn env(mut self, env: impl Into<String>) -> Self {
177        self.inner.env = Some(env.into());
178        self
179    }
180
181    /// Set deprecated message
182    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
183        self.inner.deprecated = Some(msg.into());
184        self
185    }
186
187    /// Build the final SpecFlag
188    #[must_use]
189    pub fn build(mut self) -> SpecFlag {
190        self.inner.usage = self.inner.usage();
191        if self.inner.name.is_empty() {
192            // Derive name from long or short flags
193            if let Some(long) = self.inner.long.first() {
194                self.inner.name = long.clone();
195            } else if let Some(short) = self.inner.short.first() {
196                self.inner.name = short.to_string();
197            }
198        }
199        self.inner
200    }
201}
202
203/// Builder for SpecArg
204#[derive(Debug, Default, Clone)]
205pub struct SpecArgBuilder {
206    inner: SpecArg,
207}
208
209impl SpecArgBuilder {
210    /// Create a new SpecArgBuilder
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Set the argument name
216    pub fn name(mut self, name: impl Into<String>) -> Self {
217        self.inner.name = name.into();
218        self
219    }
220
221    /// Add a default value (can be called multiple times for var args)
222    pub fn default_value(mut self, value: impl Into<String>) -> Self {
223        self.inner.default.push(value.into());
224        self.inner.required = false;
225        self
226    }
227
228    /// Add multiple default values at once
229    pub fn default_values<I, S>(mut self, values: I) -> Self
230    where
231        I: IntoIterator<Item = S>,
232        S: Into<String>,
233    {
234        self.inner
235            .default
236            .extend(values.into_iter().map(Into::into));
237        if !self.inner.default.is_empty() {
238            self.inner.required = false;
239        }
240        self
241    }
242
243    /// Set help text
244    pub fn help(mut self, text: impl Into<String>) -> Self {
245        self.inner.help = Some(text.into());
246        self
247    }
248
249    /// Set long help text
250    pub fn help_long(mut self, text: impl Into<String>) -> Self {
251        self.inner.help_long = Some(text.into());
252        self
253    }
254
255    /// Set markdown help text
256    pub fn help_md(mut self, text: impl Into<String>) -> Self {
257        self.inner.help_md = Some(text.into());
258        self
259    }
260
261    /// Set as variadic (accepts multiple values)
262    pub fn var(mut self, is_var: bool) -> Self {
263        self.inner.var = is_var;
264        self
265    }
266
267    /// Set minimum count for variadic argument
268    pub fn var_min(mut self, min: usize) -> Self {
269        self.inner.var_min = Some(min);
270        self
271    }
272
273    /// Set maximum count for variadic argument
274    pub fn var_max(mut self, max: usize) -> Self {
275        self.inner.var_max = Some(max);
276        self
277    }
278
279    /// Set as required
280    pub fn required(mut self, is_required: bool) -> Self {
281        self.inner.required = is_required;
282        self
283    }
284
285    /// Set as hidden
286    pub fn hide(mut self, is_hidden: bool) -> Self {
287        self.inner.hide = is_hidden;
288        self
289    }
290
291    /// Set environment variable name
292    pub fn env(mut self, env: impl Into<String>) -> Self {
293        self.inner.env = Some(env.into());
294        self
295    }
296
297    /// Set the double-dash behavior
298    pub fn double_dash(mut self, behavior: SpecDoubleDashChoices) -> Self {
299        self.inner.double_dash = behavior;
300        self
301    }
302
303    /// Build the final SpecArg
304    #[must_use]
305    pub fn build(mut self) -> SpecArg {
306        self.inner.usage = self.inner.usage();
307        self.inner
308    }
309}
310
311/// Builder for SpecCommand
312#[derive(Debug, Default, Clone)]
313pub struct SpecCommandBuilder {
314    inner: SpecCommand,
315}
316
317impl SpecCommandBuilder {
318    /// Create a new SpecCommandBuilder
319    pub fn new() -> Self {
320        Self::default()
321    }
322
323    /// Set the command name
324    pub fn name(mut self, name: impl Into<String>) -> Self {
325        self.inner.name = name.into();
326        self
327    }
328
329    /// Add an alias (can be called multiple times)
330    pub fn alias(mut self, alias: impl Into<String>) -> Self {
331        self.inner.aliases.push(alias.into());
332        self
333    }
334
335    /// Add multiple aliases at once
336    pub fn aliases<I, S>(mut self, aliases: I) -> Self
337    where
338        I: IntoIterator<Item = S>,
339        S: Into<String>,
340    {
341        self.inner
342            .aliases
343            .extend(aliases.into_iter().map(Into::into));
344        self
345    }
346
347    /// Add a hidden alias (can be called multiple times)
348    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
349        self.inner.hidden_aliases.push(alias.into());
350        self
351    }
352
353    /// Add multiple hidden aliases at once
354    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
355    where
356        I: IntoIterator<Item = S>,
357        S: Into<String>,
358    {
359        self.inner
360            .hidden_aliases
361            .extend(aliases.into_iter().map(Into::into));
362        self
363    }
364
365    /// Add a flag to the command
366    pub fn flag(mut self, flag: SpecFlag) -> Self {
367        self.inner.flags.push(flag);
368        self
369    }
370
371    /// Add multiple flags at once
372    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
373        self.inner.flags.extend(flags);
374        self
375    }
376
377    /// Add an argument to the command
378    pub fn arg(mut self, arg: SpecArg) -> Self {
379        self.inner.args.push(arg);
380        self
381    }
382
383    /// Add multiple arguments at once
384    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
385        self.inner.args.extend(args);
386        self
387    }
388
389    /// Set help text
390    pub fn help(mut self, text: impl Into<String>) -> Self {
391        self.inner.help = Some(text.into());
392        self
393    }
394
395    /// Set long help text
396    pub fn help_long(mut self, text: impl Into<String>) -> Self {
397        self.inner.help_long = Some(text.into());
398        self
399    }
400
401    /// Set markdown help text
402    pub fn help_md(mut self, text: impl Into<String>) -> Self {
403        self.inner.help_md = Some(text.into());
404        self
405    }
406
407    /// Set as hidden
408    pub fn hide(mut self, is_hidden: bool) -> Self {
409        self.inner.hide = is_hidden;
410        self
411    }
412
413    /// Set subcommand required
414    pub fn subcommand_required(mut self, required: bool) -> Self {
415        self.inner.subcommand_required = required;
416        self
417    }
418
419    /// Set deprecated message
420    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
421        self.inner.deprecated = Some(msg.into());
422        self
423    }
424
425    /// Set restart token for resetting argument parsing
426    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
427    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
428        self.inner.restart_token = Some(token.into());
429        self
430    }
431
432    /// Build the final SpecCommand
433    #[must_use]
434    pub fn build(mut self) -> SpecCommand {
435        self.inner.usage = self.inner.usage();
436        self.inner
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_flag_builder_basic() {
446        let flag = SpecFlagBuilder::new()
447            .name("verbose")
448            .short('v')
449            .long("verbose")
450            .help("Enable verbose output")
451            .build();
452
453        assert_eq!(flag.name, "verbose");
454        assert_eq!(flag.short, vec!['v']);
455        assert_eq!(flag.long, vec!["verbose".to_string()]);
456        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
457    }
458
459    #[test]
460    fn test_flag_builder_multiple_values() {
461        let flag = SpecFlagBuilder::new()
462            .shorts(['v', 'V'])
463            .longs(["verbose", "loud"])
464            .default_values(["info", "warn"])
465            .build();
466
467        assert_eq!(flag.short, vec!['v', 'V']);
468        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
469        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
470        assert!(!flag.required); // Should be false due to defaults
471    }
472
473    #[test]
474    fn test_flag_builder_variadic() {
475        let flag = SpecFlagBuilder::new()
476            .long("file")
477            .var(true)
478            .var_min(1)
479            .var_max(10)
480            .build();
481
482        assert!(flag.var);
483        assert_eq!(flag.var_min, Some(1));
484        assert_eq!(flag.var_max, Some(10));
485    }
486
487    #[test]
488    fn test_flag_builder_name_derivation() {
489        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
490
491        // Name should be derived from long flag
492        assert_eq!(flag.name, "verbose");
493
494        let flag2 = SpecFlagBuilder::new().short('v').build();
495
496        // Name should be derived from short flag if no long
497        assert_eq!(flag2.name, "v");
498    }
499
500    #[test]
501    fn test_arg_builder_basic() {
502        let arg = SpecArgBuilder::new()
503            .name("file")
504            .help("Input file")
505            .required(true)
506            .build();
507
508        assert_eq!(arg.name, "file");
509        assert_eq!(arg.help, Some("Input file".to_string()));
510        assert!(arg.required);
511    }
512
513    #[test]
514    fn test_arg_builder_variadic() {
515        let arg = SpecArgBuilder::new()
516            .name("files")
517            .var(true)
518            .var_min(1)
519            .var_max(10)
520            .help("Input files")
521            .build();
522
523        assert_eq!(arg.name, "files");
524        assert!(arg.var);
525        assert_eq!(arg.var_min, Some(1));
526        assert_eq!(arg.var_max, Some(10));
527    }
528
529    #[test]
530    fn test_arg_builder_defaults() {
531        let arg = SpecArgBuilder::new()
532            .name("file")
533            .default_values(["a.txt", "b.txt"])
534            .build();
535
536        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
537        assert!(!arg.required);
538    }
539
540    #[test]
541    fn test_command_builder_basic() {
542        let cmd = SpecCommandBuilder::new()
543            .name("install")
544            .help("Install packages")
545            .build();
546
547        assert_eq!(cmd.name, "install");
548        assert_eq!(cmd.help, Some("Install packages".to_string()));
549    }
550
551    #[test]
552    fn test_command_builder_aliases() {
553        let cmd = SpecCommandBuilder::new()
554            .name("install")
555            .alias("i")
556            .aliases(["add", "get"])
557            .hidden_aliases(["inst"])
558            .build();
559
560        assert_eq!(
561            cmd.aliases,
562            vec!["i".to_string(), "add".to_string(), "get".to_string()]
563        );
564        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
565    }
566
567    #[test]
568    fn test_command_builder_with_flags_and_args() {
569        let flag = SpecFlagBuilder::new().short('f').long("force").build();
570
571        let arg = SpecArgBuilder::new().name("package").required(true).build();
572
573        let cmd = SpecCommandBuilder::new()
574            .name("install")
575            .flag(flag)
576            .arg(arg)
577            .build();
578
579        assert_eq!(cmd.flags.len(), 1);
580        assert_eq!(cmd.flags[0].name, "force");
581        assert_eq!(cmd.args.len(), 1);
582        assert_eq!(cmd.args[0].name, "package");
583    }
584}