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