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