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    pub fn build(mut self) -> SpecFlag {
189        self.inner.usage = self.inner.usage();
190        if self.inner.name.is_empty() {
191            // Derive name from long or short flags
192            if let Some(long) = self.inner.long.first() {
193                self.inner.name = long.clone();
194            } else if let Some(short) = self.inner.short.first() {
195                self.inner.name = short.to_string();
196            }
197        }
198        self.inner
199    }
200}
201
202/// Builder for SpecArg
203#[derive(Debug, Default, Clone)]
204pub struct SpecArgBuilder {
205    inner: SpecArg,
206}
207
208impl SpecArgBuilder {
209    /// Create a new SpecArgBuilder
210    pub fn new() -> Self {
211        Self::default()
212    }
213
214    /// Set the argument name
215    pub fn name(mut self, name: impl Into<String>) -> Self {
216        self.inner.name = name.into();
217        self
218    }
219
220    /// Add a default value (can be called multiple times for var args)
221    pub fn default_value(mut self, value: impl Into<String>) -> Self {
222        self.inner.default.push(value.into());
223        self.inner.required = false;
224        self
225    }
226
227    /// Add multiple default values at once
228    pub fn default_values<I, S>(mut self, values: I) -> Self
229    where
230        I: IntoIterator<Item = S>,
231        S: Into<String>,
232    {
233        self.inner
234            .default
235            .extend(values.into_iter().map(Into::into));
236        if !self.inner.default.is_empty() {
237            self.inner.required = false;
238        }
239        self
240    }
241
242    /// Set help text
243    pub fn help(mut self, text: impl Into<String>) -> Self {
244        self.inner.help = Some(text.into());
245        self
246    }
247
248    /// Set long help text
249    pub fn help_long(mut self, text: impl Into<String>) -> Self {
250        self.inner.help_long = Some(text.into());
251        self
252    }
253
254    /// Set markdown help text
255    pub fn help_md(mut self, text: impl Into<String>) -> Self {
256        self.inner.help_md = Some(text.into());
257        self
258    }
259
260    /// Set as variadic (accepts multiple values)
261    pub fn var(mut self, is_var: bool) -> Self {
262        self.inner.var = is_var;
263        self
264    }
265
266    /// Set minimum count for variadic argument
267    pub fn var_min(mut self, min: usize) -> Self {
268        self.inner.var_min = Some(min);
269        self
270    }
271
272    /// Set maximum count for variadic argument
273    pub fn var_max(mut self, max: usize) -> Self {
274        self.inner.var_max = Some(max);
275        self
276    }
277
278    /// Set as required
279    pub fn required(mut self, is_required: bool) -> Self {
280        self.inner.required = is_required;
281        self
282    }
283
284    /// Set as hidden
285    pub fn hide(mut self, is_hidden: bool) -> Self {
286        self.inner.hide = is_hidden;
287        self
288    }
289
290    /// Set environment variable name
291    pub fn env(mut self, env: impl Into<String>) -> Self {
292        self.inner.env = Some(env.into());
293        self
294    }
295
296    /// Set the double-dash behavior
297    pub fn double_dash(mut self, behavior: SpecDoubleDashChoices) -> Self {
298        self.inner.double_dash = behavior;
299        self
300    }
301
302    /// Build the final SpecArg
303    pub fn build(mut self) -> SpecArg {
304        self.inner.usage = self.inner.usage();
305        self.inner
306    }
307}
308
309/// Builder for SpecCommand
310#[derive(Debug, Default, Clone)]
311pub struct SpecCommandBuilder {
312    inner: SpecCommand,
313}
314
315impl SpecCommandBuilder {
316    /// Create a new SpecCommandBuilder
317    pub fn new() -> Self {
318        Self::default()
319    }
320
321    /// Set the command name
322    pub fn name(mut self, name: impl Into<String>) -> Self {
323        self.inner.name = name.into();
324        self
325    }
326
327    /// Add an alias (can be called multiple times)
328    pub fn alias(mut self, alias: impl Into<String>) -> Self {
329        self.inner.aliases.push(alias.into());
330        self
331    }
332
333    /// Add multiple aliases at once
334    pub fn aliases<I, S>(mut self, aliases: I) -> Self
335    where
336        I: IntoIterator<Item = S>,
337        S: Into<String>,
338    {
339        self.inner
340            .aliases
341            .extend(aliases.into_iter().map(Into::into));
342        self
343    }
344
345    /// Add a hidden alias (can be called multiple times)
346    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
347        self.inner.hidden_aliases.push(alias.into());
348        self
349    }
350
351    /// Add multiple hidden aliases at once
352    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
353    where
354        I: IntoIterator<Item = S>,
355        S: Into<String>,
356    {
357        self.inner
358            .hidden_aliases
359            .extend(aliases.into_iter().map(Into::into));
360        self
361    }
362
363    /// Add a flag to the command
364    pub fn flag(mut self, flag: SpecFlag) -> Self {
365        self.inner.flags.push(flag);
366        self
367    }
368
369    /// Add multiple flags at once
370    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
371        self.inner.flags.extend(flags);
372        self
373    }
374
375    /// Add an argument to the command
376    pub fn arg(mut self, arg: SpecArg) -> Self {
377        self.inner.args.push(arg);
378        self
379    }
380
381    /// Add multiple arguments at once
382    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
383        self.inner.args.extend(args);
384        self
385    }
386
387    /// Set help text
388    pub fn help(mut self, text: impl Into<String>) -> Self {
389        self.inner.help = Some(text.into());
390        self
391    }
392
393    /// Set long help text
394    pub fn help_long(mut self, text: impl Into<String>) -> Self {
395        self.inner.help_long = Some(text.into());
396        self
397    }
398
399    /// Set markdown help text
400    pub fn help_md(mut self, text: impl Into<String>) -> Self {
401        self.inner.help_md = Some(text.into());
402        self
403    }
404
405    /// Set as hidden
406    pub fn hide(mut self, is_hidden: bool) -> Self {
407        self.inner.hide = is_hidden;
408        self
409    }
410
411    /// Set subcommand required
412    pub fn subcommand_required(mut self, required: bool) -> Self {
413        self.inner.subcommand_required = required;
414        self
415    }
416
417    /// Set deprecated message
418    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
419        self.inner.deprecated = Some(msg.into());
420        self
421    }
422
423    /// Set restart token for resetting argument parsing
424    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
425    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
426        self.inner.restart_token = Some(token.into());
427        self
428    }
429
430    /// Build the final SpecCommand
431    pub fn build(mut self) -> SpecCommand {
432        self.inner.usage = self.inner.usage();
433        self.inner
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_flag_builder_basic() {
443        let flag = SpecFlagBuilder::new()
444            .name("verbose")
445            .short('v')
446            .long("verbose")
447            .help("Enable verbose output")
448            .build();
449
450        assert_eq!(flag.name, "verbose");
451        assert_eq!(flag.short, vec!['v']);
452        assert_eq!(flag.long, vec!["verbose".to_string()]);
453        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
454    }
455
456    #[test]
457    fn test_flag_builder_multiple_values() {
458        let flag = SpecFlagBuilder::new()
459            .shorts(['v', 'V'])
460            .longs(["verbose", "loud"])
461            .default_values(["info", "warn"])
462            .build();
463
464        assert_eq!(flag.short, vec!['v', 'V']);
465        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
466        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
467        assert!(!flag.required); // Should be false due to defaults
468    }
469
470    #[test]
471    fn test_flag_builder_variadic() {
472        let flag = SpecFlagBuilder::new()
473            .long("file")
474            .var(true)
475            .var_min(1)
476            .var_max(10)
477            .build();
478
479        assert!(flag.var);
480        assert_eq!(flag.var_min, Some(1));
481        assert_eq!(flag.var_max, Some(10));
482    }
483
484    #[test]
485    fn test_flag_builder_name_derivation() {
486        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
487
488        // Name should be derived from long flag
489        assert_eq!(flag.name, "verbose");
490
491        let flag2 = SpecFlagBuilder::new().short('v').build();
492
493        // Name should be derived from short flag if no long
494        assert_eq!(flag2.name, "v");
495    }
496
497    #[test]
498    fn test_arg_builder_basic() {
499        let arg = SpecArgBuilder::new()
500            .name("file")
501            .help("Input file")
502            .required(true)
503            .build();
504
505        assert_eq!(arg.name, "file");
506        assert_eq!(arg.help, Some("Input file".to_string()));
507        assert!(arg.required);
508    }
509
510    #[test]
511    fn test_arg_builder_variadic() {
512        let arg = SpecArgBuilder::new()
513            .name("files")
514            .var(true)
515            .var_min(1)
516            .var_max(10)
517            .help("Input files")
518            .build();
519
520        assert_eq!(arg.name, "files");
521        assert!(arg.var);
522        assert_eq!(arg.var_min, Some(1));
523        assert_eq!(arg.var_max, Some(10));
524    }
525
526    #[test]
527    fn test_arg_builder_defaults() {
528        let arg = SpecArgBuilder::new()
529            .name("file")
530            .default_values(["a.txt", "b.txt"])
531            .build();
532
533        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
534        assert!(!arg.required);
535    }
536
537    #[test]
538    fn test_command_builder_basic() {
539        let cmd = SpecCommandBuilder::new()
540            .name("install")
541            .help("Install packages")
542            .build();
543
544        assert_eq!(cmd.name, "install");
545        assert_eq!(cmd.help, Some("Install packages".to_string()));
546    }
547
548    #[test]
549    fn test_command_builder_aliases() {
550        let cmd = SpecCommandBuilder::new()
551            .name("install")
552            .alias("i")
553            .aliases(["add", "get"])
554            .hidden_aliases(["inst"])
555            .build();
556
557        assert_eq!(
558            cmd.aliases,
559            vec!["i".to_string(), "add".to_string(), "get".to_string()]
560        );
561        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
562    }
563
564    #[test]
565    fn test_command_builder_with_flags_and_args() {
566        let flag = SpecFlagBuilder::new().short('f').long("force").build();
567
568        let arg = SpecArgBuilder::new().name("package").required(true).build();
569
570        let cmd = SpecCommandBuilder::new()
571            .name("install")
572            .flag(flag)
573            .arg(arg)
574            .build();
575
576        assert_eq!(cmd.flags.len(), 1);
577        assert_eq!(cmd.flags[0].name, "force");
578        assert_eq!(cmd.args.len(), 1);
579        assert_eq!(cmd.args[0].name, "package");
580    }
581}