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, Form, KeyKind, TypeDef};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7/// Overrides for one type from spec.toml.
8#[derive(Debug, Clone, Default, Deserialize, Serialize)]
9#[serde(deny_unknown_fields)]
10pub struct TypeOverride {
11    pub enabled: Option<bool>,
12    pub about: Option<String>,
13    pub key: Option<KeyKind>,
14    pub form: Option<Form>,
15    pub statuses: Option<Vec<String>>,
16    pub initial: Option<String>,
17    pub sealed: Option<bool>,
18    pub fields: Option<Vec<String>>,
19    pub template: Option<String>,
20    pub transitions: Option<BTreeMap<String, Vec<String>>>,
21    pub require_when: Option<BTreeMap<String, Vec<String>>>,
22    pub sections: Option<Vec<String>>,
23}
24
25#[derive(Debug, Clone)]
26pub struct Registry {
27    pub types: Vec<TypeDef>,
28}
29
30fn apply(t: &mut TypeDef, o: &TypeOverride) {
31    if let Some(v) = &o.about {
32        t.about = v.clone();
33    }
34    if let Some(v) = o.key {
35        t.key = v;
36    }
37    if let Some(v) = o.form {
38        t.form = v;
39    }
40    if let Some(v) = &o.statuses {
41        t.statuses = v.clone();
42    }
43    if let Some(v) = &o.initial {
44        t.initial = v.clone();
45    }
46    if let Some(v) = o.sealed {
47        t.sealed = v;
48    }
49    if let Some(v) = &o.fields {
50        t.fields = v.clone();
51    }
52    if let Some(v) = &o.template {
53        t.template = v.clone();
54    }
55    if let Some(v) = &o.transitions {
56        t.transitions = v.clone();
57    }
58    if let Some(v) = &o.require_when {
59        t.require_when = v.clone();
60    }
61    if let Some(v) = &o.sections {
62        t.sections = v.clone();
63    }
64}
65
66/// `[types.<name>]` tables that look like typos: a name that is not built in, close to one that
67/// is, and without an `about` (which a deliberate new type would have); or a table disabling a
68/// type that does not exist.
69pub fn suspect_overrides(overrides: &BTreeMap<String, TypeOverride>) -> Vec<String> {
70    let builtin: Vec<String> = doctype::builtin().into_iter().map(|t| t.name).collect();
71    let mut out = vec![];
72    for (name, o) in overrides {
73        if builtin.contains(name) {
74            continue;
75        }
76        let near = crate::check::rules::support::nearest(name, builtin.iter().map(String::as_str))
77            .or_else(|| builtin.iter().map(String::as_str).find(|b| name.strip_suffix('s') == Some(*b)));
78        if o.enabled == Some(false) {
79            let hint = near.map(|n| format!("; did you mean [types.{n}]?")).unwrap_or_default();
80            out.push(format!("[types.{name}] disables a type that does not exist{hint}"));
81        } else if let Some(n) = near
82            && o.about.is_none()
83        {
84            out.push(format!(
85                "[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)"
86            ));
87        }
88    }
89    out
90}
91
92impl Registry {
93    pub fn new(overrides: &BTreeMap<String, TypeOverride>) -> Self {
94        let mut types = doctype::builtin();
95        for (name, o) in overrides {
96            let pos = types.iter().position(|t| &t.name == name);
97            if o.enabled == Some(false) {
98                if let Some(p) = pos {
99                    types.remove(p);
100                }
101                continue;
102            }
103            match pos {
104                Some(p) => apply(&mut types[p], o),
105                None => {
106                    let mut t = doctype::custom(name);
107                    apply(&mut t, o);
108                    types.push(t);
109                }
110            }
111        }
112        Registry { types }
113    }
114
115    pub fn get(&self, name: &str) -> Option<&TypeDef> {
116        self.types.iter().find(|t| t.name == name)
117    }
118
119    pub fn names(&self) -> Vec<&str> {
120        self.types.iter().map(|t| t.name.as_str()).collect()
121    }
122}
123
124#[cfg(test)]
125mod suspect_tests {
126    use super::{TypeOverride, suspect_overrides};
127    use std::collections::BTreeMap;
128
129    #[test]
130    fn near_misses_are_named_and_deliberate_types_are_not() {
131        let mut m = BTreeMap::new();
132        m.insert("adrs".to_string(), TypeOverride { sections: Some(vec!["Decision".into()]), ..Default::default() });
133        m.insert("memo".to_string(), TypeOverride { about: Some("Short notes".into()), ..Default::default() });
134        m.insert("rfc".to_string(), TypeOverride::default());
135        let got = suspect_overrides(&m);
136        assert_eq!(got.len(), 1, "{got:?}");
137        assert!(got[0].contains("did you mean [types.adr]"), "{got:?}");
138    }
139}