usage/spec/
arg.rs

1use kdl::{KdlDocument, KdlEntry, KdlNode};
2use serde::Serialize;
3use std::fmt::Display;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use crate::error::UsageErr;
8use crate::spec::builder::SpecArgBuilder;
9use crate::spec::context::ParsingContext;
10use crate::spec::helpers::NodeHelper;
11use crate::spec::is_false;
12use crate::{string, SpecChoices};
13
14#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, strum::EnumString, strum::Display)]
15#[strum(serialize_all = "snake_case")]
16pub enum SpecDoubleDashChoices {
17    /// Once an arg is entered, behave as if "--" was passed
18    Automatic,
19    /// Allow "--" to be passed
20    #[default]
21    Optional,
22    /// Require "--" to be passed
23    Required,
24}
25
26#[derive(Debug, Default, Clone, Serialize)]
27pub struct SpecArg {
28    pub name: String,
29    pub usage: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub help: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub help_long: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub help_md: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub help_first_line: Option<String>,
38    pub required: bool,
39    pub double_dash: SpecDoubleDashChoices,
40    #[serde(skip_serializing_if = "is_false")]
41    pub var: bool,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub var_min: Option<usize>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub var_max: Option<usize>,
46    pub hide: bool,
47    #[serde(skip_serializing_if = "Vec::is_empty")]
48    pub default: Vec<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub choices: Option<SpecChoices>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub env: Option<String>,
53}
54
55impl SpecArg {
56    /// Create a new builder for SpecArg
57    pub fn builder() -> SpecArgBuilder {
58        SpecArgBuilder::new()
59    }
60
61    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
62        let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
63        for (k, v) in node.props() {
64            match k {
65                "help" => arg.help = Some(v.ensure_string()?),
66                "long_help" => arg.help_long = Some(v.ensure_string()?),
67                "help_long" => arg.help_long = Some(v.ensure_string()?),
68                "help_md" => arg.help_md = Some(v.ensure_string()?),
69                "required" => arg.required = v.ensure_bool()?,
70                "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
71                "var" => arg.var = v.ensure_bool()?,
72                "hide" => arg.hide = v.ensure_bool()?,
73                "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
74                "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
75                "default" => arg.default = vec![v.ensure_string()?],
76                "env" => arg.env = v.ensure_string().map(Some)?,
77                k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
78            }
79        }
80        if !arg.default.is_empty() {
81            arg.required = false;
82        }
83        for child in node.children() {
84            match child.name() {
85                "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
86                "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
87                "default" => {
88                    // Support both single value and multiple values
89                    // default "bar"            -> vec!["bar"]
90                    // default { "xyz"; "bar" } -> vec!["xyz", "bar"]
91                    let children = child.children();
92                    if children.is_empty() {
93                        // Single value: default "bar"
94                        arg.default = vec![child.arg(0)?.ensure_string()?];
95                    } else {
96                        // Multiple values from children: default { "xyz"; "bar" }
97                        // In KDL, these are child nodes where the string is the node name
98                        arg.default = children.iter().map(|c| c.name().to_string()).collect();
99                    }
100                }
101                k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
102            }
103        }
104        arg.usage = arg.usage();
105        if let Some(help) = &arg.help {
106            arg.help_first_line = Some(string::first_line(help));
107        }
108        Ok(arg)
109    }
110}
111
112impl SpecArg {
113    pub fn usage(&self) -> String {
114        let name = if self.double_dash == SpecDoubleDashChoices::Required {
115            format!("-- {}", self.name)
116        } else {
117            self.name.clone()
118        };
119        let mut name = if self.required {
120            format!("<{name}>")
121        } else {
122            format!("[{name}]")
123        };
124        if self.var {
125            name = format!("{name}…");
126        }
127        name
128    }
129}
130
131impl From<&SpecArg> for KdlNode {
132    fn from(arg: &SpecArg) -> Self {
133        let mut node = KdlNode::new("arg");
134        node.push(KdlEntry::new(arg.usage()));
135        if let Some(desc) = &arg.help {
136            node.push(KdlEntry::new_prop("help", desc.clone()));
137        }
138        if let Some(desc) = &arg.help_long {
139            node.push(KdlEntry::new_prop("help_long", desc.clone()));
140        }
141        if let Some(desc) = &arg.help_md {
142            node.push(KdlEntry::new_prop("help_md", desc.clone()));
143        }
144        if !arg.required {
145            node.push(KdlEntry::new_prop("required", false));
146        }
147        if arg.double_dash == SpecDoubleDashChoices::Automatic {
148            node.push(KdlEntry::new_prop(
149                "double_dash",
150                arg.double_dash.to_string(),
151            ));
152        }
153        if arg.var {
154            node.push(KdlEntry::new_prop("var", true));
155        }
156        if let Some(min) = arg.var_min {
157            node.push(KdlEntry::new_prop("var_min", min as i128));
158        }
159        if let Some(max) = arg.var_max {
160            node.push(KdlEntry::new_prop("var_max", max as i128));
161        }
162        if arg.hide {
163            node.push(KdlEntry::new_prop("hide", true));
164        }
165        // Serialize default values
166        if !arg.default.is_empty() {
167            if arg.default.len() == 1 {
168                // Single value: use property default="bar"
169                node.push(KdlEntry::new_prop("default", arg.default[0].clone()));
170            } else {
171                // Multiple values: use child node default { "xyz"; "bar" }
172                let children = node.children_mut().get_or_insert_with(KdlDocument::new);
173                let mut default_node = KdlNode::new("default");
174                let default_children = default_node
175                    .children_mut()
176                    .get_or_insert_with(KdlDocument::new);
177                for val in &arg.default {
178                    default_children
179                        .nodes_mut()
180                        .push(KdlNode::new(val.as_str()));
181                }
182                children.nodes_mut().push(default_node);
183            }
184        }
185        if let Some(env) = &arg.env {
186            node.push(KdlEntry::new_prop("env", env.clone()));
187        }
188        if let Some(choices) = &arg.choices {
189            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
190            children.nodes_mut().push(choices.into());
191        }
192        node
193    }
194}
195
196impl From<&str> for SpecArg {
197    fn from(input: &str) -> Self {
198        let mut arg = SpecArg {
199            name: input.to_string(),
200            required: true,
201            ..Default::default()
202        };
203        // Handle trailing ellipsis: "foo..." or "foo…" or "<foo>..." or "[foo]..."
204        if let Some(name) = arg
205            .name
206            .strip_suffix("...")
207            .or_else(|| arg.name.strip_suffix("…"))
208        {
209            arg.var = true;
210            arg.name = name.to_string();
211        }
212        let first = arg.name.chars().next().unwrap_or_default();
213        let last = arg.name.chars().last().unwrap_or_default();
214        match (first, last) {
215            ('[', ']') => {
216                arg.name = arg.name[1..arg.name.len() - 1].to_string();
217                arg.required = false;
218            }
219            ('<', '>') => {
220                arg.name = arg.name[1..arg.name.len() - 1].to_string();
221            }
222            _ => {}
223        }
224        if let Some(name) = arg.name.strip_prefix("-- ") {
225            arg.double_dash = SpecDoubleDashChoices::Required;
226            arg.name = name.to_string();
227        }
228        arg
229    }
230}
231impl FromStr for SpecArg {
232    type Err = UsageErr;
233    fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
234        Ok(input.into())
235    }
236}
237
238#[cfg(feature = "clap")]
239impl From<&clap::Arg> for SpecArg {
240    fn from(arg: &clap::Arg) -> Self {
241        let required = arg.is_required_set();
242        let help = arg.get_help().map(|s| s.to_string());
243        let help_long = arg.get_long_help().map(|s| s.to_string());
244        let help_first_line = help.as_ref().map(|s| string::first_line(s));
245        let hide = arg.is_hide_set();
246        let var = matches!(
247            arg.get_action(),
248            clap::ArgAction::Count | clap::ArgAction::Append
249        );
250        let choices = arg
251            .get_possible_values()
252            .iter()
253            .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
254            .collect::<Vec<_>>();
255        let mut arg = Self {
256            name: arg
257                .get_value_names()
258                .unwrap_or_default()
259                .first()
260                .cloned()
261                .unwrap_or_default()
262                .to_string(),
263            usage: "".into(),
264            required,
265            double_dash: if arg.is_last_set() {
266                SpecDoubleDashChoices::Required
267            } else if arg.is_trailing_var_arg_set() {
268                SpecDoubleDashChoices::Automatic
269            } else {
270                SpecDoubleDashChoices::Optional
271            },
272            help,
273            help_long,
274            help_md: None,
275            help_first_line,
276            var,
277            var_max: None,
278            var_min: None,
279            hide,
280            default: arg
281                .get_default_values()
282                .iter()
283                .map(|v| v.to_string_lossy().to_string())
284                .collect(),
285            choices: None,
286            env: None,
287        };
288        if !choices.is_empty() {
289            arg.choices = Some(SpecChoices { choices });
290        }
291
292        arg
293    }
294}
295
296impl Display for SpecArg {
297    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298        write!(f, "{}", self.usage())
299    }
300}
301impl PartialEq for SpecArg {
302    fn eq(&self, other: &Self) -> bool {
303        self.name == other.name
304    }
305}
306impl Eq for SpecArg {}
307impl Hash for SpecArg {
308    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
309        self.name.hash(state);
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use crate::Spec;
316    use insta::assert_snapshot;
317
318    #[test]
319    fn test_arg_with_env() {
320        let spec = Spec::parse(
321            &Default::default(),
322            r#"
323arg "<input>" env="MY_INPUT" help="Input file"
324arg "<output>" env="MY_OUTPUT"
325            "#,
326        )
327        .unwrap();
328
329        assert_snapshot!(spec, @r#"
330        arg <input> help="Input file" env=MY_INPUT
331        arg <output> env=MY_OUTPUT
332        "#);
333
334        let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
335        assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
336
337        let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
338        assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
339    }
340
341    #[test]
342    fn test_arg_with_env_child_node() {
343        let spec = Spec::parse(
344            &Default::default(),
345            r#"
346arg "<input>" help="Input file" {
347    env "MY_INPUT"
348}
349arg "<output>" {
350    env "MY_OUTPUT"
351}
352            "#,
353        )
354        .unwrap();
355
356        assert_snapshot!(spec, @r#"
357        arg <input> help="Input file" env=MY_INPUT
358        arg <output> env=MY_OUTPUT
359        "#);
360
361        let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
362        assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
363
364        let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
365        assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
366    }
367
368    #[test]
369    fn test_arg_variadic_syntax() {
370        use crate::SpecArg;
371
372        // Trailing ellipsis with required brackets
373        let arg: SpecArg = "<files>...".into();
374        assert_eq!(arg.name, "files");
375        assert!(arg.var);
376        assert!(arg.required);
377
378        // Trailing ellipsis with optional brackets
379        let arg: SpecArg = "[files]...".into();
380        assert_eq!(arg.name, "files");
381        assert!(arg.var);
382        assert!(!arg.required);
383
384        // Unicode ellipsis
385        let arg: SpecArg = "<files>…".into();
386        assert_eq!(arg.name, "files");
387        assert!(arg.var);
388
389        let arg: SpecArg = "[files]…".into();
390        assert_eq!(arg.name, "files");
391        assert!(arg.var);
392        assert!(!arg.required);
393    }
394}