1pub mod registry;
12
13use indexmap::{IndexMap, IndexSet};
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::fmt;
16
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
28pub enum NArgs {
29 Fixed(usize),
30 #[default]
31 ZeroOrMore,
32 OneOrMore,
33 Optional,
34 AtLeast(usize),
35}
36
37impl Serialize for NArgs {
38 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
39 match self {
40 NArgs::Fixed(value) => serializer.serialize_u64(*value as u64),
41 NArgs::ZeroOrMore => serializer.serialize_str("*"),
42 NArgs::OneOrMore => serializer.serialize_str("+"),
43 NArgs::Optional => serializer.serialize_str("?"),
44 NArgs::AtLeast(value) => serializer.serialize_str(&format!("{value}+")),
45 }
46 }
47}
48
49impl<'de> Deserialize<'de> for NArgs {
50 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
51 struct Visitor;
52
53 impl<'de> serde::de::Visitor<'de> for Visitor {
54 type Value = NArgs;
55
56 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, r#"integer or string ("*", "+", "?", "N+")"#)
58 }
59
60 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<NArgs, E> {
61 Ok(NArgs::Fixed(v as usize))
62 }
63
64 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<NArgs, E> {
65 Ok(NArgs::Fixed(v.max(0) as usize))
66 }
67
68 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<NArgs, E> {
69 match v {
70 "*" => Ok(NArgs::ZeroOrMore),
71 "+" => Ok(NArgs::OneOrMore),
72 "?" => Ok(NArgs::Optional),
73 s if s.ends_with('+') && s.len() > 1 => {
74 let n = s[..s.len() - 1]
75 .parse::<usize>()
76 .map_err(|_| E::custom(format!("invalid NArgs pattern: {s}")))?;
77 Ok(NArgs::AtLeast(n))
78 }
79 s => {
80 let n = s
81 .parse::<usize>()
82 .map_err(|_| E::custom(format!("invalid NArgs value: {s}")))?;
83 Ok(NArgs::Fixed(n))
84 }
85 }
86 }
87 }
88
89 d.deserialize_any(Visitor)
90 }
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct LayoutOverrides {
98 pub line_width: Option<usize>,
100 pub tab_size: Option<usize>,
102 pub dangle_parens: Option<bool>,
104 pub always_wrap: Option<bool>,
106 pub max_pargs_hwrap: Option<usize>,
108}
109
110#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
112#[serde(deny_unknown_fields)]
113pub struct KwargSpec {
114 #[serde(default)]
116 pub nargs: NArgs,
117 #[serde(default)]
119 pub kwargs: IndexMap<String, KwargSpec>,
120 #[serde(default)]
122 pub flags: IndexSet<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct CommandForm {
129 #[serde(default)]
131 pub pargs: NArgs,
132 #[serde(default)]
134 pub kwargs: IndexMap<String, KwargSpec>,
135 #[serde(default)]
137 pub flags: IndexSet<String>,
138 #[serde(default)]
140 pub layout: Option<LayoutOverrides>,
141}
142
143impl Default for CommandForm {
144 fn default() -> Self {
145 Self {
146 pargs: NArgs::ZeroOrMore,
147 kwargs: IndexMap::new(),
148 flags: IndexSet::new(),
149 layout: None,
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
155#[serde(untagged)]
156pub enum CommandSpec {
157 Discriminated {
160 forms: IndexMap<String, CommandForm>,
162 #[serde(default)]
164 fallback: Option<CommandForm>,
165 },
166 Single(CommandForm),
168}
169
170impl CommandSpec {
171 pub fn form_for(&self, first_arg: Option<&str>) -> &CommandForm {
176 match self {
177 CommandSpec::Single(form) => form,
178 CommandSpec::Discriminated { forms, fallback } => {
179 let key = first_arg.unwrap_or_default();
180 forms
181 .get(key)
182 .or_else(|| {
183 has_ascii_lowercase(key)
184 .then(|| key.to_ascii_uppercase())
185 .and_then(|normalized| forms.get(&normalized))
186 })
187 .or(fallback.as_ref())
188 .unwrap_or_else(|| {
189 forms
190 .values()
191 .next()
192 .expect("discriminated spec has a form")
193 })
194 }
195 }
196 }
197}
198
199fn has_ascii_lowercase(s: &str) -> bool {
200 s.bytes().any(|byte| byte.is_ascii_lowercase())
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
204pub struct SpecMetadata {
205 #[serde(default)]
207 pub cmake_version: String,
208 #[serde(default)]
210 pub audited_at: String,
211 #[serde(default)]
213 pub notes: String,
214}
215
216#[derive(Debug, Default, Deserialize)]
218pub struct SpecFile {
219 #[serde(default)]
221 pub metadata: SpecMetadata,
222 #[serde(default)]
224 pub commands: IndexMap<String, CommandSpec>,
225}
226
227#[derive(Debug, Clone, Default, Deserialize, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct LayoutOverridesOverride {
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub line_width: Option<usize>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub tab_size: Option<usize>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub dangle_parens: Option<bool>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub always_wrap: Option<bool>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub max_pargs_hwrap: Option<usize>,
247}
248
249#[derive(Debug, Clone, Default, Deserialize, Serialize)]
251#[serde(deny_unknown_fields)]
252pub struct KwargSpecOverride {
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub nargs: Option<NArgs>,
256 #[serde(default)]
258 #[serde(skip_serializing_if = "IndexMap::is_empty")]
259 pub kwargs: IndexMap<String, KwargSpecOverride>,
260 #[serde(default)]
262 #[serde(skip_serializing_if = "IndexSet::is_empty")]
263 pub flags: IndexSet<String>,
264}
265
266#[derive(Debug, Clone, Default, Deserialize, Serialize)]
268#[serde(deny_unknown_fields)]
269pub struct CommandFormOverride {
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub pargs: Option<NArgs>,
273 #[serde(default)]
275 #[serde(skip_serializing_if = "IndexMap::is_empty")]
276 pub kwargs: IndexMap<String, KwargSpecOverride>,
277 #[serde(default)]
279 #[serde(skip_serializing_if = "IndexSet::is_empty")]
280 pub flags: IndexSet<String>,
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub layout: Option<LayoutOverridesOverride>,
284}
285
286#[derive(Debug, Clone, Deserialize, Serialize)]
288#[serde(untagged)]
289pub enum CommandSpecOverride {
290 Single(CommandFormOverride),
292 Discriminated {
294 #[serde(default)]
296 #[serde(skip_serializing_if = "IndexMap::is_empty")]
297 forms: IndexMap<String, CommandFormOverride>,
298 #[serde(default)]
300 #[serde(skip_serializing_if = "Option::is_none")]
301 fallback: Option<CommandFormOverride>,
302 },
303}
304
305#[derive(Debug, Default, Deserialize, Serialize)]
307pub struct SpecOverrideFile {
308 #[serde(default)]
310 pub commands: IndexMap<String, CommandSpecOverride>,
311}
312
313impl CommandSpecOverride {
314 pub fn into_full_spec(self) -> CommandSpec {
317 match self {
318 CommandSpecOverride::Single(form) => CommandSpec::Single(form.into_full_form()),
319 CommandSpecOverride::Discriminated { forms, fallback } => CommandSpec::Discriminated {
320 forms: forms
321 .into_iter()
322 .map(|(name, form)| (name.to_ascii_uppercase(), form.into_full_form()))
323 .collect(),
324 fallback: fallback.map(CommandFormOverride::into_full_form),
325 },
326 }
327 }
328}
329
330impl CommandFormOverride {
331 pub fn into_full_form(self) -> CommandForm {
333 CommandForm {
334 pargs: self.pargs.unwrap_or_default(),
335 kwargs: self
336 .kwargs
337 .into_iter()
338 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
339 .collect(),
340 flags: self
341 .flags
342 .into_iter()
343 .map(|flag| flag.to_ascii_uppercase())
344 .collect(),
345 layout: self.layout.map(LayoutOverridesOverride::into_full_layout),
346 }
347 }
348}
349
350impl KwargSpecOverride {
351 pub fn into_full_spec(self) -> KwargSpec {
353 KwargSpec {
354 nargs: self.nargs.unwrap_or_default(),
355 kwargs: self
356 .kwargs
357 .into_iter()
358 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
359 .collect(),
360 flags: self
361 .flags
362 .into_iter()
363 .map(|flag| flag.to_ascii_uppercase())
364 .collect(),
365 }
366 }
367}
368
369impl LayoutOverridesOverride {
370 pub fn into_full_layout(self) -> LayoutOverrides {
372 LayoutOverrides {
373 line_width: self.line_width,
374 tab_size: self.tab_size,
375 dangle_parens: self.dangle_parens,
376 always_wrap: self.always_wrap,
377 max_pargs_hwrap: self.max_pargs_hwrap,
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn nargs_serialize_round_trip() {
388 let values = [
389 NArgs::Fixed(3),
390 NArgs::ZeroOrMore,
391 NArgs::OneOrMore,
392 NArgs::Optional,
393 NArgs::AtLeast(2),
394 ];
395 for value in values {
396 let encoded = serde_json::to_string(&value).unwrap();
397 let decoded: NArgs = serde_json::from_str(&encoded).unwrap();
398 assert_eq!(decoded, value);
399 }
400 }
401
402 #[test]
403 fn nargs_invalid_pattern_is_rejected() {
404 let err = toml::from_str::<KwargSpec>("nargs = \"abc+\"\n").unwrap_err();
405 assert!(err.to_string().contains("invalid NArgs pattern"));
406 }
407
408 #[test]
409 fn nargs_integer() {
410 let src = "nargs = 1\n";
411 let spec: KwargSpec = toml::from_str(src).unwrap();
412 assert_eq!(spec.nargs, NArgs::Fixed(1));
413 }
414
415 #[test]
416 fn nargs_zero_or_more() {
417 let src = "nargs = \"*\"\n";
418 let spec: KwargSpec = toml::from_str(src).unwrap();
419 assert_eq!(spec.nargs, NArgs::ZeroOrMore);
420 }
421
422 #[test]
423 fn nargs_one_or_more() {
424 let src = "nargs = \"+\"\n";
425 let spec: KwargSpec = toml::from_str(src).unwrap();
426 assert_eq!(spec.nargs, NArgs::OneOrMore);
427 }
428
429 #[test]
430 fn nargs_optional() {
431 let src = "nargs = \"?\"\n";
432 let spec: KwargSpec = toml::from_str(src).unwrap();
433 assert_eq!(spec.nargs, NArgs::Optional);
434 }
435
436 #[test]
437 fn nargs_at_least() {
438 let src = "nargs = \"2+\"\n";
439 let spec: KwargSpec = toml::from_str(src).unwrap();
440 assert_eq!(spec.nargs, NArgs::AtLeast(2));
441 }
442
443 #[test]
444 fn single_command_form() {
445 let src = r#"
446pargs = 1
447flags = ["REQUIRED"]
448
449[kwargs.COMPONENTS]
450nargs = "+"
451"#;
452 let form: CommandForm = toml::from_str(src).unwrap();
453 assert_eq!(form.pargs, NArgs::Fixed(1));
454 assert!(form.flags.contains("REQUIRED"));
455 assert!(form.kwargs.contains_key("COMPONENTS"));
456 }
457
458 #[test]
459 fn discriminated_command() {
460 let src = r#"
461[forms.TARGETS]
462pargs = "+"
463
464[forms.TARGETS.kwargs.DESTINATION]
465nargs = 1
466
467[forms.FILES]
468pargs = "+"
469"#;
470 let spec: CommandSpec = toml::from_str(src).unwrap();
471 assert!(matches!(spec, CommandSpec::Discriminated { .. }));
472 let form = spec.form_for(Some("targets"));
473 assert!(form.kwargs.contains_key("DESTINATION"));
474 }
475
476 #[test]
477 fn discriminated_command_uses_fallback_when_no_key_matches() {
478 let src = r#"
479[forms.FILE]
480pargs = 1
481
482[fallback]
483pargs = 2
484"#;
485 let spec: CommandSpec = toml::from_str(src).unwrap();
486 let form = spec.form_for(Some("unknown"));
487 assert_eq!(form.pargs, NArgs::Fixed(2));
488 }
489
490 #[test]
491 fn command_spec_override_into_full_spec_normalizes_casing() {
492 let override_spec = CommandSpecOverride::Single(CommandFormOverride {
493 pargs: Some(NArgs::Fixed(1)),
494 flags: ["quiet".to_owned()].into_iter().collect(),
495 kwargs: [(
496 "sources".to_owned(),
497 KwargSpecOverride {
498 nargs: Some(NArgs::OneOrMore),
499 ..KwargSpecOverride::default()
500 },
501 )]
502 .into_iter()
503 .collect(),
504 layout: Some(LayoutOverridesOverride {
505 always_wrap: Some(true),
506 ..LayoutOverridesOverride::default()
507 }),
508 });
509
510 let full = override_spec.into_full_spec();
511 let form = full.form_for(None);
512 assert!(form.flags.contains("QUIET"));
513 assert!(form.kwargs.contains_key("SOURCES"));
514 assert_eq!(form.kwargs["SOURCES"].nargs, NArgs::OneOrMore);
515 assert_eq!(form.layout.as_ref().unwrap().always_wrap, Some(true));
516 }
517
518 #[test]
519 fn partial_override_round_trips() {
520 let src = r#"
521layout.always_wrap = true
522
523[kwargs.COMPONENTS]
524nargs = "+"
525"#;
526 let override_form: CommandFormOverride = toml::from_str(src).unwrap();
527 assert_eq!(override_form.layout.unwrap().always_wrap, Some(true));
528 assert_eq!(
529 override_form.kwargs["COMPONENTS"].nargs,
530 Some(NArgs::OneOrMore)
531 );
532 }
533}