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::{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    /// Build the final SpecArg
297    pub fn build(mut self) -> SpecArg {
298        self.inner.usage = self.inner.usage();
299        self.inner
300    }
301}
302
303/// Builder for SpecCommand
304#[derive(Debug, Default, Clone)]
305pub struct SpecCommandBuilder {
306    inner: SpecCommand,
307}
308
309impl SpecCommandBuilder {
310    /// Create a new SpecCommandBuilder
311    pub fn new() -> Self {
312        Self::default()
313    }
314
315    /// Set the command name
316    pub fn name(mut self, name: impl Into<String>) -> Self {
317        self.inner.name = name.into();
318        self
319    }
320
321    /// Add an alias (can be called multiple times)
322    pub fn alias(mut self, alias: impl Into<String>) -> Self {
323        self.inner.aliases.push(alias.into());
324        self
325    }
326
327    /// Add multiple aliases at once
328    pub fn aliases<I, S>(mut self, aliases: I) -> Self
329    where
330        I: IntoIterator<Item = S>,
331        S: Into<String>,
332    {
333        self.inner
334            .aliases
335            .extend(aliases.into_iter().map(Into::into));
336        self
337    }
338
339    /// Add a hidden alias (can be called multiple times)
340    pub fn hidden_alias(mut self, alias: impl Into<String>) -> Self {
341        self.inner.hidden_aliases.push(alias.into());
342        self
343    }
344
345    /// Add multiple hidden aliases at once
346    pub fn hidden_aliases<I, S>(mut self, aliases: I) -> Self
347    where
348        I: IntoIterator<Item = S>,
349        S: Into<String>,
350    {
351        self.inner
352            .hidden_aliases
353            .extend(aliases.into_iter().map(Into::into));
354        self
355    }
356
357    /// Add a flag to the command
358    pub fn flag(mut self, flag: SpecFlag) -> Self {
359        self.inner.flags.push(flag);
360        self
361    }
362
363    /// Add multiple flags at once
364    pub fn flags(mut self, flags: impl IntoIterator<Item = SpecFlag>) -> Self {
365        self.inner.flags.extend(flags);
366        self
367    }
368
369    /// Add an argument to the command
370    pub fn arg(mut self, arg: SpecArg) -> Self {
371        self.inner.args.push(arg);
372        self
373    }
374
375    /// Add multiple arguments at once
376    pub fn args(mut self, args: impl IntoIterator<Item = SpecArg>) -> Self {
377        self.inner.args.extend(args);
378        self
379    }
380
381    /// Set help text
382    pub fn help(mut self, text: impl Into<String>) -> Self {
383        self.inner.help = Some(text.into());
384        self
385    }
386
387    /// Set long help text
388    pub fn help_long(mut self, text: impl Into<String>) -> Self {
389        self.inner.help_long = Some(text.into());
390        self
391    }
392
393    /// Set markdown help text
394    pub fn help_md(mut self, text: impl Into<String>) -> Self {
395        self.inner.help_md = Some(text.into());
396        self
397    }
398
399    /// Set as hidden
400    pub fn hide(mut self, is_hidden: bool) -> Self {
401        self.inner.hide = is_hidden;
402        self
403    }
404
405    /// Set subcommand required
406    pub fn subcommand_required(mut self, required: bool) -> Self {
407        self.inner.subcommand_required = required;
408        self
409    }
410
411    /// Set deprecated message
412    pub fn deprecated(mut self, msg: impl Into<String>) -> Self {
413        self.inner.deprecated = Some(msg.into());
414        self
415    }
416
417    /// Set restart token for resetting argument parsing
418    /// e.g., `mise run lint ::: test ::: check` with restart_token=":::"
419    pub fn restart_token(mut self, token: impl Into<String>) -> Self {
420        self.inner.restart_token = Some(token.into());
421        self
422    }
423
424    /// Build the final SpecCommand
425    pub fn build(mut self) -> SpecCommand {
426        self.inner.usage = self.inner.usage();
427        self.inner
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_flag_builder_basic() {
437        let flag = SpecFlagBuilder::new()
438            .name("verbose")
439            .short('v')
440            .long("verbose")
441            .help("Enable verbose output")
442            .build();
443
444        assert_eq!(flag.name, "verbose");
445        assert_eq!(flag.short, vec!['v']);
446        assert_eq!(flag.long, vec!["verbose".to_string()]);
447        assert_eq!(flag.help, Some("Enable verbose output".to_string()));
448    }
449
450    #[test]
451    fn test_flag_builder_multiple_values() {
452        let flag = SpecFlagBuilder::new()
453            .shorts(['v', 'V'])
454            .longs(["verbose", "loud"])
455            .default_values(["info", "warn"])
456            .build();
457
458        assert_eq!(flag.short, vec!['v', 'V']);
459        assert_eq!(flag.long, vec!["verbose".to_string(), "loud".to_string()]);
460        assert_eq!(flag.default, vec!["info".to_string(), "warn".to_string()]);
461        assert!(!flag.required); // Should be false due to defaults
462    }
463
464    #[test]
465    fn test_flag_builder_variadic() {
466        let flag = SpecFlagBuilder::new()
467            .long("file")
468            .var(true)
469            .var_min(1)
470            .var_max(10)
471            .build();
472
473        assert!(flag.var);
474        assert_eq!(flag.var_min, Some(1));
475        assert_eq!(flag.var_max, Some(10));
476    }
477
478    #[test]
479    fn test_flag_builder_name_derivation() {
480        let flag = SpecFlagBuilder::new().short('v').long("verbose").build();
481
482        // Name should be derived from long flag
483        assert_eq!(flag.name, "verbose");
484
485        let flag2 = SpecFlagBuilder::new().short('v').build();
486
487        // Name should be derived from short flag if no long
488        assert_eq!(flag2.name, "v");
489    }
490
491    #[test]
492    fn test_arg_builder_basic() {
493        let arg = SpecArgBuilder::new()
494            .name("file")
495            .help("Input file")
496            .required(true)
497            .build();
498
499        assert_eq!(arg.name, "file");
500        assert_eq!(arg.help, Some("Input file".to_string()));
501        assert!(arg.required);
502    }
503
504    #[test]
505    fn test_arg_builder_variadic() {
506        let arg = SpecArgBuilder::new()
507            .name("files")
508            .var(true)
509            .var_min(1)
510            .var_max(10)
511            .help("Input files")
512            .build();
513
514        assert_eq!(arg.name, "files");
515        assert!(arg.var);
516        assert_eq!(arg.var_min, Some(1));
517        assert_eq!(arg.var_max, Some(10));
518    }
519
520    #[test]
521    fn test_arg_builder_defaults() {
522        let arg = SpecArgBuilder::new()
523            .name("file")
524            .default_values(["a.txt", "b.txt"])
525            .build();
526
527        assert_eq!(arg.default, vec!["a.txt".to_string(), "b.txt".to_string()]);
528        assert!(!arg.required);
529    }
530
531    #[test]
532    fn test_command_builder_basic() {
533        let cmd = SpecCommandBuilder::new()
534            .name("install")
535            .help("Install packages")
536            .build();
537
538        assert_eq!(cmd.name, "install");
539        assert_eq!(cmd.help, Some("Install packages".to_string()));
540    }
541
542    #[test]
543    fn test_command_builder_aliases() {
544        let cmd = SpecCommandBuilder::new()
545            .name("install")
546            .alias("i")
547            .aliases(["add", "get"])
548            .hidden_aliases(["inst"])
549            .build();
550
551        assert_eq!(
552            cmd.aliases,
553            vec!["i".to_string(), "add".to_string(), "get".to_string()]
554        );
555        assert_eq!(cmd.hidden_aliases, vec!["inst".to_string()]);
556    }
557
558    #[test]
559    fn test_command_builder_with_flags_and_args() {
560        let flag = SpecFlagBuilder::new().short('f').long("force").build();
561
562        let arg = SpecArgBuilder::new().name("package").required(true).build();
563
564        let cmd = SpecCommandBuilder::new()
565            .name("install")
566            .flag(flag)
567            .arg(arg)
568            .build();
569
570        assert_eq!(cmd.flags.len(), 1);
571        assert_eq!(cmd.flags[0].name, "force");
572        assert_eq!(cmd.args.len(), 1);
573        assert_eq!(cmd.args[0].name, "package");
574    }
575}