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