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