specfold_core/model/
registry.rs1use super::doctype::{self, FieldKind, Form, KeyKind, TypeDef};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
10#[serde(untagged)]
11pub enum FieldsDecl {
12 List(Vec<String>),
13 Kinds(BTreeMap<String, FieldKind>),
14}
15
16#[derive(Debug, Clone, Default, Deserialize, Serialize)]
18#[serde(deny_unknown_fields)]
19pub struct TypeOverride {
20 pub enabled: Option<bool>,
21 pub about: Option<String>,
22 pub key: Option<KeyKind>,
23 pub form: Option<Form>,
24 pub statuses: Option<Vec<String>>,
25 pub initial: Option<String>,
26 pub sealed: Option<bool>,
27 pub fields: Option<FieldsDecl>,
28 pub template: Option<String>,
29 pub transitions: Option<BTreeMap<String, Vec<String>>>,
30 pub require_when: Option<BTreeMap<String, Vec<String>>>,
31 pub sections: Option<Vec<String>>,
32 pub placeholders: Option<Vec<String>>,
33 pub distinct: Option<Vec<Vec<String>>>,
34}
35
36#[derive(Debug, Clone)]
37pub struct Registry {
38 pub types: Vec<TypeDef>,
39}
40
41fn apply(t: &mut TypeDef, o: &TypeOverride) {
42 if let Some(v) = &o.about {
43 t.about = v.clone();
44 }
45 if let Some(v) = o.key {
46 t.key = v;
47 }
48 if let Some(v) = o.form {
49 t.form = v;
50 }
51 if let Some(v) = &o.statuses {
52 t.statuses = v.clone();
53 }
54 if let Some(v) = &o.initial {
55 t.initial = v.clone();
56 }
57 if let Some(v) = o.sealed {
58 t.sealed = v;
59 }
60 match &o.fields {
61 Some(FieldsDecl::List(v)) => t.fields = v.clone(),
62 Some(FieldsDecl::Kinds(m)) => {
63 for k in m.keys() {
64 if !t.fields.contains(k) && !k.starts_with("rel.") {
65 t.fields.push(k.clone());
66 }
67 }
68 t.kinds = m.clone();
69 }
70 None => {}
71 }
72 if let Some(v) = &o.placeholders {
73 t.placeholders = v.clone();
74 }
75 if let Some(v) = &o.distinct {
76 t.distinct = v.clone();
77 }
78 if let Some(v) = &o.template {
79 t.template = v.clone();
80 }
81 if let Some(v) = &o.transitions {
82 t.transitions = v.clone();
83 }
84 if let Some(v) = &o.require_when {
85 t.require_when = v.clone();
86 }
87 if let Some(v) = &o.sections {
88 t.sections = v.clone();
89 }
90}
91
92pub fn suspect_overrides(overrides: &BTreeMap<String, TypeOverride>) -> Vec<String> {
96 let builtin: Vec<String> = doctype::builtin().into_iter().map(|t| t.name).collect();
97 let mut out = vec![];
98 for (name, o) in overrides {
99 if builtin.contains(name) {
100 continue;
101 }
102 let near = crate::check::rules::support::nearest(name, builtin.iter().map(String::as_str))
103 .or_else(|| builtin.iter().map(String::as_str).find(|b| name.strip_suffix('s') == Some(*b)));
104 if o.enabled == Some(false) {
105 let hint = near.map(|n| format!("; did you mean [types.{n}]?")).unwrap_or_default();
106 out.push(format!("[types.{name}] disables a type that does not exist{hint}"));
107 } else if let Some(n) = near
108 && o.about.is_none()
109 {
110 out.push(format!(
111 "[types.{name}] is not a built-in type, so it creates a new one; did you mean [types.{n}]? (give it an `about` to keep it as a new type)"
112 ));
113 }
114 }
115 for t in Registry::new(overrides).types {
117 for s in t.require_when.keys().filter(|s| !t.statuses.contains(s)) {
118 out.push(format!(
119 "[types.{}] require_when names status '{s}', which {} documents never have, so it never applies; statuses: {}",
120 t.name,
121 t.name,
122 t.statuses.join(", ")
123 ));
124 }
125 }
126 out
127}
128
129pub fn suspect_coverage(overrides: &BTreeMap<String, TypeOverride>, coverage: &[String]) -> Vec<String> {
131 let r = Registry::new(overrides);
132 let names: Vec<&str> = r.types.iter().map(|t| t.name.as_str()).collect();
133 coverage
134 .iter()
135 .filter(|c| !names.contains(&c.as_str()))
136 .map(|c| {
137 let near = crate::check::rules::support::nearest(c, names.iter().copied())
138 .map(|n| format!("; did you mean '{n}'?"))
139 .unwrap_or_default();
140 format!(
141 "[audit] coverage_types names type '{c}', which does not exist, so it is never checked{near} (types: {})",
142 names.join(", ")
143 )
144 })
145 .collect()
146}
147
148impl Registry {
149 pub fn new(overrides: &BTreeMap<String, TypeOverride>) -> Self {
150 let mut types = doctype::builtin();
151 for (name, o) in overrides {
152 let pos = types.iter().position(|t| &t.name == name);
153 if o.enabled == Some(false) {
154 if let Some(p) = pos {
155 types.remove(p);
156 }
157 continue;
158 }
159 match pos {
160 Some(p) => apply(&mut types[p], o),
161 None => {
162 let mut t = doctype::custom(name);
163 apply(&mut t, o);
164 types.push(t);
165 }
166 }
167 }
168 Registry { types }
169 }
170
171 pub fn get(&self, name: &str) -> Option<&TypeDef> {
172 self.types.iter().find(|t| t.name == name)
173 }
174
175 pub fn names(&self) -> Vec<&str> {
176 self.types.iter().map(|t| t.name.as_str()).collect()
177 }
178}
179
180#[cfg(test)]
181mod suspect_tests {
182 use super::{TypeOverride, suspect_overrides};
183 use std::collections::BTreeMap;
184
185 #[test]
186 fn near_misses_are_named_and_deliberate_types_are_not() {
187 let mut m = BTreeMap::new();
188 m.insert("adrs".to_string(), TypeOverride { sections: Some(vec!["Decision".into()]), ..Default::default() });
189 m.insert("memo".to_string(), TypeOverride { about: Some("Short notes".into()), ..Default::default() });
190 m.insert("rfc".to_string(), TypeOverride::default());
191 let got = suspect_overrides(&m);
192 assert_eq!(got.len(), 1, "{got:?}");
193 assert!(got[0].contains("did you mean [types.adr]"), "{got:?}");
194 let mut m = BTreeMap::new();
195 let when = BTreeMap::from([("accepted".to_string(), vec!["approvers".to_string()])]);
196 m.insert("adr".to_string(), TypeOverride { require_when: Some(when), ..Default::default() });
197 let got = suspect_overrides(&m);
198 assert!(got.len() == 1 && got[0].contains("status 'accepted'") && got[0].contains("active"), "C110: {got:?}");
199 }
200}