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