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, SpecAdmonition, SpecAdmonitionKind, SpecChoices};
14#[cfg(feature = "clap")]
15use crate::{SpecChoice, SpecChoiceAlias};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct SpecRequiredIfEq {
20 pub selector: String,
21 pub value: String,
22}
23
24#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, strum::EnumString, strum::Display)]
25#[strum(serialize_all = "snake_case")]
26pub enum SpecDoubleDashChoices {
27 Automatic,
29 #[default]
31 Optional,
32 Required,
34 Preserve,
36}
37
38#[derive(Debug, Default, Clone, Serialize)]
55#[non_exhaustive]
56pub struct SpecArg {
57 pub name: String,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
62 pub value_names: Vec<String>,
63 pub usage: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub help: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub help_long: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub help_md: Option<String>,
74 #[serde(skip_serializing_if = "Vec::is_empty")]
76 pub admonitions: Vec<SpecAdmonition>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub help_first_line: Option<String>,
80 pub required: bool,
82 pub double_dash: SpecDoubleDashChoices,
84 #[serde(skip_serializing_if = "is_false")]
86 pub var: bool,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub var_min: Option<usize>,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub var_max: Option<usize>,
93 #[serde(skip_serializing_if = "Option::is_none")]
100 pub delimiter: Option<char>,
101 #[serde(skip_serializing_if = "is_false")]
103 pub allow_negative_numbers: bool,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub value_terminator: Option<String>,
107 pub hide: bool,
109 #[serde(skip_serializing_if = "is_false")]
111 pub hide_default_value: bool,
112 #[serde(skip_serializing_if = "is_false")]
114 pub hide_env: bool,
115 #[serde(skip_serializing_if = "is_false")]
117 pub hide_env_values: bool,
118 #[serde(skip_serializing_if = "is_false")]
120 pub hide_possible_values: bool,
121 #[serde(skip_serializing_if = "is_false")]
123 pub hide_short_help: bool,
124 #[serde(skip_serializing_if = "is_false")]
126 pub hide_long_help: bool,
127 #[serde(skip_serializing_if = "Vec::is_empty")]
132 pub conflicts: Vec<String>,
133 #[serde(skip_serializing_if = "Vec::is_empty")]
135 pub requires: Vec<String>,
136 #[serde(skip_serializing_if = "Vec::is_empty")]
138 pub required_if: Vec<String>,
139 #[serde(skip_serializing_if = "Vec::is_empty")]
141 pub required_if_eq: Vec<SpecRequiredIfEq>,
142 #[serde(skip_serializing_if = "Vec::is_empty")]
144 pub required_if_eq_all: Vec<SpecRequiredIfEq>,
145 #[serde(skip_serializing_if = "Vec::is_empty")]
147 pub required_unless: Vec<String>,
148 #[serde(skip_serializing_if = "Vec::is_empty")]
150 pub required_unless_all: Vec<String>,
151 #[serde(skip_serializing_if = "Vec::is_empty")]
153 pub default: Vec<String>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub choices: Option<SpecChoices>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub validate: Option<String>,
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub validate_error: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none")]
166 pub effect: Option<SpecCommandEffect>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub env: Option<String>,
170 #[serde(skip_serializing_if = "Vec::is_empty")]
172 pub env_fallback: Vec<String>,
173 #[serde(skip_serializing_if = "Vec::is_empty")]
175 pub deprecated_env: Vec<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
179 pub help_heading: Option<String>,
180 #[serde(skip_serializing_if = "Option::is_none")]
184 pub surface: Option<String>,
185 #[serde(skip_serializing_if = "Vec::is_empty")]
189 pub available_if: Vec<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub display_order: Option<usize>,
193}
194
195impl SpecArg {
196 pub fn builder() -> SpecArgBuilder {
198 SpecArgBuilder::new()
199 }
200
201 pub fn env_names(&self) -> impl Iterator<Item = &str> {
203 self.env
204 .iter()
205 .map(String::as_str)
206 .chain(self.env_fallback.iter().map(String::as_str))
207 .chain(self.deprecated_env.iter().map(String::as_str))
208 }
209
210 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
211 let mut arg: SpecArg = node.arg(0)?.ensure_string()?.parse()?;
212 for (k, v) in node.props() {
213 match k {
214 "help" => arg.help = Some(v.ensure_string()?),
215 "long_help" => arg.help_long = Some(v.ensure_string()?),
216 "help_long" => arg.help_long = Some(v.ensure_string()?),
217 "help_md" => arg.help_md = Some(v.ensure_string()?),
218 "required" => arg.required = v.ensure_bool()?,
219 "double_dash" => arg.double_dash = v.ensure_string()?.parse()?,
220 "var" => arg.var = v.ensure_bool()?,
221 "delimiter" => {
222 let raw = v.ensure_string()?;
223 let mut chars = raw.chars();
224 match (chars.next(), chars.next()) {
225 (Some(c), None) if c.is_ascii() => arg.delimiter = Some(c),
231 (Some(c), None) => bail_parse!(
232 ctx,
233 v.entry.span(),
234 "a delimiter is one byte, and {c:?} is more than one; use an \
235 ASCII separator"
236 ),
237 _ => bail_parse!(
238 ctx,
239 v.entry.span(),
240 "a delimiter is one character, and {raw:?} is not"
241 ),
242 }
243 }
244 "allow_negative_numbers" => arg.allow_negative_numbers = v.ensure_bool()?,
245 "value_terminator" => arg.value_terminator = v.ensure_string().map(Some)?,
246 "hide" => arg.hide = v.ensure_bool()?,
247 "hide_default_value" => arg.hide_default_value = v.ensure_bool()?,
248 "hide_env" => arg.hide_env = v.ensure_bool()?,
249 "hide_env_values" => arg.hide_env_values = v.ensure_bool()?,
250 "hide_possible_values" => arg.hide_possible_values = v.ensure_bool()?,
251 "hide_short_help" => arg.hide_short_help = v.ensure_bool()?,
252 "hide_long_help" => arg.hide_long_help = v.ensure_bool()?,
253 "conflicts" => arg.conflicts = vec![v.ensure_string()?],
254 "requires" => arg.requires = vec![v.ensure_string()?],
255 "required_if" => arg.required_if = vec![v.ensure_string()?],
256 "required_unless" => arg.required_unless = vec![v.ensure_string()?],
257 "required_unless_all" => arg.required_unless_all = vec![v.ensure_string()?],
258 "var_min" => arg.var_min = v.ensure_usize().map(Some)?,
259 "var_max" => arg.var_max = v.ensure_usize().map(Some)?,
260 "default" => arg.default = vec![v.ensure_string()?],
261 "effect" => {
262 let raw = v.ensure_string()?;
263 match raw.parse() {
264 Ok(effect) => arg.effect = Some(effect),
265 Err(_) => bail_parse!(
266 ctx,
267 v.entry.span(),
268 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
269 ),
270 }
271 }
272 "env" => arg.env = v.ensure_string().map(Some)?,
273 "env_fallback" => arg.env_fallback = vec![v.ensure_string()?],
274 "deprecated_env" => arg.deprecated_env = vec![v.ensure_string()?],
275 "validate" => arg.validate = v.ensure_string().map(Some)?,
276 "validate_error" => arg.validate_error = v.ensure_string().map(Some)?,
277 "help_heading" => arg.help_heading = v.ensure_string().map(Some)?,
278 "surface" => arg.surface = v.ensure_string().map(Some)?,
279 "available_if" => arg.available_if = vec![v.ensure_string()?],
280 "display_order" => arg.display_order = v.ensure_usize().map(Some)?,
281 k => bail_parse!(ctx, v.entry.span(), "unsupported arg key {k}"),
282 }
283 }
284 if !arg.default.is_empty() {
285 arg.required = false;
286 }
287 for child in node.children() {
288 match child.name() {
289 "choices" => arg.choices = Some(SpecChoices::parse(ctx, &child)?),
290 "effect" => {
291 let a = child.arg(0)?;
292 let raw = a.ensure_string()?;
293 match raw.parse() {
294 Ok(effect) => arg.effect = Some(effect),
295 Err(_) => bail_parse!(
296 ctx,
297 a.entry.span(),
298 "unsupported effect {raw}, expected one of: {EFFECT_VALUES}"
299 ),
300 }
301 }
302 "env" => arg.env = child.arg(0)?.ensure_string().map(Some)?,
303 "env_fallback" => arg.env_fallback = string_args(&child)?,
304 "deprecated_env" => arg.deprecated_env = string_args(&child)?,
305 "validate" => arg.validate = child.arg(0)?.ensure_string().map(Some)?,
306 "validate_error" => {
307 arg.validate_error = child.arg(0)?.ensure_string().map(Some)?;
308 }
309 "help_heading" => {
310 arg.help_heading = child.arg(0)?.ensure_string().map(Some)?;
311 }
312 "surface" => arg.surface = child.arg(0)?.ensure_string().map(Some)?,
313 "available_if" => arg.available_if = string_args(&child)?,
314 "display_order" => {
315 arg.display_order = child.arg(0)?.ensure_usize().map(Some)?;
316 }
317 "default" => {
318 let children = child.children();
322 if children.is_empty() {
323 arg.default = vec![child.arg(0)?.ensure_string()?];
325 } else {
326 arg.default = children.iter().map(|c| c.name().to_string()).collect();
329 }
330 }
331 "help" => arg.help = Some(child.arg(0)?.ensure_string()?),
332 "long_help" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
333 "help_long" => arg.help_long = Some(child.arg(0)?.ensure_string()?),
334 "help_md" => arg.help_md = Some(child.arg(0)?.ensure_string()?),
335 "note" => arg
336 .admonitions
337 .push(SpecAdmonition::note(child.arg(0)?.ensure_string()?)),
338 "warning" => arg
339 .admonitions
340 .push(SpecAdmonition::warning(child.arg(0)?.ensure_string()?)),
341 "required" => arg.required = child.arg(0)?.ensure_bool()?,
342 "var" => arg.var = child.arg(0)?.ensure_bool()?,
343 "var_min" => arg.var_min = child.arg(0)?.ensure_usize().map(Some)?,
344 "var_max" => arg.var_max = child.arg(0)?.ensure_usize().map(Some)?,
345 "value_names" => {
346 arg.value_names = child
347 .ensure_arg_len(1..)?
348 .args()
349 .map(|entry| entry.ensure_string())
350 .collect::<Result<Vec<_>, _>>()?;
351 }
352 "allow_negative_numbers" => {
353 arg.allow_negative_numbers = child.arg(0)?.ensure_bool()?;
354 }
355 "value_terminator" => {
356 arg.value_terminator = child.arg(0)?.ensure_string().map(Some)?;
357 }
358 "hide" => arg.hide = child.arg(0)?.ensure_bool()?,
359 "hide_default_value" => arg.hide_default_value = child.arg(0)?.ensure_bool()?,
360 "hide_env" => arg.hide_env = child.arg(0)?.ensure_bool()?,
361 "hide_env_values" => arg.hide_env_values = child.arg(0)?.ensure_bool()?,
362 "hide_possible_values" => arg.hide_possible_values = child.arg(0)?.ensure_bool()?,
363 "hide_short_help" => arg.hide_short_help = child.arg(0)?.ensure_bool()?,
364 "hide_long_help" => arg.hide_long_help = child.arg(0)?.ensure_bool()?,
365 "conflicts" => {
366 arg.conflicts = child
367 .ensure_arg_len(1..)?
368 .args()
369 .map(|entry| entry.ensure_string())
370 .collect::<Result<Vec<_>, _>>()?;
371 }
372 "requires" => arg.requires = string_args(&child)?,
373 "required_if" => arg.required_if = string_args(&child)?,
374 "required_if_eq" => arg.required_if_eq.push(required_if_eq(&child)?),
375 "required_if_eq_all" => {
376 let len = child.args().count();
377 if len < 2 || len % 2 != 0 {
378 bail_parse!(
379 ctx,
380 child.node.name().span(),
381 "required_if_eq_all needs selector/value pairs"
382 );
383 }
384 arg.required_if_eq_all = required_if_eq_pairs(&child)?;
385 }
386 "required_unless" => arg.required_unless = string_args(&child)?,
387 "required_unless_all" => arg.required_unless_all = string_args(&child)?,
388 "double_dash" => arg.double_dash = child.arg(0)?.ensure_string()?.parse()?,
389 k => bail_parse!(ctx, child.node.name().span(), "unsupported arg child {k}"),
390 }
391 }
392 if let Some(first) = arg.value_names.first() {
393 arg.name.clone_from(first);
394 }
395 if arg.value_names.len() > 1 {
396 let arity = arg.value_names.len();
397 match (arg.var_min, arg.var_max) {
398 (None, None) => {
399 arg.var_min = Some(arity);
400 arg.var_max = Some(arity);
401 }
402 (Some(min), Some(max)) if min == arity && max == arity => {}
403 _ => bail_parse!(
404 ctx,
405 node.node.name().span(),
406 "{arity} value names require var_min={arity} and var_max={arity}"
407 ),
408 }
409 arg.var = true;
410 }
411 if arg.validate_error.is_some() && arg.validate.is_none() {
412 bail_parse!(
413 ctx,
414 node.node.name().span(),
415 "validate_error requires a validate expression"
416 );
417 }
418 if arg.value_terminator.as_deref() == Some("") {
419 bail_parse!(
420 ctx,
421 node.node.name().span(),
422 "value_terminator cannot be empty"
423 );
424 }
425 if arg.value_terminator.is_some() && !arg.var {
426 bail_parse!(
427 ctx,
428 node.node.name().span(),
429 "value_terminator requires a variadic argument"
430 );
431 }
432 #[cfg(feature = "validation")]
433 if let Some(expression) = &arg.validate {
434 if let Err(error) = usage_validation::check(expression) {
435 bail_parse!(
436 ctx,
437 node.node.name().span(),
438 "invalid validation expression: {error}"
439 );
440 }
441 }
442 arg.usage = arg.usage();
443 if let Some(help) = &arg.help {
444 arg.help_first_line = Some(string::first_line(help));
445 }
446 Ok(arg)
447 }
448}
449
450impl SpecArg {
451 pub fn usage(&self) -> String {
452 let exact_arity = self.var.then_some(()).and_then(|()| {
453 self.var_min
454 .zip(self.var_max)
455 .filter(|(min, max)| min == max && *min > 1)
456 .map(|(arity, _)| arity)
457 });
458 if self.value_names.len() > 1 || exact_arity.is_some() {
459 let labels = if self.value_names.len() > 1 {
460 self.value_names.clone()
461 } else {
462 vec![
463 self.value_names
464 .first()
465 .cloned()
466 .unwrap_or_else(|| self.name.clone());
467 exact_arity.expect("branch checked")
468 ]
469 };
470 let placeholders = labels
471 .iter()
472 .map(|name| {
473 if self.required {
474 format!("<{name}>")
475 } else {
476 format!("[{name}]")
477 }
478 })
479 .collect::<Vec<_>>()
480 .join(" ");
481 return if self.double_dash == SpecDoubleDashChoices::Required {
482 format!("-- {placeholders}")
483 } else {
484 placeholders
485 };
486 }
487 let name = if self.double_dash == SpecDoubleDashChoices::Required {
488 format!("-- {}", self.name)
489 } else {
490 self.name.clone()
491 };
492 let mut name = if self.required {
493 format!("<{name}>")
494 } else {
495 format!("[{name}]")
496 };
497 if self.var {
498 name = format!("{name}…");
499 }
500 name
501 }
502}
503
504impl From<&SpecArg> for KdlNode {
505 fn from(arg: &SpecArg) -> Self {
506 let mut node = KdlNode::new("arg");
507 node.push(KdlEntry::new(arg.usage()));
508 if let Some(desc) = &arg.help {
509 node.push(string_entry(Some("help"), desc));
510 }
511 if let Some(desc) = &arg.help_long {
512 node.push(string_entry(Some("help_long"), desc));
513 }
514 if let Some(desc) = &arg.help_md {
515 node.push(string_entry(Some("help_md"), desc));
516 }
517 for admonition in &arg.admonitions {
518 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
519 let name = match admonition.kind {
520 SpecAdmonitionKind::Note => "note",
521 SpecAdmonitionKind::Warning => "warning",
522 };
523 let mut block = KdlNode::new(name);
524 block.push(string_entry(None, &admonition.text));
525 children.nodes_mut().push(block);
526 }
527 if !arg.required {
528 node.push(KdlEntry::new_prop("required", false));
529 }
530 if arg.double_dash == SpecDoubleDashChoices::Automatic
531 || arg.double_dash == SpecDoubleDashChoices::Preserve
532 {
533 node.push(KdlEntry::new_prop(
534 "double_dash",
535 arg.double_dash.to_string(),
536 ));
537 }
538 if arg.var {
539 node.push(KdlEntry::new_prop("var", true));
540 }
541 if let Some(min) = arg.var_min {
542 node.push(KdlEntry::new_prop("var_min", min as i128));
543 }
544 if let Some(max) = arg.var_max {
545 node.push(KdlEntry::new_prop("var_max", max as i128));
546 }
547 if let Some(delimiter) = arg.delimiter {
548 node.push(string_entry(Some("delimiter"), &delimiter.to_string()));
549 }
550 if arg.allow_negative_numbers {
551 node.push(KdlEntry::new_prop("allow_negative_numbers", true));
552 }
553 if let Some(terminator) = &arg.value_terminator {
554 node.push(string_entry(Some("value_terminator"), terminator));
555 }
556 if arg.hide {
557 node.push(KdlEntry::new_prop("hide", true));
558 }
559 for (name, hidden) in [
560 ("hide_default_value", arg.hide_default_value),
561 ("hide_env", arg.hide_env),
562 ("hide_env_values", arg.hide_env_values),
563 ("hide_possible_values", arg.hide_possible_values),
564 ("hide_short_help", arg.hide_short_help),
565 ("hide_long_help", arg.hide_long_help),
566 ] {
567 if hidden {
568 node.push(KdlEntry::new_prop(name, true));
569 }
570 }
571 if arg.conflicts.len() == 1 {
572 node.push(string_entry(Some("conflicts"), &arg.conflicts[0]));
573 } else if !arg.conflicts.is_empty() {
574 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
575 let mut conflicts = KdlNode::new("conflicts");
576 for target in &arg.conflicts {
577 conflicts.push(string_entry(None, target));
578 }
579 children.nodes_mut().push(conflicts);
580 }
581 serialize_selector_list(&mut node, "requires", &arg.requires);
582 serialize_selector_list(&mut node, "required_if", &arg.required_if);
583 serialize_required_if_eq(&mut node, "required_if_eq", &arg.required_if_eq);
584 if !arg.required_if_eq_all.is_empty() {
585 serialize_required_if_eq(&mut node, "required_if_eq_all", &arg.required_if_eq_all);
586 }
587 serialize_selector_list(&mut node, "required_unless", &arg.required_unless);
588 serialize_selector_list(&mut node, "required_unless_all", &arg.required_unless_all);
589 if !arg.default.is_empty() {
591 if arg.default.len() == 1 {
592 node.push(string_entry(Some("default"), &arg.default[0]));
594 } else {
595 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
597 let mut default_node = KdlNode::new("default");
598 let default_children = default_node
599 .children_mut()
600 .get_or_insert_with(KdlDocument::new);
601 for val in &arg.default {
602 default_children
603 .nodes_mut()
604 .push(KdlNode::new(val.as_str()));
605 }
606 children.nodes_mut().push(default_node);
607 }
608 }
609 if let Some(env) = &arg.env {
610 node.push(string_entry(Some("env"), env));
611 }
612 serialize_selector_list(&mut node, "env_fallback", &arg.env_fallback);
613 serialize_selector_list(&mut node, "deprecated_env", &arg.deprecated_env);
614 if let Some(validate) = &arg.validate {
615 node.push(string_entry(Some("validate"), validate));
616 }
617 if arg.validate.is_some() {
618 if let Some(error) = &arg.validate_error {
619 node.push(string_entry(Some("validate_error"), error));
620 }
621 }
622 if let Some(help_heading) = &arg.help_heading {
623 node.push(string_entry(Some("help_heading"), help_heading));
624 }
625 if let Some(surface) = &arg.surface {
626 node.push(string_entry(Some("surface"), surface));
627 }
628 serialize_selector_list(&mut node, "available_if", &arg.available_if);
629 if let Some(order) = arg.display_order {
630 node.push(KdlEntry::new_prop("display_order", order as i128));
631 }
632 if let Some(effect) = &arg.effect {
633 node.push(string_entry(Some("effect"), effect.as_str()));
634 }
635 if let Some(choices) = &arg.choices {
636 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
637 children.nodes_mut().push(choices.into());
638 }
639 node
640 }
641}
642
643fn string_args(node: &NodeHelper<'_>) -> Result<Vec<String>, UsageErr> {
644 node.ensure_arg_len(1..)?
645 .args()
646 .map(|entry| entry.ensure_string())
647 .collect()
648}
649
650fn required_if_eq(node: &NodeHelper<'_>) -> Result<SpecRequiredIfEq, UsageErr> {
651 node.ensure_arg_len(2..=2)?;
652 Ok(SpecRequiredIfEq {
653 selector: node.arg(0)?.ensure_string()?,
654 value: node.arg(1)?.ensure_string()?,
655 })
656}
657
658fn required_if_eq_pairs(node: &NodeHelper<'_>) -> Result<Vec<SpecRequiredIfEq>, UsageErr> {
659 let entries = node.args().collect::<Vec<_>>();
660 entries
661 .chunks_exact(2)
662 .map(|pair| {
663 Ok(SpecRequiredIfEq {
664 selector: pair[0].ensure_string()?,
665 value: pair[1].ensure_string()?,
666 })
667 })
668 .collect()
669}
670
671fn serialize_selector_list(node: &mut KdlNode, name: &str, selectors: &[String]) {
672 if selectors.len() == 1 {
673 node.push(string_entry(Some(name), &selectors[0]));
674 } else if !selectors.is_empty() {
675 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
676 let mut relation = KdlNode::new(name);
677 for selector in selectors {
678 relation.push(string_entry(None, selector));
679 }
680 children.nodes_mut().push(relation);
681 }
682}
683
684fn serialize_required_if_eq(node: &mut KdlNode, name: &str, conditions: &[SpecRequiredIfEq]) {
685 if conditions.is_empty() {
686 return;
687 }
688 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
689 if name == "required_if_eq_all" {
690 let mut relation = KdlNode::new(name);
691 for condition in conditions {
692 relation.push(string_entry(None, &condition.selector));
693 relation.push(string_entry(None, &condition.value));
694 }
695 children.nodes_mut().push(relation);
696 } else {
697 for condition in conditions {
698 let mut relation = KdlNode::new(name);
699 relation.push(string_entry(None, &condition.selector));
700 relation.push(string_entry(None, &condition.value));
701 children.nodes_mut().push(relation);
702 }
703 }
704}
705
706impl From<&str> for SpecArg {
707 fn from(input: &str) -> Self {
708 let (input, after_double_dash) = input
709 .strip_prefix("-- ")
710 .map_or((input, false), |rest| (rest, true));
711 if let Some(placeholders) = fixed_placeholders(input) {
712 let required = placeholders
713 .iter()
714 .all(|placeholder| placeholder.starts_with('<'));
715 let value_names = placeholders
716 .iter()
717 .map(|placeholder| placeholder[1..placeholder.len() - 1].to_string())
718 .collect::<Vec<_>>();
719 let mut arg = SpecArg {
720 name: value_names[0].clone(),
721 value_names,
722 required,
723 var: true,
724 var_min: Some(placeholders.len()),
725 var_max: Some(placeholders.len()),
726 double_dash: if after_double_dash {
727 SpecDoubleDashChoices::Required
728 } else {
729 SpecDoubleDashChoices::Optional
730 },
731 ..Default::default()
732 };
733 arg.usage = arg.usage();
734 return arg;
735 }
736 let mut arg = SpecArg {
737 name: input.to_string(),
738 required: true,
739 double_dash: if after_double_dash {
740 SpecDoubleDashChoices::Required
741 } else {
742 SpecDoubleDashChoices::Optional
743 },
744 ..Default::default()
745 };
746 if let Some(name) = arg
748 .name
749 .strip_suffix("...")
750 .or_else(|| arg.name.strip_suffix("…"))
751 {
752 arg.var = true;
753 arg.name = name.to_string();
754 }
755 let first = arg.name.chars().next().unwrap_or_default();
756 let last = arg.name.chars().last().unwrap_or_default();
757 match (first, last) {
758 ('[', ']') => {
759 arg.name = arg.name[1..arg.name.len() - 1].to_string();
760 arg.required = false;
761 }
762 ('<', '>') => {
763 arg.name = arg.name[1..arg.name.len() - 1].to_string();
764 }
765 _ => {}
766 }
767 if let Some(name) = arg.name.strip_prefix("-- ") {
771 arg.double_dash = SpecDoubleDashChoices::Required;
772 arg.name = name.to_string();
773 }
774 if !arg.var {
776 if let Some(name) = arg
777 .name
778 .strip_suffix("...")
779 .or_else(|| arg.name.strip_suffix("…"))
780 {
781 arg.var = true;
782 arg.name = name.to_string();
783 }
784 }
785 arg.usage = arg.usage();
790 arg
791 }
792}
793impl FromStr for SpecArg {
794 type Err = UsageErr;
795 fn from_str(input: &str) -> std::result::Result<Self, UsageErr> {
796 if fixed_placeholders(input.strip_prefix("-- ").unwrap_or(input)).is_some_and(
797 |placeholders| {
798 placeholders
799 .windows(2)
800 .any(|pair| pair[0].starts_with('<') != pair[1].starts_with('<'))
801 },
802 ) {
803 let message =
804 "fixed-arity placeholders must be either all required or all optional".to_string();
805 return Err(UsageErr::InvalidInput(
806 message,
807 (0, input.len()).into(),
808 miette::NamedSource::new("argument", input.to_string()),
809 ));
810 }
811 Ok(input.into())
812 }
813}
814
815fn fixed_placeholders(input: &str) -> Option<Vec<&str>> {
818 if !input.bytes().any(|byte| byte.is_ascii_whitespace()) {
819 return None;
820 }
821 let placeholders: Vec<_> = input.split_whitespace().collect();
822 (placeholders.len() > 1
823 && placeholders.iter().all(|placeholder| {
824 matches!(
825 (placeholder.chars().next(), placeholder.chars().last()),
826 (Some('<'), Some('>')) | (Some('['), Some(']'))
827 )
828 }))
829 .then_some(placeholders)
830}
831
832#[cfg(feature = "clap")]
840pub(crate) fn default_values(arg: &clap::Arg) -> Vec<String> {
841 let raw = arg
842 .get_default_values()
843 .iter()
844 .map(|v| v.to_string_lossy().to_string());
845 match arg.get_value_delimiter() {
846 Some(delimiter) => raw
847 .flat_map(|v| {
848 v.split(delimiter)
849 .map(|part| part.to_string())
850 .collect::<Vec<_>>()
851 })
852 .collect(),
853 None => raw.collect(),
854 }
855}
856
857#[cfg(feature = "clap")]
864pub(crate) fn value_bounds(source: &clap::Arg, target: &mut SpecArg, zero_values_supported: bool) {
865 if source.get_value_delimiter().is_some() {
870 return;
871 }
872
873 let Some(range) = source.get_num_args() else {
874 if target.value_names.len() > 1 {
875 let arity = target.value_names.len();
876 target.var = true;
877 target.var_min = Some(arity);
878 target.var_max = Some(arity);
879 }
880 return;
881 };
882 let min = range.min_values();
883 let max = range.max_values();
884 if max <= 1 || min == 0 && !zero_values_supported {
885 return;
886 }
887
888 target.var = true;
889 target.var_min = Some(min);
890 target.var_max = (max != usize::MAX).then_some(max);
891}
892
893#[cfg(feature = "clap")]
899pub(crate) fn value_names_from_clap(source: &clap::Arg) -> Vec<String> {
900 let names: Vec<String> = source
901 .get_value_names()
902 .unwrap_or_default()
903 .iter()
904 .map(ToString::to_string)
905 .collect();
906 if names.len() <= 1 {
907 return names;
908 }
909 let mismatched_range = source.get_num_args().is_some_and(|range| {
910 range.min_values() != names.len() || range.max_values() != names.len()
911 });
912 if source.get_value_delimiter().is_some() || mismatched_range {
913 names.into_iter().take(1).collect()
914 } else {
915 names
916 }
917}
918
919#[cfg(feature = "clap")]
921pub(crate) fn value_hint_type(hint: clap::ValueHint) -> Option<&'static str> {
922 use clap::ValueHint;
923
924 match hint {
925 ValueHint::Unknown => None,
926 ValueHint::Other => Some("none"),
927 ValueHint::AnyPath | ValueHint::FilePath => Some("path"),
928 ValueHint::DirPath => Some("dir"),
929 ValueHint::ExecutablePath => Some("executable"),
930 ValueHint::CommandName | ValueHint::CommandString => Some("command"),
931 ValueHint::CommandWithArguments => Some("command_args"),
932 ValueHint::Username => Some("username"),
933 ValueHint::Hostname => Some("hostname"),
934 ValueHint::Url => Some("url"),
935 ValueHint::EmailAddress => Some("email"),
936 _ => None,
937 }
938}
939
940#[cfg(feature = "clap")]
941pub(crate) fn choices_from_clap(arg: &clap::Arg) -> Option<SpecChoices> {
942 let possible = arg.get_possible_values();
943 if possible.is_empty() {
944 return None;
945 }
946 let choices = possible
947 .iter()
948 .map(|value| value.get_name().to_string())
949 .collect();
950 let details = possible
951 .iter()
952 .filter_map(|value| {
953 let aliases: Vec<_> = value
954 .get_name_and_aliases()
955 .skip(1)
956 .map(|alias| SpecChoiceAlias {
957 value: alias.to_string(),
958 hide: true,
960 })
961 .collect();
962 let detail = SpecChoice {
963 value: value.get_name().to_string(),
964 help: value.get_help().map(ToString::to_string),
965 hide: value.is_hide_set(),
966 aliases,
967 };
968 (detail.help.is_some() || detail.hide || !detail.aliases.is_empty()).then_some(detail)
969 })
970 .collect();
971 Some(SpecChoices {
972 choices,
973 details,
974 ignore_case: arg.is_ignore_case_set(),
975 ..Default::default()
976 })
977}
978
979#[cfg(feature = "clap")]
980impl From<&clap::Arg> for SpecArg {
981 fn from(arg: &clap::Arg) -> Self {
982 let source = arg;
983 let required = arg.is_required_set();
984 let help = arg.get_help().map(|s| s.to_string());
985 let help_long = arg.get_long_help().map(|s| s.to_string());
986 let help_first_line = help.as_ref().map(|s| string::first_line(s));
987 let hide = arg.is_hide_set();
988 let delimiter = arg.get_value_delimiter();
992 let recorded_delimiter = delimiter.filter(char::is_ascii);
993 let value_terminator = arg.get_value_terminator().map(ToString::to_string);
994 let var = matches!(
995 arg.get_action(),
996 clap::ArgAction::Count | clap::ArgAction::Append
997 ) || delimiter.is_some();
998 let choices = choices_from_clap(arg);
999 let value_names = value_names_from_clap(arg);
1000 let mut arg = Self {
1001 name: value_names
1002 .first()
1003 .cloned()
1004 .unwrap_or_else(|| source.get_id().to_string()),
1005 value_names,
1006 usage: "".into(),
1007 required,
1008 double_dash: if arg.is_last_set() {
1009 SpecDoubleDashChoices::Required
1010 } else if arg.is_trailing_var_arg_set() {
1011 SpecDoubleDashChoices::Automatic
1012 } else {
1013 SpecDoubleDashChoices::Optional
1014 },
1015 help,
1016 help_long,
1017 help_md: None,
1018 admonitions: Vec::new(),
1019 help_first_line,
1020 var,
1021 var_max: None,
1022 var_min: None,
1023 delimiter: recorded_delimiter,
1026 allow_negative_numbers: arg.is_allow_negative_numbers_set(),
1027 value_terminator: None,
1028 hide,
1029 hide_default_value: arg.is_hide_default_value_set(),
1030 hide_env: arg.is_hide_env_set(),
1031 hide_env_values: arg.is_hide_env_values_set(),
1032 hide_possible_values: arg.is_hide_possible_values_set(),
1033 hide_short_help: arg.is_hide_short_help_set(),
1034 hide_long_help: arg.is_hide_long_help_set(),
1035 conflicts: Vec::new(),
1036 requires: Vec::new(),
1037 required_if: Vec::new(),
1038 required_if_eq: Vec::new(),
1039 required_if_eq_all: Vec::new(),
1040 required_unless: Vec::new(),
1041 required_unless_all: Vec::new(),
1042 default: default_values(arg),
1043 choices: None,
1044 validate: None,
1045 validate_error: None,
1046 effect: None,
1047 env: None,
1048 env_fallback: Vec::new(),
1049 deprecated_env: Vec::new(),
1050 help_heading: arg.get_help_heading().map(|s| s.to_string()),
1051 surface: None,
1052 available_if: Vec::new(),
1053 display_order: Some(arg.get_display_order()),
1054 };
1055 arg.choices = choices;
1056
1057 value_bounds(source, &mut arg, true);
1058 if arg.var {
1059 arg.value_terminator = value_terminator;
1060 }
1061
1062 arg
1063 }
1064}
1065
1066impl Display for SpecArg {
1067 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068 write!(f, "{}", self.usage())
1069 }
1070}
1071impl PartialEq for SpecArg {
1072 fn eq(&self, other: &Self) -> bool {
1073 self.name == other.name
1074 }
1075}
1076impl Eq for SpecArg {}
1077impl Hash for SpecArg {
1078 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1079 self.name.hash(state);
1080 }
1081}
1082
1083#[cfg(all(test, feature = "validation"))]
1084mod validation_tests {
1085 use std::collections::HashMap;
1086
1087 use crate::{parse, parse::Parser, Spec};
1088
1089 fn spec() -> Spec {
1090 r#"
1091name "ex"
1092bin "ex"
1093arg "<port>" validate="int(value) >= 1 && int(value) <= 65535" validate_error="must be a valid port"
1094 "#
1095 .parse()
1096 .unwrap()
1097 }
1098
1099 #[test]
1100 fn validation_round_trips_through_kdl() {
1101 let spec = spec();
1102 let kdl = spec.to_string();
1103 let reparsed: Spec = kdl.parse().unwrap();
1104 let arg = &reparsed.cmd.args[0];
1105 assert_eq!(
1106 arg.validate.as_deref(),
1107 Some("int(value) >= 1 && int(value) <= 65535")
1108 );
1109 assert_eq!(arg.validate_error.as_deref(), Some("must be a valid port"));
1110 }
1111
1112 #[test]
1113 fn invalid_validation_declarations_are_rejected_with_the_spec() {
1114 let missing_expression = r#"name "demo"
1115bin "demo"
1116arg "<port>" validate_error="must be a port"
1117"#;
1118 assert!(missing_expression.parse::<Spec>().is_err());
1119
1120 let invalid_expression = r#"name "demo"
1121bin "demo"
1122arg "<port>" validate="int(value) >"
1123"#;
1124 assert!(invalid_expression.parse::<Spec>().is_err());
1125 }
1126
1127 #[test]
1128 fn reference_parser_validates_each_raw_value() {
1129 parse(&spec(), &["ex".to_string(), "9229".to_string()]).unwrap();
1130
1131 let error = parse(&spec(), &["ex".to_string(), "0".to_string()]).unwrap_err();
1132 assert!(
1133 error.to_string().contains("must be a valid port"),
1134 "{error:?}"
1135 );
1136
1137 let variadic: Spec = r#"
1138name "ex"
1139bin "ex"
1140arg "<port>" var=#true validate="int(value) > 0" validate_error="port must be positive"
1141 "#
1142 .parse()
1143 .unwrap();
1144 let error = parse(
1145 &variadic,
1146 &["ex".to_string(), "0".to_string(), "-1".to_string()],
1147 )
1148 .unwrap_err()
1149 .to_string();
1150 assert_eq!(error.matches("port must be positive").count(), 1, "{error}");
1151 }
1152
1153 #[test]
1154 fn reference_parser_validates_environment_and_default_fallbacks() {
1155 let spec: Spec = r#"
1156name "ex"
1157bin "ex"
1158arg "[port]" env="PORT" validate="int(value) > 0" validate_error="port must be positive"
1159flag "--mode" default="bad" {
1160 arg "<mode>" validate="value == 'good'" validate_error="mode must be good"
1161}
1162arg "[ports]..." env="PORTS" var=#true var_max=1 delimiter="," validate="int(value) > 0" validate_error="all ports must be positive"
1163flag "--levels" env="LEVELS" {
1164 arg "<level>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all levels must be good"
1165}
1166flag "--modes" default="good,bad" {
1167 arg "<mode>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all modes must be good"
1168}
1169flag "--conditional" {
1170 default_if "--trigger" "good,bad"
1171 arg "<conditional>..." var=#true var_max=1 delimiter="," validate="value == 'good'" validate_error="all conditional values must be good"
1172}
1173flag "--repeats <repeat>" env="REPEATS" var=#true var_max=1 delimiter=","
1174flag "--trigger"
1175 "#
1176 .parse()
1177 .unwrap();
1178 let env = HashMap::from([
1179 ("PORT".to_string(), "0".to_string()),
1180 ("PORTS".to_string(), "1,0".to_string()),
1181 ("LEVELS".to_string(), "good,bad".to_string()),
1182 ("REPEATS".to_string(), "one,two".to_string()),
1183 ]);
1184 let error = Parser::new(&spec)
1185 .with_env(env)
1186 .parse(&["ex".to_string(), "--trigger".to_string()])
1187 .unwrap_err();
1188 let error = error.to_string();
1189 assert!(error.contains("port must be positive"), "{error}");
1190 assert!(error.contains("mode must be good"), "{error}");
1191 assert!(error.contains("all ports must be positive"), "{error}");
1192 assert!(error.contains("all levels must be good"), "{error}");
1193 assert!(error.contains("all modes must be good"), "{error}");
1194 assert!(
1195 error.contains("all conditional values must be good"),
1196 "{error}"
1197 );
1198 assert!(
1199 error.contains("Variadic argument <ports> accepts at most 1 value(s), got 2"),
1200 "{error}"
1201 );
1202 for flag in ["levels", "modes", "conditional", "repeats"] {
1203 assert!(
1204 error.contains(&format!(
1205 "Variadic flag --{flag} accepts at most 1 value(s), got 2"
1206 )),
1207 "{error}"
1208 );
1209 }
1210 }
1211}
1212
1213#[cfg(test)]
1214mod delimiter_tests {
1215 use crate::Spec;
1216
1217 #[test]
1218 fn a_delimiter_has_to_be_one_byte() {
1219 for spec in [
1224 "flag \"--tags <tag>\" var=#true delimiter=\"§\"\n",
1225 "arg \"[tags]...\" var=#true delimiter=\"、\"\n",
1226 ] {
1227 let err = spec.parse::<Spec>().unwrap_err();
1228 assert!(format!("{err:?}").contains("one byte"), "{err:?}");
1229 }
1230
1231 let cmd = clap::Command::new("ex").arg(
1235 clap::Arg::new("tags")
1236 .long("tags")
1237 .value_delimiter('、')
1238 .action(clap::ArgAction::Set),
1239 );
1240 let spec = Spec::from(&cmd);
1241 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1242 assert_eq!(
1243 arg.delimiter, None,
1244 "a separator it cannot write is not recorded"
1245 );
1246 assert!(arg.var, "clap still splits, so the values still arrive");
1247 spec.to_string()
1248 .parse::<Spec>()
1249 .expect("what the bridge produces has to parse back");
1250 }
1251
1252 #[test]
1253 fn a_delimiter_round_trips_and_comes_across_from_clap() {
1254 let spec: Spec = "flag \"--tags <tag>\" var=#true delimiter=\",\"\n"
1255 .parse()
1256 .unwrap();
1257 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1258 assert_eq!(arg.delimiter, Some(','));
1259
1260 let reparsed: Spec = spec.to_string().parse().unwrap();
1261 let arg = reparsed.cmd.flags[0].arg.as_ref().unwrap();
1262 assert_eq!(arg.delimiter, Some(','), "{spec}");
1263
1264 let cmd = clap::Command::new("ex").arg(
1267 clap::Arg::new("tags")
1268 .long("tags")
1269 .value_delimiter(',')
1270 .num_args(1..)
1271 .default_value("a,b"),
1272 );
1273 let spec = Spec::from(&cmd);
1274 let flag = &spec.cmd.flags[0];
1275 assert_eq!(flag.arg.as_ref().unwrap().delimiter, Some(','));
1276 assert_eq!(flag.default, vec!["a", "b"]);
1279 }
1280
1281 #[test]
1282 fn a_single_valued_clap_arg_keeps_its_delimiter() {
1283 let cmd = clap::Command::new("ex").arg(
1288 clap::Arg::new("tags")
1289 .long("tags")
1290 .action(clap::ArgAction::Set)
1291 .value_delimiter(','),
1292 );
1293 let spec = Spec::from(&cmd);
1294 let arg = spec.cmd.flags[0].arg.as_ref().unwrap();
1295 assert_eq!(arg.delimiter, Some(','));
1296 assert!(arg.var, "a delimiter brings `var` with it");
1299 let _: Spec = spec.to_string().parse().expect("{spec}");
1300 }
1301
1302 #[test]
1303 fn a_single_valued_clap_positional_splits_into_stored_values() {
1304 let cmd = clap::Command::new("ex").arg(
1308 clap::Arg::new("tags")
1309 .action(clap::ArgAction::Set)
1310 .value_delimiter(',')
1311 .value_parser(["a", "b"]),
1312 );
1313 let spec = Spec::from(&cmd);
1314 let arg = &spec.cmd.args[0];
1315 assert!(arg.var, "a positional delimiter brings `var` with it");
1316 assert_eq!(arg.delimiter, Some(','));
1317
1318 let input = ["ex", "a,b"].map(str::to_string);
1319 let parsed = crate::parse(&spec, &input).expect("both split values are choices");
1320 let value = parsed
1321 .args
1322 .values()
1323 .next()
1324 .expect("the positional was stored");
1325 assert!(matches!(
1326 value,
1327 crate::parse::ParseValue::MultiString(values)
1328 if values == &["a".to_string(), "b".to_string()]
1329 ));
1330 }
1331
1332 #[test]
1333 fn a_delimiter_needs_somewhere_to_put_what_it_splits() {
1334 let err = "flag \"--tags <tag>\" delimiter=\",\"\n"
1336 .parse::<Spec>()
1337 .unwrap_err();
1338 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1339
1340 let err = "arg \"[tags]\" delimiter=\",\"\n"
1341 .parse::<Spec>()
1342 .unwrap_err();
1343 assert!(format!("{err:?}").contains("one value"), "{err:?}");
1344
1345 let err = "flag \"--quiet\" delimiter=\",\"\n"
1347 .parse::<Spec>()
1348 .unwrap_err();
1349 assert!(format!("{err:?}").contains("takes none"), "{err:?}");
1350
1351 let err = "flag \"--tags <tag>\" var=#true delimiter=\"::\"\n"
1353 .parse::<Spec>()
1354 .unwrap_err();
1355 assert!(format!("{err:?}").contains("one character"), "{err:?}");
1356 }
1357}
1358
1359#[cfg(test)]
1360mod possible_value_tests {
1361 use clap::builder::PossibleValue;
1362
1363 #[test]
1364 fn clap_possible_value_metadata_survives_the_bridge() {
1365 let command = clap::Command::new("ex").arg(
1366 clap::Arg::new("color").ignore_case(true).value_parser([
1367 PossibleValue::new("always")
1368 .help("Always use color")
1369 .alias("yes"),
1370 PossibleValue::new("never").hide(true),
1371 ]),
1372 );
1373 let spec = crate::Spec::from(&command);
1374 let choices = spec.cmd.args[0].choices.as_ref().unwrap();
1375 assert_eq!(choices.choices, ["always", "never"]);
1376 assert!(choices.ignore_case);
1377 assert!(choices.matches("YES"));
1378 assert_eq!(choices.values(), ["always"]);
1379 assert_eq!(choices.details[0].help.as_deref(), Some("Always use color"));
1380 assert!(choices.details[0].aliases[0].hide);
1381 assert!(choices.details[1].hide);
1382 }
1383}
1384
1385#[cfg(test)]
1386mod tests {
1387 use crate::{Spec, SpecArg};
1388 use insta::assert_snapshot;
1389
1390 #[test]
1391 fn test_arg_with_env() {
1392 let spec = Spec::parse(
1393 &Default::default(),
1394 r#"
1395arg "<input>" env="MY_INPUT" help="Input file"
1396arg "<output>" env="MY_OUTPUT"
1397 "#,
1398 )
1399 .unwrap();
1400
1401 assert_snapshot!(spec, @r#"
1402 arg <input> help="Input file" env=MY_INPUT
1403 arg <output> env=MY_OUTPUT
1404 "#);
1405
1406 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1407 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1408
1409 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1410 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1411 }
1412
1413 #[test]
1414 fn test_arg_with_env_child_node() {
1415 let spec = Spec::parse(
1416 &Default::default(),
1417 r#"
1418arg "<input>" help="Input file" {
1419 env "MY_INPUT"
1420}
1421arg "<output>" {
1422 env "MY_OUTPUT"
1423}
1424 "#,
1425 )
1426 .unwrap();
1427
1428 assert_snapshot!(spec, @r#"
1429 arg <input> help="Input file" env=MY_INPUT
1430 arg <output> env=MY_OUTPUT
1431 "#);
1432
1433 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1434 assert_eq!(input_arg.env, Some("MY_INPUT".to_string()));
1435
1436 let output_arg = spec.cmd.args.iter().find(|a| a.name == "output").unwrap();
1437 assert_eq!(output_arg.env, Some("MY_OUTPUT".to_string()));
1438 }
1439
1440 #[test]
1441 fn test_arg_variadic_syntax() {
1442 use crate::SpecArg;
1443
1444 let arg: SpecArg = "<files>...".into();
1446 assert_eq!(arg.name, "files");
1447 assert!(arg.var);
1448 assert!(arg.required);
1449
1450 let arg: SpecArg = "[files]...".into();
1452 assert_eq!(arg.name, "files");
1453 assert!(arg.var);
1454 assert!(!arg.required);
1455
1456 let arg: SpecArg = "<files>…".into();
1458 assert_eq!(arg.name, "files");
1459 assert!(arg.var);
1460
1461 let arg: SpecArg = "[files]…".into();
1462 assert_eq!(arg.name, "files");
1463 assert!(arg.var);
1464 assert!(!arg.required);
1465
1466 let arg: SpecArg = "[args...]".into();
1468 assert_eq!(arg.name, "args");
1469 assert!(arg.var);
1470 assert!(!arg.required);
1471
1472 let arg: SpecArg = "<args...>".into();
1473 assert_eq!(arg.name, "args");
1474 assert!(arg.var);
1475 assert!(arg.required);
1476
1477 let arg: SpecArg = "[args…]".into();
1479 assert_eq!(arg.name, "args");
1480 assert!(arg.var);
1481 assert!(!arg.required);
1482 }
1483
1484 #[test]
1485 fn fixed_arity_placeholders_round_trip() {
1486 let spec: Spec = "arg \"<START> <END>\"\n".parse().unwrap();
1487 let arg = &spec.cmd.args[0];
1488 assert_eq!(arg.value_names, ["START", "END"]);
1489 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1490 assert_eq!(arg.usage, "<START> <END>");
1491
1492 let reparsed: Spec = spec.to_string().parse().unwrap();
1493 assert_eq!(reparsed.cmd.args[0].value_names, ["START", "END"]);
1494 }
1495
1496 #[test]
1497 fn fixed_arity_placeholders_reject_mismatched_bounds() {
1498 let error = "arg \"<START> <END>\" var_min=1 var_max=2\n"
1499 .parse::<Spec>()
1500 .unwrap_err();
1501 assert!(
1502 format!("{error:?}").contains("require var_min=2 and var_max=2"),
1503 "{error:?}"
1504 );
1505 }
1506
1507 #[test]
1508 fn a_single_value_name_replaces_the_display_name() {
1509 let spec: Spec = "arg \"<input>\" { value_names \"INPUT\" }\n"
1510 .parse()
1511 .unwrap();
1512 let arg = &spec.cmd.args[0];
1513 assert_eq!(arg.name, "INPUT");
1514 assert_eq!(arg.usage, "<INPUT>");
1515
1516 let built = SpecArg::builder()
1517 .name("input")
1518 .required(true)
1519 .value_names(["INPUT"])
1520 .build();
1521 assert_eq!(built.name, "INPUT");
1522 assert_eq!(built.usage, "<INPUT>");
1523 }
1524
1525 #[test]
1526 fn builder_fixed_arity_survives_later_bound_setters() {
1527 let after = SpecArg::builder()
1528 .value_names(["START", "END"])
1529 .var(false)
1530 .var_min(1)
1531 .var_max(4)
1532 .build();
1533 let before = SpecArg::builder()
1534 .var(false)
1535 .var_min(1)
1536 .var_max(4)
1537 .value_names(["START", "END"])
1538 .build();
1539 for arg in [after, before] {
1540 assert!(arg.var);
1541 assert_eq!((arg.var_min, arg.var_max), (Some(2), Some(2)));
1542 assert_eq!(arg.usage, "[START] [END]");
1543 }
1544 }
1545
1546 #[test]
1547 fn one_label_with_exact_bounds_renders_each_value_slot() {
1548 let spec: Spec = "arg \"<item>…\" var_min=2 var_max=2 { value_names \"ITEM\" }\n"
1549 .parse()
1550 .unwrap();
1551 assert_eq!(spec.cmd.args[0].usage, "<ITEM> <ITEM>");
1552 let reparsed: Spec = spec.to_string().parse().unwrap();
1553 assert_eq!(reparsed.cmd.args[0].value_names, ["ITEM", "ITEM"]);
1554 assert_eq!(
1555 (reparsed.cmd.args[0].var_min, reparsed.cmd.args[0].var_max),
1556 (Some(2), Some(2))
1557 );
1558
1559 let built = SpecArg::builder()
1560 .value_names(["ITEM"])
1561 .required(true)
1562 .var(true)
1563 .var_min(2)
1564 .var_max(2)
1565 .build();
1566 assert_eq!(built.usage, "<ITEM> <ITEM>");
1567 }
1568
1569 #[test]
1570 fn fixed_arity_placeholders_reject_mixed_requiredness() {
1571 let error = "arg \"<START> [END]\"\n".parse::<Spec>().unwrap_err();
1572 assert!(
1573 format!("{error:?}")
1574 .contains("fixed-arity placeholders must be either all required or all optional"),
1575 "{error:?}"
1576 );
1577 }
1578
1579 #[test]
1580 fn test_arg_child_nodes() {
1581 let spec = Spec::parse(
1582 &Default::default(),
1583 r#"
1584arg "<environment>" {
1585 help "Deployment environment"
1586 choices "dev" "staging" "prod"
1587}
1588arg "[services]" {
1589 help "Services to deploy"
1590 var #true
1591 var_min 0
1592}
1593 "#,
1594 )
1595 .unwrap();
1596
1597 let env_arg = spec
1598 .cmd
1599 .args
1600 .iter()
1601 .find(|a| a.name == "environment")
1602 .unwrap();
1603 assert_eq!(env_arg.help, Some("Deployment environment".to_string()));
1604 assert!(env_arg.choices.is_some());
1605
1606 let svc_arg = spec.cmd.args.iter().find(|a| a.name == "services").unwrap();
1607 assert_eq!(svc_arg.help, Some("Services to deploy".to_string()));
1608 assert!(svc_arg.var);
1609 assert_eq!(svc_arg.var_min, Some(0));
1610 }
1611
1612 #[test]
1613 fn test_arg_long_help_child_node() {
1614 let spec = Spec::parse(
1615 &Default::default(),
1616 r#"
1617arg "<input>" {
1618 help "Input file"
1619 long_help "Extended help text for input"
1620}
1621 "#,
1622 )
1623 .unwrap();
1624
1625 let input_arg = spec.cmd.args.iter().find(|a| a.name == "input").unwrap();
1626 assert_eq!(input_arg.help, Some("Input file".to_string()));
1627 assert_eq!(
1628 input_arg.help_long,
1629 Some("Extended help text for input".to_string())
1630 );
1631 }
1632
1633 #[test]
1634 fn positional_conflicts_round_trip_without_dropping_members() {
1635 let spec: Spec = "arg \"[VALUE]\" { conflicts \"--from-file\" \"--stdin\" }\n"
1636 .parse()
1637 .unwrap();
1638 assert_eq!(
1639 spec.cmd.args[0].conflicts,
1640 vec!["--from-file".to_string(), "--stdin".to_string()]
1641 );
1642
1643 let rendered = spec.to_string();
1644 let reparsed: Spec = rendered.parse().unwrap();
1645 assert_eq!(reparsed.cmd.args[0].conflicts, spec.cmd.args[0].conflicts);
1646 }
1647}