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