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)]
97#[serde(deny_unknown_fields)]
98pub struct LayoutOverrides {
99 pub line_width: Option<usize>,
101 pub tab_size: Option<usize>,
103 pub dangle_parens: Option<bool>,
105 pub always_wrap: Option<bool>,
107 pub max_pargs_hwrap: Option<usize>,
109 pub wrap_after_first_arg: Option<bool>,
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct KwargSpec {
120 #[serde(default)]
122 pub nargs: NArgs,
123 #[serde(default)]
125 pub kwargs: IndexMap<String, KwargSpec>,
126 #[serde(default)]
128 pub flags: IndexSet<String>,
129 #[serde(default)]
132 pub sortable: bool,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct CommandForm {
139 #[serde(default)]
141 pub pargs: NArgs,
142 #[serde(default)]
144 pub kwargs: IndexMap<String, KwargSpec>,
145 #[serde(default)]
147 pub flags: IndexSet<String>,
148 #[serde(default)]
150 pub layout: Option<LayoutOverrides>,
151}
152
153impl Default for CommandForm {
154 fn default() -> Self {
155 Self {
156 pargs: NArgs::ZeroOrMore,
157 kwargs: IndexMap::new(),
158 flags: IndexSet::new(),
159 layout: None,
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
165#[serde(untagged)]
166pub enum CommandSpec {
167 Discriminated {
170 forms: IndexMap<String, CommandForm>,
172 #[serde(default)]
174 fallback: Option<CommandForm>,
175 },
176 Single(CommandForm),
178}
179
180impl CommandSpec {
181 pub fn form_for(&self, first_arg: Option<&str>) -> &CommandForm {
186 match self {
187 CommandSpec::Single(form) => form,
188 CommandSpec::Discriminated { forms, fallback } => {
189 let key = first_arg.unwrap_or_default();
190 forms
191 .get(key)
192 .or_else(|| {
193 has_ascii_lowercase(key)
194 .then(|| key.to_ascii_uppercase())
195 .and_then(|normalized| forms.get(&normalized))
196 })
197 .or(fallback.as_ref())
198 .unwrap_or_else(|| {
199 forms
200 .values()
201 .next()
202 .expect("discriminated spec has a form")
203 })
204 }
205 }
206 }
207}
208
209fn has_ascii_lowercase(s: &str) -> bool {
210 s.bytes().any(|byte| byte.is_ascii_lowercase())
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
214pub(crate) struct SpecMetadata {
215 #[serde(default)]
217 pub cmake_version: String,
218 #[serde(default)]
220 pub audited_at: String,
221 #[serde(default)]
223 pub notes: String,
224}
225
226#[derive(Debug, Default, Deserialize)]
228pub(crate) struct SpecFile {
229 #[serde(default)]
231 pub metadata: SpecMetadata,
232 #[serde(default)]
234 pub commands: IndexMap<String, CommandSpec>,
235}
236
237#[derive(Debug, Clone, Default, Deserialize, Serialize)]
240#[serde(deny_unknown_fields)]
241pub(crate) struct LayoutOverridesOverride {
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub line_width: Option<usize>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub tab_size: Option<usize>,
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub dangle_parens: Option<bool>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub always_wrap: Option<bool>,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub max_pargs_hwrap: Option<usize>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub wrap_after_first_arg: Option<bool>,
260}
261
262#[derive(Debug, Clone, Default, Deserialize, Serialize)]
264#[serde(deny_unknown_fields)]
265pub(crate) struct KwargSpecOverride {
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub nargs: Option<NArgs>,
269 #[serde(default)]
271 #[serde(skip_serializing_if = "IndexMap::is_empty")]
272 pub kwargs: IndexMap<String, KwargSpecOverride>,
273 #[serde(default)]
275 #[serde(skip_serializing_if = "IndexSet::is_empty")]
276 pub flags: IndexSet<String>,
277 #[serde(default)]
279 pub sortable: bool,
280}
281
282#[derive(Debug, Clone, Default, Deserialize, Serialize)]
284#[serde(deny_unknown_fields)]
285pub(crate) struct CommandFormOverride {
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub pargs: Option<NArgs>,
289 #[serde(default)]
291 #[serde(skip_serializing_if = "IndexMap::is_empty")]
292 pub kwargs: IndexMap<String, KwargSpecOverride>,
293 #[serde(default)]
295 #[serde(skip_serializing_if = "IndexSet::is_empty")]
296 pub flags: IndexSet<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub layout: Option<LayoutOverridesOverride>,
300}
301
302#[derive(Debug, Clone, Deserialize, Serialize)]
304#[serde(untagged)]
305pub(crate) enum CommandSpecOverride {
306 Single(CommandFormOverride),
308 Discriminated {
310 #[serde(default)]
312 #[serde(skip_serializing_if = "IndexMap::is_empty")]
313 forms: IndexMap<String, CommandFormOverride>,
314 #[serde(default)]
316 #[serde(skip_serializing_if = "Option::is_none")]
317 fallback: Option<CommandFormOverride>,
318 },
319}
320
321#[derive(Debug, Default, Deserialize, Serialize)]
323pub(crate) struct SpecOverrideFile {
324 #[serde(default)]
326 pub commands: IndexMap<String, CommandSpecOverride>,
327}
328
329impl CommandSpecOverride {
330 pub(crate) fn into_full_spec(self) -> CommandSpec {
333 match self {
334 CommandSpecOverride::Single(form) => CommandSpec::Single(form.into_full_form()),
335 CommandSpecOverride::Discriminated { forms, fallback } => CommandSpec::Discriminated {
336 forms: forms
337 .into_iter()
338 .map(|(name, form)| (name.to_ascii_uppercase(), form.into_full_form()))
339 .collect(),
340 fallback: fallback.map(CommandFormOverride::into_full_form),
341 },
342 }
343 }
344}
345
346impl CommandFormOverride {
347 pub(crate) fn into_full_form(self) -> CommandForm {
349 CommandForm {
350 pargs: self.pargs.unwrap_or_default(),
351 kwargs: self
352 .kwargs
353 .into_iter()
354 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
355 .collect(),
356 flags: self
357 .flags
358 .into_iter()
359 .map(|flag| flag.to_ascii_uppercase())
360 .collect(),
361 layout: self.layout.map(LayoutOverridesOverride::into_full_layout),
362 }
363 }
364}
365
366impl KwargSpecOverride {
367 pub(crate) fn into_full_spec(self) -> KwargSpec {
369 KwargSpec {
370 nargs: self.nargs.unwrap_or_default(),
371 kwargs: self
372 .kwargs
373 .into_iter()
374 .map(|(name, spec)| (name.to_ascii_uppercase(), spec.into_full_spec()))
375 .collect(),
376 flags: self
377 .flags
378 .into_iter()
379 .map(|flag| flag.to_ascii_uppercase())
380 .collect(),
381 sortable: self.sortable,
382 }
383 }
384}
385
386impl LayoutOverridesOverride {
387 pub(crate) fn into_full_layout(self) -> LayoutOverrides {
389 LayoutOverrides {
390 line_width: self.line_width,
391 tab_size: self.tab_size,
392 dangle_parens: self.dangle_parens,
393 always_wrap: self.always_wrap,
394 max_pargs_hwrap: self.max_pargs_hwrap,
395 wrap_after_first_arg: self.wrap_after_first_arg,
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn nargs_serialize_round_trip() {
406 let values = [
407 NArgs::Fixed(3),
408 NArgs::ZeroOrMore,
409 NArgs::OneOrMore,
410 NArgs::Optional,
411 NArgs::AtLeast(2),
412 ];
413 for value in values {
414 let encoded = serde_json::to_string(&value).unwrap();
415 let decoded: NArgs = serde_json::from_str(&encoded).unwrap();
416 assert_eq!(decoded, value);
417 }
418 }
419
420 #[test]
421 fn nargs_invalid_pattern_is_rejected() {
422 let err = toml::from_str::<KwargSpec>("nargs = \"abc+\"\n").unwrap_err();
423 assert!(err.to_string().contains("invalid NArgs pattern"));
424 }
425
426 #[test]
427 fn nargs_integer() {
428 let src = "nargs = 1\n";
429 let spec: KwargSpec = toml::from_str(src).unwrap();
430 assert_eq!(spec.nargs, NArgs::Fixed(1));
431 }
432
433 #[test]
434 fn nargs_zero_or_more() {
435 let src = "nargs = \"*\"\n";
436 let spec: KwargSpec = toml::from_str(src).unwrap();
437 assert_eq!(spec.nargs, NArgs::ZeroOrMore);
438 }
439
440 #[test]
441 fn nargs_one_or_more() {
442 let src = "nargs = \"+\"\n";
443 let spec: KwargSpec = toml::from_str(src).unwrap();
444 assert_eq!(spec.nargs, NArgs::OneOrMore);
445 }
446
447 #[test]
448 fn nargs_optional() {
449 let src = "nargs = \"?\"\n";
450 let spec: KwargSpec = toml::from_str(src).unwrap();
451 assert_eq!(spec.nargs, NArgs::Optional);
452 }
453
454 #[test]
455 fn nargs_at_least() {
456 let src = "nargs = \"2+\"\n";
457 let spec: KwargSpec = toml::from_str(src).unwrap();
458 assert_eq!(spec.nargs, NArgs::AtLeast(2));
459 }
460
461 #[test]
462 fn single_command_form() {
463 let src = r#"
464pargs = 1
465flags = ["REQUIRED"]
466
467[kwargs.COMPONENTS]
468nargs = "+"
469"#;
470 let form: CommandForm = toml::from_str(src).unwrap();
471 assert_eq!(form.pargs, NArgs::Fixed(1));
472 assert!(form.flags.contains("REQUIRED"));
473 assert!(form.kwargs.contains_key("COMPONENTS"));
474 }
475
476 #[test]
477 fn discriminated_command() {
478 let src = r#"
479[forms.TARGETS]
480pargs = "+"
481
482[forms.TARGETS.kwargs.DESTINATION]
483nargs = 1
484
485[forms.FILES]
486pargs = "+"
487"#;
488 let spec: CommandSpec = toml::from_str(src).unwrap();
489 assert!(matches!(spec, CommandSpec::Discriminated { .. }));
490 let form = spec.form_for(Some("targets"));
491 assert!(form.kwargs.contains_key("DESTINATION"));
492 }
493
494 #[test]
495 fn discriminated_command_uses_fallback_when_no_key_matches() {
496 let src = r#"
497[forms.FILE]
498pargs = 1
499
500[fallback]
501pargs = 2
502"#;
503 let spec: CommandSpec = toml::from_str(src).unwrap();
504 let form = spec.form_for(Some("unknown"));
505 assert_eq!(form.pargs, NArgs::Fixed(2));
506 }
507
508 #[test]
509 fn command_spec_override_into_full_spec_normalizes_casing() {
510 let override_spec = CommandSpecOverride::Single(CommandFormOverride {
511 pargs: Some(NArgs::Fixed(1)),
512 flags: ["quiet".to_owned()].into_iter().collect(),
513 kwargs: [(
514 "sources".to_owned(),
515 KwargSpecOverride {
516 nargs: Some(NArgs::OneOrMore),
517 ..KwargSpecOverride::default()
518 },
519 )]
520 .into_iter()
521 .collect(),
522 layout: Some(LayoutOverridesOverride {
523 always_wrap: Some(true),
524 ..LayoutOverridesOverride::default()
525 }),
526 });
527
528 let full = override_spec.into_full_spec();
529 let form = full.form_for(None);
530 assert!(form.flags.contains("QUIET"));
531 assert!(form.kwargs.contains_key("SOURCES"));
532 assert_eq!(form.kwargs["SOURCES"].nargs, NArgs::OneOrMore);
533 assert_eq!(form.layout.as_ref().unwrap().always_wrap, Some(true));
534 }
535
536 #[test]
537 fn partial_override_round_trips() {
538 let src = r#"
539layout.always_wrap = true
540
541[kwargs.COMPONENTS]
542nargs = "+"
543"#;
544 let override_form: CommandFormOverride = toml::from_str(src).unwrap();
545 assert_eq!(override_form.layout.unwrap().always_wrap, Some(true));
546 assert_eq!(
547 override_form.kwargs["COMPONENTS"].nargs,
548 Some(NArgs::OneOrMore)
549 );
550 }
551}