Skip to main content

usage/spec/
flag.rs

1use itertools::Itertools;
2use kdl::{KdlDocument, KdlEntry, KdlNode};
3use serde::Serialize;
4use std::fmt::Display;
5use std::hash::Hash;
6use std::str::FromStr;
7
8use crate::error::UsageErr::InvalidFlag;
9use crate::error::{Result, UsageErr};
10use crate::spec::arg::SpecDoubleDashChoices;
11use crate::spec::builder::SpecFlagBuilder;
12use crate::spec::context::ParsingContext;
13use crate::spec::helpers::{string_entry, NodeHelper};
14use crate::spec::is_false;
15use crate::{string, SpecArg, SpecChoices};
16
17/// A CLI flag/option specification.
18///
19/// Flags are optional arguments that start with `-` (short) or `--` (long).
20/// They can be boolean switches or accept values.
21///
22/// # Example
23///
24/// ```
25/// use usage::SpecFlag;
26///
27/// let flag = SpecFlag::builder()
28///     .short('v')
29///     .long("verbose")
30///     .help("Enable verbose output")
31///     .build();
32/// ```
33#[derive(Debug, Default, Clone, Serialize)]
34pub struct SpecFlag {
35    /// Internal name for the flag (derived from long/short if not set)
36    pub name: String,
37    /// Generated usage string (e.g., "-v, --verbose")
38    pub usage: String,
39    /// Short help text shown in command listings
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub help: Option<String>,
42    /// Extended help text shown with --help
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub help_long: Option<String>,
45    /// Markdown-formatted help text
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub help_md: Option<String>,
48    /// First line of help text (auto-generated)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub help_first_line: Option<String>,
51    /// Short flag characters (e.g., 'v' for -v)
52    pub short: Vec<char>,
53    /// Long flag names (e.g., "verbose" for --verbose)
54    pub long: Vec<String>,
55    /// Whether this flag must be provided
56    #[serde(skip_serializing_if = "is_false")]
57    pub required: bool,
58    /// Deprecation message if this flag is deprecated
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub deprecated: Option<String>,
61    /// Whether this flag can be specified multiple times
62    #[serde(skip_serializing_if = "is_false")]
63    pub var: bool,
64    /// Minimum number of times this flag must appear (for var flags)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub var_min: Option<usize>,
67    /// Maximum number of times this flag can appear (for var flags)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub var_max: Option<usize>,
70    /// Whether to hide this flag from help output
71    pub hide: bool,
72    /// Whether this flag is available to all subcommands
73    pub global: bool,
74    /// Whether this is a count flag (e.g., -vvv counts as 3)
75    #[serde(skip_serializing_if = "is_false")]
76    pub count: bool,
77    /// Argument specification if this flag takes a value
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub arg: Option<SpecArg>,
80    /// Default value(s) if the flag is not provided
81    #[serde(skip_serializing_if = "Vec::is_empty")]
82    pub default: Vec<String>,
83    /// Negation prefix (e.g., "no-" for --no-verbose)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub negate: Option<String>,
86    /// Environment variable that can set this flag's value
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub env: Option<String>,
89}
90
91impl SpecFlag {
92    /// Create a new builder for SpecFlag
93    pub fn builder() -> SpecFlagBuilder {
94        SpecFlagBuilder::new()
95    }
96
97    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
98        let mut flag: Self = node.arg(0)?.ensure_string()?.parse()?;
99        let mut allow_hyphen_values = false;
100        for (k, v) in node.props() {
101            match k {
102                "help" => flag.help = Some(v.ensure_string()?),
103                "long_help" => flag.help_long = Some(v.ensure_string()?),
104                "help_long" => flag.help_long = Some(v.ensure_string()?),
105                "help_md" => flag.help_md = Some(v.ensure_string()?),
106                "required" => flag.required = v.ensure_bool()?,
107                "var" => flag.var = v.ensure_bool()?,
108                "var_min" => flag.var_min = v.ensure_usize().map(Some)?,
109                "var_max" => flag.var_max = v.ensure_usize().map(Some)?,
110                "hide" => flag.hide = v.ensure_bool()?,
111                "deprecated" => {
112                    flag.deprecated = match v.value.as_bool() {
113                        Some(true) => Some("deprecated".into()),
114                        Some(false) => None,
115                        None => Some(v.ensure_string()?),
116                    }
117                }
118                "global" => flag.global = v.ensure_bool()?,
119                "count" => flag.count = v.ensure_bool()?,
120                "allow_hyphen_values" => allow_hyphen_values = v.ensure_bool()?,
121                "default" => {
122                    // Support both string and boolean defaults
123                    let default_value = match v.value.as_bool() {
124                        Some(b) => b.to_string(),
125                        None => v.ensure_string()?,
126                    };
127                    flag.default = vec![default_value];
128                }
129                "negate" => flag.negate = v.ensure_string().map(Some)?,
130                "env" => flag.env = v.ensure_string().map(Some)?,
131                k => bail_parse!(ctx, v.entry.span(), "unsupported flag key {k}"),
132            }
133        }
134        if !flag.default.is_empty() {
135            flag.required = false;
136        }
137        for child in node.children() {
138            match child.name() {
139                "arg" => flag.arg = Some(SpecArg::parse(ctx, &child)?),
140                "help" => flag.help = Some(child.arg(0)?.ensure_string()?),
141                "long_help" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
142                "help_long" => flag.help_long = Some(child.arg(0)?.ensure_string()?),
143                "help_md" => flag.help_md = Some(child.arg(0)?.ensure_string()?),
144                "required" => flag.required = child.arg(0)?.ensure_bool()?,
145                "var" => flag.var = child.arg(0)?.ensure_bool()?,
146                "var_min" => flag.var_min = child.arg(0)?.ensure_usize().map(Some)?,
147                "var_max" => flag.var_max = child.arg(0)?.ensure_usize().map(Some)?,
148                "hide" => flag.hide = child.arg(0)?.ensure_bool()?,
149                "deprecated" => {
150                    flag.deprecated = match child.arg(0)?.ensure_bool() {
151                        Ok(true) => Some("deprecated".into()),
152                        Ok(false) => None,
153                        _ => Some(child.arg(0)?.ensure_string()?),
154                    }
155                }
156                "global" => flag.global = child.arg(0)?.ensure_bool()?,
157                "count" => flag.count = child.arg(0)?.ensure_bool()?,
158                "allow_hyphen_values" => {
159                    allow_hyphen_values = child.arg(0)?.ensure_bool()?;
160                }
161                "default" => {
162                    // Support both single value and multiple values
163                    // default "bar"            -> vec!["bar"]
164                    // default #true            -> vec!["true"]
165                    // default { "xyz"; "bar" } -> vec!["xyz", "bar"]
166                    let children = child.children();
167                    if children.is_empty() {
168                        // Single value: default "bar" or default #true
169                        let arg = child.arg(0)?;
170                        let default_value = match arg.value.as_bool() {
171                            Some(b) => b.to_string(),
172                            None => arg.ensure_string()?,
173                        };
174                        flag.default = vec![default_value];
175                    } else {
176                        // Multiple values from children: default { "xyz"; "bar" }
177                        // In KDL, these are child nodes where the string is the node name
178                        flag.default = children.iter().map(|c| c.name().to_string()).collect();
179                    }
180                }
181                "env" => flag.env = child.arg(0)?.ensure_string().map(Some)?,
182                "choices" => {
183                    if let Some(arg) = &mut flag.arg {
184                        arg.choices = Some(SpecChoices::parse(ctx, &child)?);
185                    } else {
186                        bail_parse!(
187                            ctx,
188                            child.node.name().span(),
189                            "flag must have value to have choices"
190                        )
191                    }
192                }
193                k => bail_parse!(ctx, child.node.name().span(), "unsupported flag child {k}"),
194            }
195        }
196        if allow_hyphen_values {
197            flag.set_allow_hyphen_values(ctx, node.node.name().span(), true)?;
198        }
199        flag.usage = flag.usage();
200        flag.help_first_line = flag.help.as_ref().map(|s| string::first_line(s));
201        Ok(flag)
202    }
203    pub fn allow_hyphen_values(&self) -> bool {
204        self.arg
205            .as_ref()
206            .is_some_and(|arg| arg.double_dash == SpecDoubleDashChoices::Automatic)
207    }
208
209    pub(crate) fn set_allow_hyphen_values(
210        &mut self,
211        ctx: &ParsingContext,
212        span: miette::SourceSpan,
213        allow: bool,
214    ) -> Result<()> {
215        if let Some(arg) = &mut self.arg {
216            arg.double_dash = if allow {
217                SpecDoubleDashChoices::Automatic
218            } else if arg.double_dash == SpecDoubleDashChoices::Automatic {
219                SpecDoubleDashChoices::Optional
220            } else {
221                arg.double_dash.clone()
222            };
223            Ok(())
224        } else if allow {
225            bail_parse!(ctx, span, "flag must have value to allow hyphen values")
226        } else {
227            Ok(())
228        }
229    }
230
231    pub fn usage(&self) -> String {
232        let mut parts = vec![];
233        let name = get_name_from_short_and_long(&self.short, &self.long).unwrap_or_default();
234        if name != self.name {
235            parts.push(format!("{}:", self.name));
236        }
237        if let Some(short) = self.short.first() {
238            parts.push(format!("-{short}"));
239        }
240        if let Some(long) = self.long.first() {
241            parts.push(format!("--{long}"));
242        }
243        let mut out = parts.join(" ");
244        if self.var {
245            out = format!("{out}…");
246        }
247        if let Some(arg) = &self.arg {
248            out = format!("{} {}", out, arg.usage());
249        }
250        out
251    }
252}
253
254impl From<&SpecFlag> for KdlNode {
255    fn from(flag: &SpecFlag) -> KdlNode {
256        let mut node = KdlNode::new("flag");
257        let name = flag
258            .short
259            .iter()
260            .map(|c| format!("-{c}"))
261            .chain(flag.long.iter().map(|s| format!("--{s}")))
262            .collect_vec()
263            .join(" ");
264        node.push(KdlEntry::new(name));
265        if let Some(desc) = &flag.help {
266            node.push(string_entry(Some("help"), desc));
267        }
268        if let Some(desc) = &flag.help_long {
269            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
270            let mut node = KdlNode::new("long_help");
271            node.push(string_entry(None, desc));
272            children.nodes_mut().push(node);
273        }
274        if let Some(desc) = &flag.help_md {
275            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
276            let mut node = KdlNode::new("help_md");
277            node.push(string_entry(None, desc));
278            children.nodes_mut().push(node);
279        }
280        if flag.required {
281            node.push(KdlEntry::new_prop("required", true));
282        }
283        if flag.var {
284            node.push(KdlEntry::new_prop("var", true));
285        }
286        if let Some(var_min) = flag.var_min {
287            node.push(KdlEntry::new_prop("var_min", var_min as i128));
288        }
289        if let Some(var_max) = flag.var_max {
290            node.push(KdlEntry::new_prop("var_max", var_max as i128));
291        }
292        if flag.hide {
293            node.push(KdlEntry::new_prop("hide", true));
294        }
295        if flag.global {
296            node.push(KdlEntry::new_prop("global", true));
297        }
298        if flag.count {
299            node.push(KdlEntry::new_prop("count", true));
300        }
301        if flag.allow_hyphen_values() {
302            node.push(KdlEntry::new_prop("allow_hyphen_values", true));
303        }
304        if let Some(negate) = &flag.negate {
305            node.push(string_entry(Some("negate"), negate));
306        }
307        if let Some(env) = &flag.env {
308            node.push(string_entry(Some("env"), env));
309        }
310        if let Some(deprecated) = &flag.deprecated {
311            node.push(string_entry(Some("deprecated"), deprecated));
312        }
313        // Serialize default values
314        if !flag.default.is_empty() {
315            if flag.default.len() == 1 {
316                // Single value: use property default="bar"
317                node.push(KdlEntry::new_prop("default", flag.default[0].clone()));
318            } else {
319                // Multiple values: use child node default { "xyz"; "bar" }
320                let children = node.children_mut().get_or_insert_with(KdlDocument::new);
321                let mut default_node = KdlNode::new("default");
322                let default_children = default_node
323                    .children_mut()
324                    .get_or_insert_with(KdlDocument::new);
325                for val in &flag.default {
326                    default_children
327                        .nodes_mut()
328                        .push(KdlNode::new(val.as_str()));
329                }
330                children.nodes_mut().push(default_node);
331            }
332        }
333        if let Some(arg) = &flag.arg {
334            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
335            if flag.allow_hyphen_values() {
336                let mut arg = arg.clone();
337                arg.double_dash = SpecDoubleDashChoices::Optional;
338                children.nodes_mut().push((&arg).into());
339            } else {
340                children.nodes_mut().push(arg.into());
341            }
342        }
343        node
344    }
345}
346
347impl FromStr for SpecFlag {
348    type Err = UsageErr;
349    fn from_str(input: &str) -> Result<Self> {
350        let mut flag = Self::default();
351        let input = input.replace("...", "…").replace("…", " … ");
352        for part in input.split_whitespace() {
353            if let Some(name) = part.strip_suffix(':') {
354                flag.name = name.to_string();
355            } else if let Some(long) = part.strip_prefix("--") {
356                flag.long.push(long.to_string());
357            } else if let Some(short) = part.strip_prefix('-') {
358                if short.len() != 1 {
359                    return Err(InvalidFlag {
360                        token: format!("-{short}"),
361                        reason: "short flags must be a single character (use -- for long flags)"
362                            .to_string(),
363                        span: (0, input.len()).into(),
364                        input: input.to_string(),
365                    });
366                }
367                flag.short.push(short.chars().next().unwrap());
368            } else if part == "…" {
369                if let Some(arg) = &mut flag.arg {
370                    arg.var = true;
371                } else {
372                    flag.var = true;
373                }
374            } else if part.starts_with('<') && part.ends_with('>')
375                || part.starts_with('[') && part.ends_with(']')
376            {
377                flag.arg = Some(part.to_string().parse()?);
378            } else {
379                return Err(InvalidFlag {
380                    token: part.to_string(),
381                    reason: "unexpected token (expected -x, --long, <arg>, or [arg])".to_string(),
382                    span: (0, input.len()).into(),
383                    input: input.to_string(),
384                });
385            }
386        }
387        if flag.name.is_empty() {
388            flag.name = get_name_from_short_and_long(&flag.short, &flag.long).unwrap_or_default();
389        }
390        flag.usage = flag.usage();
391        Ok(flag)
392    }
393}
394
395#[cfg(feature = "clap")]
396impl From<&clap::Arg> for SpecFlag {
397    fn from(c: &clap::Arg) -> Self {
398        let required = c.is_required_set();
399        let help = c.get_help().map(|s| s.to_string());
400        let help_long = c.get_long_help().map(|s| s.to_string());
401        let help_first_line = help.as_ref().map(|s| string::first_line(s));
402        let hide = c.is_hide_set();
403        let var = matches!(
404            c.get_action(),
405            clap::ArgAction::Count | clap::ArgAction::Append
406        );
407        let default: Vec<String> = c
408            .get_default_values()
409            .iter()
410            .map(|s| s.to_string_lossy().to_string())
411            .collect();
412        let short = c.get_short_and_visible_aliases().unwrap_or_default();
413        let long = c
414            .get_long_and_visible_aliases()
415            .unwrap_or_default()
416            .into_iter()
417            .map(|s| s.to_string())
418            .collect::<Vec<_>>();
419        let name = get_name_from_short_and_long(&short, &long).unwrap_or_default();
420        let arg = if let clap::ArgAction::Set | clap::ArgAction::Append = c.get_action() {
421            let mut arg = SpecArg::from(
422                c.get_value_names()
423                    .map(|s| s.iter().map(|s| s.to_string()).join(" "))
424                    .unwrap_or(name.clone())
425                    .as_str(),
426            );
427
428            let choices = c
429                .get_possible_values()
430                .iter()
431                .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
432                .collect::<Vec<_>>();
433            if !choices.is_empty() {
434                arg.choices = Some(SpecChoices {
435                    choices,
436                    ..Default::default()
437                });
438            }
439
440            Some(arg)
441        } else {
442            None
443        };
444        let mut flag = Self {
445            name,
446            usage: "".into(),
447            short,
448            long,
449            required,
450            help,
451            help_long,
452            help_md: None,
453            help_first_line,
454            var,
455            var_min: None,
456            var_max: None,
457            hide,
458            global: c.is_global_set(),
459            arg,
460            count: matches!(c.get_action(), clap::ArgAction::Count),
461            default,
462            deprecated: None,
463            negate: None,
464            env: None,
465        };
466        if c.is_allow_hyphen_values_set() {
467            if let Some(arg) = &mut flag.arg {
468                arg.double_dash = SpecDoubleDashChoices::Automatic;
469            }
470        }
471        flag
472    }
473}
474
475// #[cfg(feature = "clap")]
476// impl From<&SpecFlag> for clap::Arg {
477//     fn from(flag: &SpecFlag) -> Self {
478//         let mut a = clap::Arg::new(&flag.name);
479//         if let Some(desc) = &flag.help {
480//             a = a.help(desc);
481//         }
482//         if flag.required {
483//             a = a.required(true);
484//         }
485//         if let Some(arg) = &flag.arg {
486//             a = a.value_name(&arg.name);
487//             if arg.var {
488//                 a = a.action(clap::ArgAction::Append)
489//             } else {
490//                 a = a.action(clap::ArgAction::Set)
491//             }
492//         } else {
493//             a = a.action(clap::ArgAction::SetTrue)
494//         }
495//         // let mut a = clap::Arg::new(&flag.name)
496//         //     .required(flag.required)
497//         //     .action(clap::ArgAction::SetTrue);
498//         if let Some(short) = flag.short.first() {
499//             a = a.short(*short);
500//         }
501//         if let Some(long) = flag.long.first() {
502//             a = a.long(long);
503//         }
504//         for short in flag.short.iter().skip(1) {
505//             a = a.visible_short_alias(*short);
506//         }
507//         for long in flag.long.iter().skip(1) {
508//             a = a.visible_alias(long);
509//         }
510//         // cmd = cmd.arg(a);
511//         // if flag.multiple {
512//         //     a = a.multiple(true);
513//         // }
514//         // if flag.hide {
515//         //     a = a.hide_possible_values(true);
516//         // }
517//         a
518//     }
519// }
520
521impl Display for SpecFlag {
522    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523        write!(f, "{}", self.usage())
524    }
525}
526impl PartialEq for SpecFlag {
527    fn eq(&self, other: &Self) -> bool {
528        self.name == other.name
529    }
530}
531impl Eq for SpecFlag {}
532impl Hash for SpecFlag {
533    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
534        self.name.hash(state);
535    }
536}
537
538fn get_name_from_short_and_long(short: &[char], long: &[String]) -> Option<String> {
539    long.first()
540        .map(|s| s.to_string())
541        .or_else(|| short.first().map(|c| c.to_string()))
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::Spec;
548    use insta::assert_snapshot;
549
550    #[test]
551    fn from_str() {
552        assert_snapshot!("-f".parse::<SpecFlag>().unwrap(), @"-f");
553        assert_snapshot!("--flag".parse::<SpecFlag>().unwrap(), @"--flag");
554        assert_snapshot!("-f --flag".parse::<SpecFlag>().unwrap(), @"-f --flag");
555        assert_snapshot!("-f --flag…".parse::<SpecFlag>().unwrap(), @"-f --flag…");
556        assert_snapshot!("-f --flag …".parse::<SpecFlag>().unwrap(), @"-f --flag…");
557        assert_snapshot!("--flag <arg>".parse::<SpecFlag>().unwrap(), @"--flag <arg>");
558        assert_snapshot!("-f --flag <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>");
559        assert_snapshot!("-f --flag… <arg>".parse::<SpecFlag>().unwrap(), @"-f --flag… <arg>");
560        assert_snapshot!("-f --flag <arg>…".parse::<SpecFlag>().unwrap(), @"-f --flag <arg>…");
561        assert_snapshot!("myflag: -f".parse::<SpecFlag>().unwrap(), @"myflag: -f");
562        assert_snapshot!("myflag: -f --flag <arg>".parse::<SpecFlag>().unwrap(), @"myflag: -f --flag <arg>");
563    }
564
565    #[test]
566    fn test_flag_with_env() {
567        let spec = Spec::parse(
568            &Default::default(),
569            r#"
570flag "--color" env="MYCLI_COLOR" help="Enable color output"
571flag "--verbose" env="MYCLI_VERBOSE"
572            "#,
573        )
574        .unwrap();
575
576        assert_snapshot!(spec, @r#"
577        flag --color help="Enable color output" env=MYCLI_COLOR
578        flag --verbose env=MYCLI_VERBOSE
579        "#);
580
581        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
582        assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
583
584        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
585        assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
586    }
587
588    #[test]
589    fn test_flag_with_env_child_node() {
590        let spec = Spec::parse(
591            &Default::default(),
592            r#"
593flag "--color" help="Enable color output" {
594    env "MYCLI_COLOR"
595}
596flag "--verbose" {
597    env "MYCLI_VERBOSE"
598}
599            "#,
600        )
601        .unwrap();
602
603        assert_snapshot!(spec, @r#"
604        flag --color help="Enable color output" env=MYCLI_COLOR
605        flag --verbose env=MYCLI_VERBOSE
606        "#);
607
608        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
609        assert_eq!(color_flag.env, Some("MYCLI_COLOR".to_string()));
610
611        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
612        assert_eq!(verbose_flag.env, Some("MYCLI_VERBOSE".to_string()));
613    }
614
615    #[test]
616    fn test_flag_with_boolean_defaults() {
617        let spec = Spec::parse(
618            &Default::default(),
619            r#"
620flag "--color" default=#true
621flag "--verbose" default=#false
622flag "--debug" default="true"
623flag "--quiet" default="false"
624            "#,
625        )
626        .unwrap();
627
628        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
629        assert_eq!(color_flag.default, vec!["true".to_string()]);
630
631        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
632        assert_eq!(verbose_flag.default, vec!["false".to_string()]);
633
634        let debug_flag = spec.cmd.flags.iter().find(|f| f.name == "debug").unwrap();
635        assert_eq!(debug_flag.default, vec!["true".to_string()]);
636
637        let quiet_flag = spec.cmd.flags.iter().find(|f| f.name == "quiet").unwrap();
638        assert_eq!(quiet_flag.default, vec!["false".to_string()]);
639    }
640
641    #[test]
642    fn test_flag_with_boolean_defaults_child_node() {
643        let spec = Spec::parse(
644            &Default::default(),
645            r#"
646flag "--color" {
647    default #true
648}
649flag "--verbose" {
650    default #false
651}
652            "#,
653        )
654        .unwrap();
655
656        let color_flag = spec.cmd.flags.iter().find(|f| f.name == "color").unwrap();
657        assert_eq!(color_flag.default, vec!["true".to_string()]);
658
659        let verbose_flag = spec.cmd.flags.iter().find(|f| f.name == "verbose").unwrap();
660        assert_eq!(verbose_flag.default, vec!["false".to_string()]);
661    }
662
663    #[test]
664    fn test_flag_with_single_default() {
665        let spec = Spec::parse(
666            &Default::default(),
667            r#"
668flag "--foo <foo>" var=#true default="bar"
669            "#,
670        )
671        .unwrap();
672
673        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
674        assert!(flag.var);
675        assert_eq!(flag.default, vec!["bar".to_string()]);
676    }
677
678    #[test]
679    fn test_flag_with_multiple_defaults_child_node() {
680        let spec = Spec::parse(
681            &Default::default(),
682            r#"
683flag "--foo <foo>" var=#true {
684    default {
685        "xyz"
686        "bar"
687    }
688}
689            "#,
690        )
691        .unwrap();
692
693        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
694        assert!(flag.var);
695        assert_eq!(flag.default, vec!["xyz".to_string(), "bar".to_string()]);
696    }
697
698    #[test]
699    fn test_flag_with_single_default_child_node() {
700        let spec = Spec::parse(
701            &Default::default(),
702            r#"
703flag "--foo <foo>" var=#true {
704    default "bar"
705}
706            "#,
707        )
708        .unwrap();
709
710        let flag = spec.cmd.flags.iter().find(|f| f.name == "foo").unwrap();
711        assert!(flag.var);
712        assert_eq!(flag.default, vec!["bar".to_string()]);
713    }
714
715    #[test]
716    fn test_flag_default_serialization_single() {
717        let spec = Spec::parse(
718            &Default::default(),
719            r#"
720flag "--foo <foo>" default="bar"
721            "#,
722        )
723        .unwrap();
724
725        // When serialized, single default should use property format
726        let output = spec.to_string();
727        assert!(output.contains("default=bar") || output.contains(r#"default="bar""#));
728    }
729
730    #[test]
731    fn test_flag_default_serialization_multiple() {
732        let spec = Spec::parse(
733            &Default::default(),
734            r#"
735flag "--foo <foo>" var=#true {
736    default {
737        "xyz"
738        "bar"
739    }
740}
741            "#,
742        )
743        .unwrap();
744
745        // When serialized, multiple defaults should use child node format
746        let output = spec.to_string();
747        // The output should contain a default block with children
748        assert!(output.contains("default {"));
749    }
750}