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 .or_else(|| forms.values().next())
237 .unwrap_or_else(|| empty_command_form())
244 }
245 }
246 }
247}
248
249fn empty_command_form() -> &'static CommandForm {
250 static EMPTY: std::sync::OnceLock<CommandForm> = std::sync::OnceLock::new();
251 EMPTY.get_or_init(CommandForm::default)
252}
253
254pub(crate) fn has_ascii_lowercase(s: &str) -> bool {
255 s.bytes().any(|byte| byte.is_ascii_lowercase())
256}
257
258pub(crate) fn has_ascii_uppercase(s: &str) -> bool {
259 s.bytes().any(|byte| byte.is_ascii_uppercase())
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
263pub(crate) struct SpecMetadata {
264 #[serde(default)]
266 pub cmake_version: String,
267 #[serde(default)]
269 pub audited_at: String,
270 #[serde(default)]
272 pub notes: String,
273}
274
275#[derive(Debug, Default, Deserialize, Serialize)]
277pub(crate) struct SpecFile {
278 #[serde(default)]
280 pub metadata: SpecMetadata,
281 #[serde(default)]
283 pub commands: IndexMap<String, CommandSpec>,
284}
285
286#[derive(Debug, Clone, Default, Deserialize, Serialize)]
289#[serde(deny_unknown_fields)]
290pub(crate) struct LayoutOverridesOverride {
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub line_width: Option<usize>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub tab_size: Option<usize>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub dangle_parens: Option<bool>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub always_wrap: Option<bool>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub max_pargs_hwrap: Option<usize>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub wrap_after_first_arg: Option<bool>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub continuation_align: Option<crate::config::ContinuationAlign>,
312}
313
314#[derive(Debug, Clone, Default, Deserialize, Serialize)]
316#[serde(deny_unknown_fields)]
317pub(crate) struct KwargSpecOverride {
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub nargs: Option<NArgs>,
321 #[serde(default)]
323 #[serde(skip_serializing_if = "IndexMap::is_empty")]
324 pub kwargs: IndexMap<String, KwargSpecOverride>,
325 #[serde(default)]
327 #[serde(skip_serializing_if = "IndexSet::is_empty")]
328 pub flags: IndexSet<String>,
329 #[serde(default)]
331 pub sortable: bool,
332}
333
334#[derive(Debug, Clone, Default, Deserialize, Serialize)]
336#[serde(deny_unknown_fields)]
337pub(crate) struct CommandFormOverride {
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub pargs: Option<NArgs>,
341 #[serde(default)]
343 #[serde(skip_serializing_if = "IndexMap::is_empty")]
344 pub kwargs: IndexMap<String, KwargSpecOverride>,
345 #[serde(default)]
347 #[serde(skip_serializing_if = "IndexSet::is_empty")]
348 pub flags: IndexSet<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub layout: Option<LayoutOverridesOverride>,
352}
353
354#[derive(Debug, Clone, Deserialize, Serialize)]
356#[serde(untagged)]
357pub(crate) enum CommandSpecOverride {
358 Single(CommandFormOverride),
360 Discriminated {
362 #[serde(default)]
364 #[serde(skip_serializing_if = "IndexMap::is_empty")]
365 forms: IndexMap<String, CommandFormOverride>,
366 #[serde(default)]
368 #[serde(skip_serializing_if = "Option::is_none")]
369 fallback: Option<CommandFormOverride>,
370 },
371}
372
373#[derive(Debug, Default, Deserialize, Serialize)]
375pub(crate) struct SpecOverrideFile {
376 #[serde(default)]
378 pub commands: IndexMap<String, CommandSpecOverride>,
379}
380
381impl CommandSpecOverride {
382 pub(crate) fn into_full_spec(self) -> CommandSpec {
385 match self {
386 CommandSpecOverride::Single(form) => CommandSpec::Single(form.into_full_form()),
387 CommandSpecOverride::Discriminated { forms, fallback } => CommandSpec::Discriminated {
388 forms: forms
389 .into_iter()
390 .map(|(name, form)| (name.to_ascii_uppercase(), form.into_full_form()))
391 .collect(),
392 fallback: fallback.map(CommandFormOverride::into_full_form),
393 },
394 }
395 }
396}
397
398impl CommandFormOverride {
399 pub(crate) fn into_full_form(self) -> CommandForm {
401 CommandForm {
402 pargs: self.pargs.unwrap_or_default(),
403 kwargs: self
404 .kwargs
405 .into_iter()
406 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
407 .collect(),
408 flags: self
409 .flags
410 .into_iter()
411 .map(|flag| flag.to_ascii_uppercase())
412 .collect(),
413 layout: self.layout.map(LayoutOverridesOverride::into_full_layout),
414 }
415 }
416}
417
418impl KwargSpecOverride {
419 pub(crate) fn into_full_spec(self) -> KwargSpec {
421 KwargSpec {
422 nargs: self.nargs.unwrap_or_default(),
423 kwargs: self
424 .kwargs
425 .into_iter()
426 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
427 .collect(),
428 flags: self
429 .flags
430 .into_iter()
431 .map(|flag| flag.to_ascii_uppercase())
432 .collect(),
433 sortable: self.sortable,
434 }
435 }
436}
437
438impl LayoutOverridesOverride {
439 pub(crate) fn into_full_layout(self) -> LayoutOverrides {
441 LayoutOverrides {
442 line_width: self.line_width,
443 tab_size: self.tab_size,
444 dangle_parens: self.dangle_parens,
445 always_wrap: self.always_wrap,
446 max_pargs_hwrap: self.max_pargs_hwrap,
447 wrap_after_first_arg: self.wrap_after_first_arg,
448 continuation_align: self.continuation_align,
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn nargs_serialize_round_trip() {
459 let values = [
460 NArgs::Fixed(3),
461 NArgs::ZeroOrMore,
462 NArgs::OneOrMore,
463 NArgs::Optional,
464 NArgs::AtLeast(2),
465 ];
466 for value in values {
467 let encoded = serde_json::to_string(&value).unwrap();
468 let decoded: NArgs = serde_json::from_str(&encoded).unwrap();
469 assert_eq!(decoded, value);
470 }
471 }
472
473 #[test]
474 fn nargs_invalid_pattern_is_rejected() {
475 let err = toml::from_str::<KwargSpec>("nargs = \"abc+\"\n").unwrap_err();
476 assert!(err.to_string().contains("invalid NArgs pattern"));
477 }
478
479 #[test]
480 fn nargs_integer() {
481 let src = "nargs = 1\n";
482 let spec: KwargSpec = toml::from_str(src).unwrap();
483 assert_eq!(spec.nargs, NArgs::Fixed(1));
484 }
485
486 #[test]
487 fn nargs_zero_or_more() {
488 let src = "nargs = \"*\"\n";
489 let spec: KwargSpec = toml::from_str(src).unwrap();
490 assert_eq!(spec.nargs, NArgs::ZeroOrMore);
491 }
492
493 #[test]
494 fn nargs_one_or_more() {
495 let src = "nargs = \"+\"\n";
496 let spec: KwargSpec = toml::from_str(src).unwrap();
497 assert_eq!(spec.nargs, NArgs::OneOrMore);
498 }
499
500 #[test]
501 fn nargs_optional() {
502 let src = "nargs = \"?\"\n";
503 let spec: KwargSpec = toml::from_str(src).unwrap();
504 assert_eq!(spec.nargs, NArgs::Optional);
505 }
506
507 #[test]
508 fn nargs_at_least() {
509 let src = "nargs = \"2+\"\n";
510 let spec: KwargSpec = toml::from_str(src).unwrap();
511 assert_eq!(spec.nargs, NArgs::AtLeast(2));
512 }
513
514 #[test]
515 fn single_command_form() {
516 let src = r#"
517pargs = 1
518flags = ["REQUIRED"]
519
520[kwargs.COMPONENTS]
521nargs = "+"
522"#;
523 let form: CommandForm = toml::from_str(src).unwrap();
524 assert_eq!(form.pargs, NArgs::Fixed(1));
525 assert!(form.flags.contains("REQUIRED"));
526 assert!(form.kwargs.contains_key("COMPONENTS"));
527 }
528
529 #[test]
530 fn discriminated_command() {
531 let src = r#"
532[forms.TARGETS]
533pargs = "+"
534
535[forms.TARGETS.kwargs.DESTINATION]
536nargs = 1
537
538[forms.FILES]
539pargs = "+"
540"#;
541 let spec: CommandSpec = toml::from_str(src).unwrap();
542 assert!(matches!(spec, CommandSpec::Discriminated { .. }));
543 let form = spec.form_for(Some("targets"));
544 assert!(form.kwargs.contains_key("DESTINATION"));
545 }
546
547 #[test]
548 fn discriminated_command_uses_fallback_when_no_key_matches() {
549 let src = r#"
550[forms.FILE]
551pargs = 1
552
553[fallback]
554pargs = 2
555"#;
556 let spec: CommandSpec = toml::from_str(src).unwrap();
557 let form = spec.form_for(Some("unknown"));
558 assert_eq!(form.pargs, NArgs::Fixed(2));
559 }
560
561 #[test]
562 fn command_spec_override_into_full_spec_normalizes_casing() {
563 let override_spec = CommandSpecOverride::Single(CommandFormOverride {
564 pargs: Some(NArgs::Fixed(1)),
565 flags: ["quiet".to_owned()].into_iter().collect(),
566 kwargs: [(
567 "sources".to_owned(),
568 KwargSpecOverride {
569 nargs: Some(NArgs::OneOrMore),
570 ..KwargSpecOverride::default()
571 },
572 )]
573 .into_iter()
574 .collect(),
575 layout: Some(LayoutOverridesOverride {
576 always_wrap: Some(true),
577 ..LayoutOverridesOverride::default()
578 }),
579 });
580
581 let full = override_spec.into_full_spec();
582 let form = full.form_for(None);
583 assert!(form.flags.contains("QUIET"));
584 assert!(form.kwargs.contains_key("SOURCES"));
585 assert_eq!(form.kwargs["SOURCES"].nargs, NArgs::OneOrMore);
586 assert_eq!(form.layout.as_ref().unwrap().always_wrap, Some(true));
587 }
588
589 #[test]
590 fn partial_override_round_trips() {
591 let src = r#"
592layout.always_wrap = true
593
594[kwargs.COMPONENTS]
595nargs = "+"
596"#;
597 let override_form: CommandFormOverride = toml::from_str(src).unwrap();
598 assert_eq!(override_form.layout.unwrap().always_wrap, Some(true));
599 assert_eq!(
600 override_form.kwargs["COMPONENTS"].nargs,
601 Some(NArgs::OneOrMore)
602 );
603 }
604}