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 Preserve,
26}
27
28#[derive(Debug, Default, Clone, Serialize)]
45pub struct SpecArg {
46 pub name: String,
48 pub usage: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub help: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub help_long: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub help_md: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub help_first_line: Option<String>,
62 pub required: bool,
64 pub double_dash: SpecDoubleDashChoices,
66 #[serde(skip_serializing_if = "is_false")]
68 pub var: bool,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub var_min: Option<usize>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub var_max: Option<usize>,
75 pub hide: bool,
77 #[serde(skip_serializing_if = "Vec::is_empty")]
79 pub default: Vec<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub choices: Option<SpecChoices>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub env: Option<String>,
86}
87
88impl SpecArg {
89 pub fn builder() -> SpecArgBuilder {
91 SpecArgBuilder::new()
92 }
93
94 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
95 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
96 for (k, v) in node.props() {
97 match k {
98 "help" => arg.help = Some(v.ensure_string()?),
99 "long_help" => arg.help_long = Some(v.ensure_string()?),
100 "help_long" => arg.help_long = Some(v.ensure_string()?),
101 "help_md" => arg.help_md = Some(v.ensure_string()?),
102 "required" => arg.required = v.ensure_bool()?,
103 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
104 "var" => arg.var = v.ensure_bool()?,
105 "hide" => arg.hide = v.ensure_bool()?,
106 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
107 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
108 "default" => arg.default = vec![v.ensure_string()?],
109 "env" => arg.env = v.ensure_string().map(Some)?,
110 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
111 }
112 }
113 if !arg.default.is_empty() {
114 arg.required = false;
115 }
116 for child in node.children() {
117 match child.name() {
118 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
119 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
120 "default" => {
121 let children = child.children();
125 if children.is_empty() {
126 arg.default = vec![child.arg(0)?.ensure_string()?];
128 } else {
129 arg.default = children.iter().map(|c| c.name().to_string()).collect();
132 }
133 }
134 "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
135 "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
136 "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
137 "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
138 "required" => arg.required = child.arg(0)?.ensure_bool()?,
139 "var" => arg.var = child.arg(0)?.ensure_bool()?,
140 "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
141 "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
142 "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
143 "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
144 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
145 }
146 }
147 arg.usage = arg.usage();
148 if let Some(help) = &arg.help {
149 arg.help_first_line = Some(string::first_line(help));
150 }
151 Ok(arg)
152 }
153}
154
155impl SpecArg {
156 pub fn usage(&self) -> String {
157 let name = if self.double_dash == SpecDoubleDashChoices::Required {
158 format!("-- {}", self.name)
159 } else {
160 self.name.clone()
161 };
162 let mut name = if self.required {
163 format!("<{name}>")
164 } else {
165 format!("[{name}]")
166 };
167 if self.var {
168 name = format!("{name}…");
169 }
170 name
171 }
172}
173
174impl From<&SpecArg> for KdlNode {
175 fn from(arg: &SpecArg) -> Self {
176 let mut node = KdlNode::new("arg");
177 node.push(KdlEntry::new(arg.usage()));
178 if let Some(desc) = &arg.help {
179 node.push(KdlEntry::new_prop("help", desc.clone()));
180 }
181 if let Some(desc) = &arg.help_long {
182 node.push(KdlEntry::new_prop("help_long", desc.clone()));
183 }
184 if let Some(desc) = &arg.help_md {
185 node.push(KdlEntry::new_prop("help_md", desc.clone()));
186 }
187 if !arg.required {
188 node.push(KdlEntry::new_prop("required", false));
189 }
190 if arg.double_dash == SpecDoubleDashChoices::Automatic
191 || arg.double_dash == SpecDoubleDashChoices::Preserve
192 {
193 node.push(KdlEntry::new_prop(
194 "double_dash",
195 arg.double_dash.to_string(),
196 ));
197 }
198 if arg.var {
199 node.push(KdlEntry::new_prop("var", true));
200 }
201 if let Some(min) = arg.var_min {
202 node.push(KdlEntry::new_prop("var_min", min as i128));
203 }
204 if let Some(max) = arg.var_max {
205 node.push(KdlEntry::new_prop("var_max", max as i128));
206 }
207 if arg.hide {
208 node.push(KdlEntry::new_prop("hide", true));
209 }
210 if !arg.default.is_empty() {
212 if arg.default.len() == 1 {
213 node.push(KdlEntry::new_prop("default", arg.default[0].clone()));
215 } else {
216 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
218 let mut default_node = KdlNode::new("default");
219 let default_children = default_node
220 .children_mut()
221 .get_or_insert_with(KdlDocument::new);
222 for val in &arg.default {
223 default_children
224 .nodes_mut()
225 .push(KdlNode::new(val.as_str()));
226 }
227 children.nodes_mut().push(default_node);
228 }
229 }
230 if let Some(env) = &arg.env {
231 node.push(KdlEntry::new_prop("env", env.clone()));
232 }
233 if let Some(choices) = &arg.choices {
234 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
235 children.nodes_mut().push(choices.into());
236 }
237 node
238 }
239}
240
241impl From<&str> for SpecArg {
242 fn from(input: &str) -> Self {
243 let mut arg = SpecArg {
244 name: input.to_string(),
245 required: true,
246 ..Default::default()
247 };
248 if let Some(name) = arg
250 .name
251 .strip_suffix("...")
252 .or_else(|| arg.name.strip_suffix("…"))
253 {
254 arg.var = true;
255 arg.name = name.to_string();
256 }
257 let first = arg.name.chars().next().unwrap_or_default();
258 let last = arg.name.chars().last().unwrap_or_default();
259 match (first, last) {
260 ('[', ']') => {
261 arg.name = arg.name[1..arg.name.len() - 1].to_string();
262 arg.required = false;
263 }
264 ('<', '>') => {
265 arg.name = arg.name[1..arg.name.len() - 1].to_string();
266 }
267 _ => {}
268 }
269 if !arg.var {
271 if let Some(name) = arg
272 .name
273 .strip_suffix("...")
274 .or_else(|| arg.name.strip_suffix("…"))
275 {
276 arg.var = true;
277 arg.name = name.to_string();
278 }
279 }
280 if let Some(name) = arg.name.strip_prefix("-- ") {
281 arg.double_dash = SpecDoubleDashChoices::Required;
282 arg.name = name.to_string();
283 }
284 arg
285 }
286}
287impl FromStr for SpecArg {
288 type Err = UsageErr;
289 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
290 Ok(input.into())
291 }
292}
293
294#[cfg(feature = "clap")]
295impl From<&clap::Arg> for SpecArg {
296 fn from(arg: &clap::Arg) -> Self {
297 let required = arg.is_required_set();
298 let help = arg.get_help().map(|s| s.to_string());
299 let help_long = arg.get_long_help().map(|s| s.to_string());
300 let help_first_line = help.as_ref().map(|s| string::first_line(s));
301 let hide = arg.is_hide_set();
302 let var = matches!(
303 arg.get_action(),
304 clap::ArgAction::Count | clap::ArgAction::Append
305 );
306 let choices = arg
307 .get_possible_values()
308 .iter()
309 .flat_map(|v| v.get_name_and_aliases().map(|s| s.to_string()))
310 .collect::<Vec<_>>();
311 let mut arg = Self {
312 name: arg
313 .get_value_names()
314 .unwrap_or_default()
315 .first()
316 .cloned()
317 .unwrap_or_default()
318 .to_string(),
319 usage: "".into(),
320 required,
321 double_dash: if arg.is_last_set() {
322 SpecDoubleDashChoices::Required
323 } else if arg.is_trailing_var_arg_set() {
324 SpecDoubleDashChoices::Automatic
325 } else {
326 SpecDoubleDashChoices::Optional
327 },
328 help,
329 help_long,
330 help_md: None,
331 help_first_line,
332 var,
333 var_max: None,
334 var_min: None,
335 hide,
336 default: arg
337 .get_default_values()
338 .iter()
339 .map(|v| v.to_string_lossy().to_string())
340 .collect(),
341 choices: None,
342 env: None,
343 };
344 if !choices.is_empty() {
345 arg.choices = Some(SpecChoices {
346 choices,
347 ..Default::default()
348 });
349 }
350
351 arg
352 }
353}
354
355impl Display for SpecArg {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 write!(f, "{}", self.usage())
358 }
359}
360impl PartialEq for SpecArg {
361 fn eq(&self, other: &Self) -> bool {
362 self.name == other.name
363 }
364}
365impl Eq for SpecArg {}
366impl Hash for SpecArg {
367 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
368 self.name.hash(state);
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use crate::Spec;
375 use insta::assert_snapshot;
376
377 #[test]
378 fn test_arg_with_env() {
379 let spec = Spec::parse(
380 &Default::default(),
381 r#"
382arg "<input>" env="MY_INPUT" help="Input file"
383arg "<output>" env="MY_OUTPUT"
384 "#,
385 )
386 .unwrap();
387
388 assert_snapshot!(spec, @r#"
389 arg <input> help="Input file" env=MY_INPUT
390 arg <output> env=MY_OUTPUT
391 "#);
392
393 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
394 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
395
396 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
397 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
398 }
399
400 #[test]
401 fn test_arg_with_env_child_node() {
402 let spec = Spec::parse(
403 &Default::default(),
404 r#"
405arg "<input>" help="Input file" {
406 env "MY_INPUT"
407}
408arg "<output>" {
409 env "MY_OUTPUT"
410}
411 "#,
412 )
413 .unwrap();
414
415 assert_snapshot!(spec, @r#"
416 arg <input> help="Input file" env=MY_INPUT
417 arg <output> env=MY_OUTPUT
418 "#);
419
420 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
421 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
422
423 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
424 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
425 }
426
427 #[test]
428 fn test_arg_variadic_syntax() {
429 use crate::SpecArg;
430
431 let arg: SpecArg = "<files>...".into();
433 assert_eq!(arg.name, "files");
434 assert!(arg.var);
435 assert!(arg.required);
436
437 let arg: SpecArg = "[files]...".into();
439 assert_eq!(arg.name, "files");
440 assert!(arg.var);
441 assert!(!arg.required);
442
443 let arg: SpecArg = "<files>…".into();
445 assert_eq!(arg.name, "files");
446 assert!(arg.var);
447
448 let arg: SpecArg = "[files]…".into();
449 assert_eq!(arg.name, "files");
450 assert!(arg.var);
451 assert!(!arg.required);
452
453 let arg: SpecArg = "[args...]".into();
455 assert_eq!(arg.name, "args");
456 assert!(arg.var);
457 assert!(!arg.required);
458
459 let arg: SpecArg = "<args...>".into();
460 assert_eq!(arg.name, "args");
461 assert!(arg.var);
462 assert!(arg.required);
463
464 let arg: SpecArg = "[args…]".into();
466 assert_eq!(arg.name, "args");
467 assert!(arg.var);
468 assert!(!arg.required);
469 }
470
471 #[test]
472 fn test_arg_child_nodes() {
473 let spec = Spec::parse(
474 &Default::default(),
475 r#"
476arg "<environment>" {
477 help "Deployment environment"
478 choices "dev" "staging" "prod"
479}
480arg "[services]" {
481 help "Services to deploy"
482 var #true
483 var_min 0
484}
485 "#,
486 )
487 .unwrap();
488
489 let env_arg = spec
490 .cmd
491 .args
492 .iter()
493 .find(|a| a.name == "environment")
494 .unwrap();
495 assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
496 assert!(env_arg.choices.is_some());
497
498 let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
499 assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
500 assert!(svc_arg.var);
501 assert_eq!(svc_arg.var_min, Some(0));
502 }
503
504 #[test]
505 fn test_arg_long_help_child_node() {
506 let spec = Spec::parse(
507 &Default::default(),
508 r#"
509arg "<input>" {
510 help "Input file"
511 long_help "Extended help text for input"
512}
513 "#,
514 )
515 .unwrap();
516
517 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
518 assert_eq!(input_arg.help, Some("Input file".to_string()));
519 assert_eq!(
520 input_arg.help_long,
521 Some("Extended help text for input".to_string())
522 );
523 }
524}