1pub mod registry;
28
29use indexmap::{IndexMap, IndexSet};
30use serde::{Deserialize, Deserializer, Serialize, Serializer};
31use std::fmt;
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub enum NArgs {
45 Fixed(usize),
48 #[default]
52 ZeroOrMore,
53 OneOrMore,
58 Optional,
60 AtLeast(usize),
63}
64
65impl Serialize for NArgs {
66 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
67 match self {
68 NArgs::Fixed(value) => serializer.serialize_u64(*value as u64),
69 NArgs::ZeroOrMore => serializer.serialize_str("*"),
70 NArgs::OneOrMore => serializer.serialize_str("+"),
71 NArgs::Optional => serializer.serialize_str("?"),
72 NArgs::AtLeast(value) => serializer.serialize_str(&format!("{value}+")),
73 }
74 }
75}
76
77impl<'de> Deserialize<'de> for NArgs {
78 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
79 struct Visitor;
80
81 impl<'de> serde::de::Visitor<'de> for Visitor {
82 type Value = NArgs;
83
84 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 write!(f, r#"integer or string ("*", "+", "?", "N+")"#)
86 }
87
88 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<NArgs, E> {
89 Ok(NArgs::Fixed(v as usize))
90 }
91
92 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<NArgs, E> {
93 Ok(NArgs::Fixed(v.max(0) as usize))
94 }
95
96 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<NArgs, E> {
97 match v {
98 "*" => Ok(NArgs::ZeroOrMore),
99 "+" => Ok(NArgs::OneOrMore),
100 "?" => Ok(NArgs::Optional),
101 s if s.ends_with('+') && s.len() > 1 => {
102 let n = s[..s.len() - 1]
103 .parse::<usize>()
104 .map_err(|_| E::custom(format!("invalid NArgs pattern: {s}")))?;
105 Ok(NArgs::AtLeast(n))
106 }
107 s => {
108 let n = s
109 .parse::<usize>()
110 .map_err(|_| E::custom(format!("invalid NArgs value: {s}")))?;
111 Ok(NArgs::Fixed(n))
112 }
113 }
114 }
115 }
116
117 d.deserialize_any(Visitor)
118 }
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
125#[serde(deny_unknown_fields)]
126pub struct LayoutOverrides {
127 pub line_width: Option<usize>,
129 pub tab_size: Option<usize>,
131 pub dangle_parens: Option<bool>,
133 pub always_wrap: Option<bool>,
135 pub max_pargs_hwrap: Option<usize>,
137 pub wrap_after_first_arg: Option<bool>,
142 pub continuation_align: Option<crate::config::ContinuationAlign>,
144}
145
146#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
148#[serde(deny_unknown_fields)]
149pub struct KwargSpec {
150 #[serde(default)]
152 pub nargs: NArgs,
153 #[serde(default)]
155 pub kwargs: IndexMap<String, KwargSpec>,
156 #[serde(default)]
158 pub flags: IndexSet<String>,
159 #[serde(default)]
162 pub sortable: bool,
163 #[serde(default)]
171 pub no_autosort: bool,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
176#[serde(deny_unknown_fields)]
177pub struct CommandForm {
178 #[serde(default)]
180 pub pargs: NArgs,
181 #[serde(default)]
183 pub kwargs: IndexMap<String, KwargSpec>,
184 #[serde(default)]
186 pub flags: IndexSet<String>,
187 #[serde(default)]
192 pub layout: Option<LayoutOverrides>,
193}
194
195impl Default for CommandForm {
196 fn default() -> Self {
197 Self {
198 pargs: NArgs::ZeroOrMore,
199 kwargs: IndexMap::new(),
200 flags: IndexSet::new(),
201 layout: None,
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
207#[serde(untagged)]
208pub enum CommandSpec {
209 Discriminated {
215 forms: IndexMap<String, CommandForm>,
217 #[serde(default)]
219 fallback: Option<CommandForm>,
220 },
221 Single(CommandForm),
225}
226
227impl CommandSpec {
228 pub fn form_for(&self, first_arg: Option<&str>) -> &CommandForm {
233 match self {
234 CommandSpec::Single(form) => form,
235 CommandSpec::Discriminated { forms, fallback } => {
236 let key = first_arg.unwrap_or_default();
237 forms
238 .get(key)
239 .or_else(|| {
240 has_ascii_lowercase(key)
241 .then(|| key.to_ascii_uppercase())
242 .and_then(|normalized| forms.get(&normalized))
243 })
244 .or(fallback.as_ref())
245 .or_else(|| forms.values().next())
246 .unwrap_or_else(|| empty_command_form())
253 }
254 }
255 }
256}
257
258fn empty_command_form() -> &'static CommandForm {
259 static EMPTY: std::sync::OnceLock<CommandForm> = std::sync::OnceLock::new();
260 EMPTY.get_or_init(CommandForm::default)
261}
262
263pub(crate) fn has_ascii_lowercase(s: &str) -> bool {
264 s.bytes().any(|byte| byte.is_ascii_lowercase())
265}
266
267pub(crate) fn has_ascii_uppercase(s: &str) -> bool {
268 s.bytes().any(|byte| byte.is_ascii_uppercase())
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
272pub(crate) struct SpecMetadata {
273 #[serde(default)]
275 pub cmake_version: String,
276 #[serde(default)]
278 pub audited_at: String,
279 #[serde(default)]
281 pub notes: String,
282}
283
284#[derive(Debug, Default, Deserialize, Serialize)]
286pub(crate) struct SpecFile {
287 #[serde(default)]
289 pub metadata: SpecMetadata,
290 #[serde(default)]
292 pub commands: IndexMap<String, CommandSpec>,
293}
294
295#[derive(Debug, Clone, Default, Deserialize, Serialize)]
298#[serde(deny_unknown_fields)]
299pub(crate) struct LayoutOverridesOverride {
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub line_width: Option<usize>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub tab_size: Option<usize>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub dangle_parens: Option<bool>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub always_wrap: Option<bool>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub max_pargs_hwrap: Option<usize>,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 pub wrap_after_first_arg: Option<bool>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub continuation_align: Option<crate::config::ContinuationAlign>,
321}
322
323#[derive(Debug, Clone, Default, Deserialize, Serialize)]
325#[serde(deny_unknown_fields)]
326pub(crate) struct KwargSpecOverride {
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub nargs: Option<NArgs>,
330 #[serde(default)]
332 #[serde(skip_serializing_if = "IndexMap::is_empty")]
333 pub kwargs: IndexMap<String, KwargSpecOverride>,
334 #[serde(default)]
336 #[serde(skip_serializing_if = "IndexSet::is_empty")]
337 pub flags: IndexSet<String>,
338 #[serde(default)]
340 pub sortable: bool,
341 #[serde(default)]
343 pub no_autosort: bool,
344}
345
346#[derive(Debug, Clone, Default, Deserialize, Serialize)]
348#[serde(deny_unknown_fields)]
349pub(crate) struct CommandFormOverride {
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub pargs: Option<NArgs>,
353 #[serde(default)]
355 #[serde(skip_serializing_if = "IndexMap::is_empty")]
356 pub kwargs: IndexMap<String, KwargSpecOverride>,
357 #[serde(default)]
359 #[serde(skip_serializing_if = "IndexSet::is_empty")]
360 pub flags: IndexSet<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub layout: Option<LayoutOverridesOverride>,
364}
365
366#[derive(Debug, Clone, Deserialize, Serialize)]
368#[serde(untagged)]
369pub(crate) enum CommandSpecOverride {
370 Single(CommandFormOverride),
372 Discriminated {
374 #[serde(default)]
376 #[serde(skip_serializing_if = "IndexMap::is_empty")]
377 forms: IndexMap<String, CommandFormOverride>,
378 #[serde(default)]
380 #[serde(skip_serializing_if = "Option::is_none")]
381 fallback: Option<CommandFormOverride>,
382 },
383}
384
385#[derive(Debug, Default, Deserialize, Serialize)]
387pub(crate) struct SpecOverrideFile {
388 #[serde(default)]
390 pub commands: IndexMap<String, CommandSpecOverride>,
391}
392
393impl CommandSpecOverride {
394 pub(crate) fn into_full_spec(self) -> CommandSpec {
397 match self {
398 CommandSpecOverride::Single(form) => CommandSpec::Single(form.into_full_form()),
399 CommandSpecOverride::Discriminated { forms, fallback } => CommandSpec::Discriminated {
400 forms: forms
401 .into_iter()
402 .map(|(name, form)| (name.to_ascii_uppercase(), form.into_full_form()))
403 .collect(),
404 fallback: fallback.map(CommandFormOverride::into_full_form),
405 },
406 }
407 }
408}
409
410impl CommandFormOverride {
411 pub(crate) fn into_full_form(self) -> CommandForm {
413 CommandForm {
414 pargs: self.pargs.unwrap_or_default(),
415 kwargs: self
416 .kwargs
417 .into_iter()
418 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
419 .collect(),
420 flags: self
421 .flags
422 .into_iter()
423 .map(|flag| flag.to_ascii_uppercase())
424 .collect(),
425 layout: self.layout.map(LayoutOverridesOverride::into_full_layout),
426 }
427 }
428}
429
430impl KwargSpecOverride {
431 pub(crate) fn into_full_spec(self) -> KwargSpec {
433 KwargSpec {
434 nargs: self.nargs.unwrap_or_default(),
435 kwargs: self
436 .kwargs
437 .into_iter()
438 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
439 .collect(),
440 flags: self
441 .flags
442 .into_iter()
443 .map(|flag| flag.to_ascii_uppercase())
444 .collect(),
445 sortable: self.sortable,
446 no_autosort: self.no_autosort,
447 }
448 }
449}
450
451impl LayoutOverridesOverride {
452 pub(crate) fn into_full_layout(self) -> LayoutOverrides {
454 LayoutOverrides {
455 line_width: self.line_width,
456 tab_size: self.tab_size,
457 dangle_parens: self.dangle_parens,
458 always_wrap: self.always_wrap,
459 max_pargs_hwrap: self.max_pargs_hwrap,
460 wrap_after_first_arg: self.wrap_after_first_arg,
461 continuation_align: self.continuation_align,
462 }
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn nargs_serialize_round_trip() {
472 let values = [
473 NArgs::Fixed(3),
474 NArgs::ZeroOrMore,
475 NArgs::OneOrMore,
476 NArgs::Optional,
477 NArgs::AtLeast(2),
478 ];
479 for value in values {
480 let encoded = serde_json::to_string(&value).unwrap();
481 let decoded: NArgs = serde_json::from_str(&encoded).unwrap();
482 assert_eq!(decoded, value);
483 }
484 }
485
486 #[test]
487 fn nargs_invalid_pattern_is_rejected() {
488 let err = toml::from_str::<KwargSpec>("nargs = \"abc+\"\n").unwrap_err();
489 assert!(err.to_string().contains("invalid NArgs pattern"));
490 }
491
492 #[test]
493 fn nargs_integer() {
494 let src = "nargs = 1\n";
495 let spec: KwargSpec = toml::from_str(src).unwrap();
496 assert_eq!(spec.nargs, NArgs::Fixed(1));
497 }
498
499 #[test]
500 fn nargs_zero_or_more() {
501 let src = "nargs = \"*\"\n";
502 let spec: KwargSpec = toml::from_str(src).unwrap();
503 assert_eq!(spec.nargs, NArgs::ZeroOrMore);
504 }
505
506 #[test]
507 fn nargs_one_or_more() {
508 let src = "nargs = \"+\"\n";
509 let spec: KwargSpec = toml::from_str(src).unwrap();
510 assert_eq!(spec.nargs, NArgs::OneOrMore);
511 }
512
513 #[test]
514 fn nargs_optional() {
515 let src = "nargs = \"?\"\n";
516 let spec: KwargSpec = toml::from_str(src).unwrap();
517 assert_eq!(spec.nargs, NArgs::Optional);
518 }
519
520 #[test]
521 fn nargs_at_least() {
522 let src = "nargs = \"2+\"\n";
523 let spec: KwargSpec = toml::from_str(src).unwrap();
524 assert_eq!(spec.nargs, NArgs::AtLeast(2));
525 }
526
527 #[test]
528 fn single_command_form() {
529 let src = r#"
530pargs = 1
531flags = ["REQUIRED"]
532
533[kwargs.COMPONENTS]
534nargs = "+"
535"#;
536 let form: CommandForm = toml::from_str(src).unwrap();
537 assert_eq!(form.pargs, NArgs::Fixed(1));
538 assert!(form.flags.contains("REQUIRED"));
539 assert!(form.kwargs.contains_key("COMPONENTS"));
540 }
541
542 #[test]
543 fn discriminated_command() {
544 let src = r#"
545[forms.TARGETS]
546pargs = "+"
547
548[forms.TARGETS.kwargs.DESTINATION]
549nargs = 1
550
551[forms.FILES]
552pargs = "+"
553"#;
554 let spec: CommandSpec = toml::from_str(src).unwrap();
555 assert!(matches!(spec, CommandSpec::Discriminated { .. }));
556 let form = spec.form_for(Some("targets"));
557 assert!(form.kwargs.contains_key("DESTINATION"));
558 }
559
560 #[test]
561 fn discriminated_command_uses_fallback_when_no_key_matches() {
562 let src = r#"
563[forms.FILE]
564pargs = 1
565
566[fallback]
567pargs = 2
568"#;
569 let spec: CommandSpec = toml::from_str(src).unwrap();
570 let form = spec.form_for(Some("unknown"));
571 assert_eq!(form.pargs, NArgs::Fixed(2));
572 }
573
574 #[test]
575 fn command_spec_override_into_full_spec_normalizes_casing() {
576 let override_spec = CommandSpecOverride::Single(CommandFormOverride {
577 pargs: Some(NArgs::Fixed(1)),
578 flags: ["quiet".to_owned()].into_iter().collect(),
579 kwargs: [(
580 "sources".to_owned(),
581 KwargSpecOverride {
582 nargs: Some(NArgs::OneOrMore),
583 ..KwargSpecOverride::default()
584 },
585 )]
586 .into_iter()
587 .collect(),
588 layout: Some(LayoutOverridesOverride {
589 always_wrap: Some(true),
590 ..LayoutOverridesOverride::default()
591 }),
592 });
593
594 let full = override_spec.into_full_spec();
595 let form = full.form_for(None);
596 assert!(form.flags.contains("QUIET"));
597 assert!(form.kwargs.contains_key("SOURCES"));
598 assert_eq!(form.kwargs["SOURCES"].nargs, NArgs::OneOrMore);
599 assert_eq!(form.layout.as_ref().unwrap().always_wrap, Some(true));
600 }
601
602 #[test]
603 fn partial_override_round_trips() {
604 let src = r#"
605layout.always_wrap = true
606
607[kwargs.COMPONENTS]
608nargs = "+"
609"#;
610 let override_form: CommandFormOverride = toml::from_str(src).unwrap();
611 assert_eq!(override_form.layout.unwrap().always_wrap, Some(true));
612 assert_eq!(
613 override_form.kwargs["COMPONENTS"].nargs,
614 Some(NArgs::OneOrMore)
615 );
616 }
617}