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