usage/spec/
arg.rs

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