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}
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 = v.ensure_string().map(Some)?,
70                k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
71            }
72        }
73        if arg.default.is_some() {
74            arg.required = false;
75        }
76        for child in node.children() {
77            match child.name() {
78                "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
79                k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
80            }
81        }
82        arg.usage = arg.usage();
83        if let Some(help) = &arg.help {
84            arg.help_first_line = Some(string::first_line(help));
85        }
86        Ok(arg)
87    }
88}
89
90impl SpecArg {
91    pub fn usage(&self) -> String {
92        let name = if self.double_dash == SpecDoubleDashChoices::Required {
93            format!("-- {}", self.name)
94        } else {
95            self.name.clone()
96        };
97        let mut name = if self.required {
98            format!("<{}>", name)
99        } else {
100            format!("[{}]", name)
101        };
102        if self.var {
103            name = format!("{}...", name);
104        }
105        name
106    }
107}
108
109impl From<&SpecArg> for KdlNode {
110    fn from(arg: &SpecArg) -> Self {
111        let mut node = KdlNode::new("arg");
112        node.push(KdlEntry::new(arg.usage()));
113        if let Some(desc) = &arg.help {
114            node.push(KdlEntry::new_prop("help", desc.clone()));
115        }
116        if let Some(desc) = &arg.help_long {
117            node.push(KdlEntry::new_prop("help_long", desc.clone()));
118        }
119        if let Some(desc) = &arg.help_md {
120            node.push(KdlEntry::new_prop("help_md", desc.clone()));
121        }
122        if !arg.required {
123            node.push(KdlEntry::new_prop("required", false));
124        }
125        if arg.double_dash == SpecDoubleDashChoices::Automatic {
126            node.push(KdlEntry::new_prop(
127                "double_dash",
128                arg.double_dash.to_string(),
129            ));
130        }
131        if arg.var {
132            node.push(KdlEntry::new_prop("var", true));
133        }
134        if let Some(min) = arg.var_min {
135            node.push(KdlEntry::new_prop("var_min", min as i128));
136        }
137        if let Some(max) = arg.var_max {
138            node.push(KdlEntry::new_prop("var_max", max as i128));
139        }
140        if arg.hide {
141            node.push(KdlEntry::new_prop("hide", true));
142        }
143        if let Some(default) = &arg.default {
144            node.push(KdlEntry::new_prop("default", default.clone()));
145        }
146        if let Some(choices) = &arg.choices {
147            let children = node.children_mut().get_or_insert_with(KdlDocument::new);
148            children.nodes_mut().push(choices.into());
149        }
150        node
151    }
152}
153
154impl From<&str> for SpecArg {
155    fn from(input: &str) -> Self {
156        let mut arg = SpecArg {
157            name: input.to_string(),
158            required: true,
159            ..Default::default()
160        };
161        if let Some(name) = arg.name.strip_suffix("...") {
162            arg.var = true;
163            arg.name = name.to_string();
164        }
165        let first = arg.name.chars().next().unwrap_or_default();
166        let last = arg.name.chars().last().unwrap_or_default();
167        match (first, last) {
168            ('[', ']') => {
169                arg.name = arg.name[1..arg.name.len() - 1].to_string();
170                arg.required = false;
171            }
172            ('<', '>') => {
173                arg.name = arg.name[1..arg.name.len() - 1].to_string();
174            }
175            _ => {}
176        }
177        if let Some(name) = arg.name.strip_prefix("-- ") {
178            arg.double_dash = SpecDoubleDashChoices::Required;
179            arg.name = name.to_string();
180        }
181        arg
182    }
183}
184impl FromStr for SpecArg {
185    type Err = UsageErr;
186    fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
187        Ok(input.into())
188    }
189}
190
191#[cfg(feature = "clap")]
192impl From<&clap::Arg> for SpecArg {
193    fn from(arg: &clap::Arg) -> Self {
194        let required = arg.is_required_set();
195        let help = arg.get_help().map(|s| s.to_string());
196        let help_long = arg.get_long_help().map(|s| s.to_string());
197        let help_first_line = help.as_ref().map(|s| string::first_line(s));
198        let hide = arg.is_hide_set();
199        let var = matches!(
200            arg.get_action(),
201            clap::ArgAction::Count | clap::ArgAction::Append
202        );
203        let choices = arg
204            .get_possible_values()
205            .iter()
206            .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
207            .collect::<Vec<_>>();
208        let mut arg = Self {
209            name: arg
210                .get_value_names()
211                .unwrap_or_default()
212                .first()
213                .cloned()
214                .unwrap_or_default()
215                .to_string(),
216            usage: "".into(),
217            required,
218            double_dash: if arg.is_last_set() {
219                SpecDoubleDashChoices::Required
220            } else if arg.is_trailing_var_arg_set() {
221                SpecDoubleDashChoices::Automatic
222            } else {
223                SpecDoubleDashChoices::Optional
224            },
225            help,
226            help_long,
227            help_md: None,
228            help_first_line,
229            var,
230            var_max: None,
231            var_min: None,
232            hide,
233            default: if arg.get_default_values().is_empty() {
234                None
235            } else {
236                Some(
237                    arg.get_default_values()
238                        .iter()
239                        .map(|v| v.to_string_lossy().to_string())
240                        .join("|"),
241                )
242            },
243            choices: None,
244        };
245        if !choices.is_empty() {
246            arg.choices = Some(SpecChoices { choices });
247        }
248
249        arg
250    }
251}
252
253impl Display for SpecArg {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(f, "{}", self.usage())
256    }
257}
258impl PartialEq for SpecArg {
259    fn eq(&self, other: &Self) -> bool {
260        self.name == other.name
261    }
262}
263impl Eq for SpecArg {}
264impl Hash for SpecArg {
265    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
266        self.name.hash(state);
267    }
268}