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}
164
165#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
167#[serde(deny_unknown_fields)]
168pub struct CommandForm {
169 #[serde(default)]
171 pub pargs: NArgs,
172 #[serde(default)]
174 pub kwargs: IndexMap<String, KwargSpec>,
175 #[serde(default)]
177 pub flags: IndexSet<String>,
178 #[serde(default)]
183 pub layout: Option<LayoutOverrides>,
184}
185
186impl Default for CommandForm {
187 fn default() -> Self {
188 Self {
189 pargs: NArgs::ZeroOrMore,
190 kwargs: IndexMap::new(),
191 flags: IndexSet::new(),
192 layout: None,
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
198#[serde(untagged)]
199pub enum CommandSpec {
200 Discriminated {
206 forms: IndexMap<String, CommandForm>,
208 #[serde(default)]
210 fallback: Option<CommandForm>,
211 },
212 Single(CommandForm),
216}
217
218impl CommandSpec {
219 pub fn form_for(&self, first_arg: Option<&str>) -> &CommandForm {
224 match self {
225 CommandSpec::Single(form) => form,
226 CommandSpec::Discriminated { forms, fallback } => {
227 let key = first_arg.unwrap_or_default();
228 forms
229 .get(key)
230 .or_else(|| {
231 has_ascii_lowercase(key)
232 .then(|| key.to_ascii_uppercase())
233 .and_then(|normalized| forms.get(&normalized))
234 })
235 .or(fallback.as_ref())
236 .unwrap_or_else(|| {
237 forms
238 .values()
239 .next()
240 .expect("discriminated spec has a form")
241 })
242 }
243 }
244 }
245}
246
247fn has_ascii_lowercase(s: &str) -> bool {
248 s.bytes().any(|byte| byte.is_ascii_lowercase())
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
252pub(crate) struct SpecMetadata {
253 #[serde(default)]
255 pub cmake_version: String,
256 #[serde(default)]
258 pub audited_at: String,
259 #[serde(default)]
261 pub notes: String,
262}
263
264#[derive(Debug, Default, Deserialize, Serialize)]
266pub(crate) struct SpecFile {
267 #[serde(default)]
269 pub metadata: SpecMetadata,
270 #[serde(default)]
272 pub commands: IndexMap<String, CommandSpec>,
273}
274
275#[derive(Debug, Clone, Default, Deserialize, Serialize)]
278#[serde(deny_unknown_fields)]
279pub(crate) struct LayoutOverridesOverride {
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub line_width: Option<usize>,
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub tab_size: Option<usize>,
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub dangle_parens: Option<bool>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub always_wrap: Option<bool>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub max_pargs_hwrap: Option<usize>,
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub wrap_after_first_arg: Option<bool>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub continuation_align: Option<crate::config::ContinuationAlign>,
301}
302
303#[derive(Debug, Clone, Default, Deserialize, Serialize)]
305#[serde(deny_unknown_fields)]
306pub(crate) struct KwargSpecOverride {
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub nargs: Option<NArgs>,
310 #[serde(default)]
312 #[serde(skip_serializing_if = "IndexMap::is_empty")]
313 pub kwargs: IndexMap<String, KwargSpecOverride>,
314 #[serde(default)]
316 #[serde(skip_serializing_if = "IndexSet::is_empty")]
317 pub flags: IndexSet<String>,
318 #[serde(default)]
320 pub sortable: bool,
321}
322
323#[derive(Debug, Clone, Default, Deserialize, Serialize)]
325#[serde(deny_unknown_fields)]
326pub(crate) struct CommandFormOverride {
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub pargs: 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(skip_serializing_if = "Option::is_none")]
340 pub layout: Option<LayoutOverridesOverride>,
341}
342
343#[derive(Debug, Clone, Deserialize, Serialize)]
345#[serde(untagged)]
346pub(crate) enum CommandSpecOverride {
347 Single(CommandFormOverride),
349 Discriminated {
351 #[serde(default)]
353 #[serde(skip_serializing_if = "IndexMap::is_empty")]
354 forms: IndexMap<String, CommandFormOverride>,
355 #[serde(default)]
357 #[serde(skip_serializing_if = "Option::is_none")]
358 fallback: Option<CommandFormOverride>,
359 },
360}
361
362#[derive(Debug, Default, Deserialize, Serialize)]
364pub(crate) struct SpecOverrideFile {
365 #[serde(default)]
367 pub commands: IndexMap<String, CommandSpecOverride>,
368}
369
370impl CommandSpecOverride {
371 pub(crate) fn into_full_spec(self) -> CommandSpec {
374 match self {
375 CommandSpecOverride::Single(form) => CommandSpec::Single(form.into_full_form()),
376 CommandSpecOverride::Discriminated { forms, fallback } => CommandSpec::Discriminated {
377 forms: forms
378 .into_iter()
379 .map(|(name, form)| (name.to_ascii_uppercase(), form.into_full_form()))
380 .collect(),
381 fallback: fallback.map(CommandFormOverride::into_full_form),
382 },
383 }
384 }
385}
386
387impl CommandFormOverride {
388 pub(crate) fn into_full_form(self) -> CommandForm {
390 CommandForm {
391 pargs: self.pargs.unwrap_or_default(),
392 kwargs: self
393 .kwargs
394 .into_iter()
395 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
396 .collect(),
397 flags: self
398 .flags
399 .into_iter()
400 .map(|flag| flag.to_ascii_uppercase())
401 .collect(),
402 layout: self.layout.map(LayoutOverridesOverride::into_full_layout),
403 }
404 }
405}
406
407impl KwargSpecOverride {
408 pub(crate) fn into_full_spec(self) -> KwargSpec {
410 KwargSpec {
411 nargs: self.nargs.unwrap_or_default(),
412 kwargs: self
413 .kwargs
414 .into_iter()
415 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
416 .collect(),
417 flags: self
418 .flags
419 .into_iter()
420 .map(|flag| flag.to_ascii_uppercase())
421 .collect(),
422 sortable: self.sortable,
423 }
424 }
425}
426
427impl LayoutOverridesOverride {
428 pub(crate) fn into_full_layout(self) -> LayoutOverrides {
430 LayoutOverrides {
431 line_width: self.line_width,
432 tab_size: self.tab_size,
433 dangle_parens: self.dangle_parens,
434 always_wrap: self.always_wrap,
435 max_pargs_hwrap: self.max_pargs_hwrap,
436 wrap_after_first_arg: self.wrap_after_first_arg,
437 continuation_align: self.continuation_align,
438 }
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn nargs_serialize_round_trip() {
448 let values = [
449 NArgs::Fixed(3),
450 NArgs::ZeroOrMore,
451 NArgs::OneOrMore,
452 NArgs::Optional,
453 NArgs::AtLeast(2),
454 ];
455 for value in values {
456 let encoded = serde_json::to_string(&value).unwrap();
457 let decoded: NArgs = serde_json::from_str(&encoded).unwrap();
458 assert_eq!(decoded, value);
459 }
460 }
461
462 #[test]
463 fn nargs_invalid_pattern_is_rejected() {
464 let err = toml::from_str::<KwargSpec>("nargs = \"abc+\"\n").unwrap_err();
465 assert!(err.to_string().contains("invalid NArgs pattern"));
466 }
467
468 #[test]
469 fn nargs_integer() {
470 let src = "nargs = 1\n";
471 let spec: KwargSpec = toml::from_str(src).unwrap();
472 assert_eq!(spec.nargs, NArgs::Fixed(1));
473 }
474
475 #[test]
476 fn nargs_zero_or_more() {
477 let src = "nargs = \"*\"\n";
478 let spec: KwargSpec = toml::from_str(src).unwrap();
479 assert_eq!(spec.nargs, NArgs::ZeroOrMore);
480 }
481
482 #[test]
483 fn nargs_one_or_more() {
484 let src = "nargs = \"+\"\n";
485 let spec: KwargSpec = toml::from_str(src).unwrap();
486 assert_eq!(spec.nargs, NArgs::OneOrMore);
487 }
488
489 #[test]
490 fn nargs_optional() {
491 let src = "nargs = \"?\"\n";
492 let spec: KwargSpec = toml::from_str(src).unwrap();
493 assert_eq!(spec.nargs, NArgs::Optional);
494 }
495
496 #[test]
497 fn nargs_at_least() {
498 let src = "nargs = \"2+\"\n";
499 let spec: KwargSpec = toml::from_str(src).unwrap();
500 assert_eq!(spec.nargs, NArgs::AtLeast(2));
501 }
502
503 #[test]
504 fn single_command_form() {
505 let src = r#"
506pargs = 1
507flags = ["REQUIRED"]
508
509[kwargs.COMPONENTS]
510nargs = "+"
511"#;
512 let form: CommandForm = toml::from_str(src).unwrap();
513 assert_eq!(form.pargs, NArgs::Fixed(1));
514 assert!(form.flags.contains("REQUIRED"));
515 assert!(form.kwargs.contains_key("COMPONENTS"));
516 }
517
518 #[test]
519 fn discriminated_command() {
520 let src = r#"
521[forms.TARGETS]
522pargs = "+"
523
524[forms.TARGETS.kwargs.DESTINATION]
525nargs = 1
526
527[forms.FILES]
528pargs = "+"
529"#;
530 let spec: CommandSpec = toml::from_str(src).unwrap();
531 assert!(matches!(spec, CommandSpec::Discriminated { .. }));
532 let form = spec.form_for(Some("targets"));
533 assert!(form.kwargs.contains_key("DESTINATION"));
534 }
535
536 #[test]
537 fn discriminated_command_uses_fallback_when_no_key_matches() {
538 let src = r#"
539[forms.FILE]
540pargs = 1
541
542[fallback]
543pargs = 2
544"#;
545 let spec: CommandSpec = toml::from_str(src).unwrap();
546 let form = spec.form_for(Some("unknown"));
547 assert_eq!(form.pargs, NArgs::Fixed(2));
548 }
549
550 #[test]
551 fn command_spec_override_into_full_spec_normalizes_casing() {
552 let override_spec = CommandSpecOverride::Single(CommandFormOverride {
553 pargs: Some(NArgs::Fixed(1)),
554 flags: ["quiet".to_owned()].into_iter().collect(),
555 kwargs: [(
556 "sources".to_owned(),
557 KwargSpecOverride {
558 nargs: Some(NArgs::OneOrMore),
559 ..KwargSpecOverride::default()
560 },
561 )]
562 .into_iter()
563 .collect(),
564 layout: Some(LayoutOverridesOverride {
565 always_wrap: Some(true),
566 ..LayoutOverridesOverride::default()
567 }),
568 });
569
570 let full = override_spec.into_full_spec();
571 let form = full.form_for(None);
572 assert!(form.flags.contains("QUIET"));
573 assert!(form.kwargs.contains_key("SOURCES"));
574 assert_eq!(form.kwargs["SOURCES"].nargs, NArgs::OneOrMore);
575 assert_eq!(form.layout.as_ref().unwrap().always_wrap, Some(true));
576 }
577
578 #[test]
579 fn partial_override_round_trips() {
580 let src = r#"
581layout.always_wrap = true
582
583[kwargs.COMPONENTS]
584nargs = "+"
585"#;
586 let override_form: CommandFormOverride = toml::from_str(src).unwrap();
587 assert_eq!(override_form.layout.unwrap().always_wrap, Some(true));
588 assert_eq!(
589 override_form.kwargs["COMPONENTS"].nargs,
590 Some(NArgs::OneOrMore)
591 );
592 }
593}