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 Automatic,
19 #[default]
21 Optional,
22 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 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 let children = child.children();
92 if children.is_empty() {
93 arg.default = vec![child.arg(0)?.ensure_string()?];
95 } else {
96 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 if !arg.default.is_empty() {
167 if arg.default.len() == 1 {
168 node.push(KdlEntry::new_prop("default", arg.default[0].clone()));
170 } else {
171 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 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 let arg: SpecArg = "<files>...".into();
374 assert_eq!(arg.name, "files");
375 assert!(arg.var);
376 assert!(arg.required);
377
378 let arg: SpecArg = "[files]...".into();
380 assert_eq!(arg.name, "files");
381 assert!(arg.var);
382 assert!(!arg.required);
383
384 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}