Skip to main content

specfold_core/model/
registry.rs

1//! The type registry: the built-in preset with a project's overrides applied.
2
3use super::doctype::{self, FieldKind, Form, KeyKind, TypeDef};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7/// `fields` in a type table: a list of keys (replacing the built-in list), or a table of keys
8/// and value kinds (`{ approvers = "people" }`), which adds the keys and gives rule S8 their kinds.
9#[derive(Debug, Clone, Deserialize, Serialize)]
10#[serde(untagged)]
11pub enum FieldsDecl {
12    List(Vec<String>),
13    Kinds(BTreeMap<String, FieldKind>),
14}
15
16/// Overrides for one type from spec.toml.
17#[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
92/// `[types.<name>]` tables that look like typos: a name that is not built in, close to one that
93/// is, and without an `about` (which a deliberate new type would have); or a table disabling a
94/// type that does not exist.
95pub 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    // C110: a status `require_when` names that the type does not have never applies.
116    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
129/// `[audit] coverage_types` names that are no enabled type: coverage never looks at them.
130pub 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}