1use crate::error::Result;
2use crate::kdl::{KdlDocument, KdlEntry, KdlNode};
3use crate::spec::context::ParsingContext;
4use crate::spec::helpers::{string_entry, NodeHelper};
5use crate::{SpecArg, SpecFlag};
6use serde::Serialize;
7use std::collections::HashSet;
8
9#[derive(Debug, Default, Clone, Serialize)]
11#[non_exhaustive]
12pub struct SpecClause {
13 pub name: String,
14 pub separator: Option<String>,
15 pub flags: Vec<SpecFlag>,
16 pub args: Vec<SpecArg>,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub help: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub help_long: Option<String>,
21 pub usage: String,
22}
23
24impl SpecClause {
25 pub(crate) fn conflicting_flag_spelling(&self, command_flags: &[SpecFlag]) -> Option<String> {
26 fn spellings(flag: &SpecFlag) -> impl Iterator<Item = String> + '_ {
27 flag.long
28 .iter()
29 .chain(&flag.hidden_aliases)
30 .map(|name| format!("--{name}"))
31 .chain(
32 flag.short
33 .iter()
34 .chain(&flag.hidden_short_aliases)
35 .map(|name| format!("-{name}")),
36 )
37 .chain(flag.negate.iter().cloned())
38 }
39
40 let mut seen = command_flags
41 .iter()
42 .flat_map(spellings)
43 .collect::<HashSet<_>>();
44 self.flags
45 .iter()
46 .flat_map(spellings)
47 .find(|spelling| !seen.insert(spelling.clone()))
48 }
49
50 pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self> {
51 let mut clause = Self {
52 name: node.arg(0)?.ensure_string()?,
53 ..Self::default()
54 };
55 for (key, value) in node.props() {
56 match key {
57 "separator" => clause.separator = Some(value.ensure_string()?),
58 "help" => clause.help = Some(value.ensure_string()?),
59 "help_long" | "long_help" => clause.help_long = Some(value.ensure_string()?),
60 key => bail_parse!(ctx, value.entry.span(), "unsupported clause key {key}"),
61 }
62 }
63 for child in node.children() {
64 match child.name() {
65 "arg" => clause.args.push(SpecArg::parse(ctx, &child)?),
66 "flag" => clause.flags.push(SpecFlag::parse(ctx, &child)?),
67 key => bail_parse!(
68 ctx,
69 child.node.name().span(),
70 "unsupported clause child {key}"
71 ),
72 }
73 }
74 if clause.name.is_empty() {
75 bail_parse!(ctx, node.span(), "a clause needs a name");
76 }
77 if clause.separator.as_ref().is_some_and(String::is_empty) {
78 bail_parse!(ctx, node.span(), "clause separator cannot be empty");
79 }
80 if clause
81 .separator
82 .as_ref()
83 .is_some_and(|separator| separator.starts_with('-'))
84 {
85 bail_parse!(ctx, node.span(), "clause separator cannot start with `-`");
86 }
87 if clause.args.is_empty() {
88 bail_parse!(
89 ctx,
90 node.span(),
91 "clause {} needs at least one argument",
92 clause.name
93 );
94 }
95 if clause.separator.is_none()
96 && (clause.args.len() != 1 || !clause.args[0].required || clause.args[0].var)
97 {
98 bail_parse!(
99 ctx,
100 node.span(),
101 "an implicit clause needs exactly one required, non-variadic positional argument"
102 );
103 }
104 clause.usage = clause.usage();
105 Ok(clause)
106 }
107
108 pub fn usage(&self) -> String {
109 let inner = self
110 .args
111 .iter()
112 .map(SpecArg::usage)
113 .collect::<Vec<_>>()
114 .join(" ");
115 match &self.separator {
116 Some(separator) => format!("[{inner} [{separator} {inner}]…]"),
117 None => {
118 let Some(arg) = self.args.first() else {
119 return String::new();
120 };
121 let mut arg = arg.clone();
122 arg.required = false;
123 arg.var = true;
124 arg.usage()
125 }
126 }
127 }
128}
129
130impl From<&SpecClause> for KdlNode {
131 fn from(clause: &SpecClause) -> Self {
132 let mut node = KdlNode::new("clause");
133 node.push(KdlEntry::new(clause.name.clone()));
134 if let Some(separator) = &clause.separator {
135 node.push(string_entry(Some("separator"), separator));
136 }
137 if let Some(help) = &clause.help {
138 node.push(string_entry(Some("help"), help));
139 }
140 if let Some(help) = &clause.help_long {
141 node.push(string_entry(Some("help_long"), help));
142 }
143 let children = node.children_mut().get_or_insert_with(KdlDocument::new);
144 children
145 .nodes_mut()
146 .extend(clause.flags.iter().map(Into::into));
147 children
148 .nodes_mut()
149 .extend(clause.args.iter().map(Into::into));
150 node
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::SpecClause;
157
158 #[test]
159 fn an_empty_default_clause_has_an_empty_synopsis() {
160 assert_eq!(SpecClause::default().usage(), "");
161 }
162}