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