Skip to main content

alint_core/
registry.rs

1use std::collections::HashMap;
2
3use crate::config::RuleSpec;
4use crate::error::{Error, Result};
5use crate::rule::Rule;
6
7pub type RuleBuilder = fn(&RuleSpec) -> Result<Box<dyn Rule>>;
8
9/// Map from `kind` string → factory function. Built-in rule crates register
10/// themselves here at startup, and plugin rules (in later phases) will too.
11#[derive(Default)]
12pub struct RuleRegistry {
13    builders: HashMap<String, RuleBuilder>,
14}
15
16impl std::fmt::Debug for RuleRegistry {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("RuleRegistry")
19            .field("kinds", &self.builders.keys().collect::<Vec<_>>())
20            .finish()
21    }
22}
23
24impl RuleRegistry {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn register(&mut self, kind: &str, builder: RuleBuilder) {
30        self.builders.insert(kind.to_string(), builder);
31    }
32
33    pub fn build(&self, spec: &RuleSpec) -> Result<Box<dyn Rule>> {
34        let builder = self
35            .builders
36            .get(&spec.kind)
37            .ok_or_else(|| Error::UnknownRuleKind(spec.kind.clone()))?;
38        builder(spec)
39    }
40
41    pub fn known_kinds(&self) -> impl Iterator<Item = &str> {
42        self.builders.keys().map(String::as_str)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use crate::level::Level;
50
51    fn fake_spec(kind: &str) -> RuleSpec {
52        RuleSpec {
53            id: "t".into(),
54            kind: kind.into(),
55            level: Level::Error,
56            paths: None,
57            message: None,
58            policy_url: None,
59            when: None,
60            fix: None,
61            git_tracked_only: false,
62            scope_filter: None,
63            extra: serde_yaml_ng::Mapping::new(),
64        }
65    }
66
67    fn ok_builder(_spec: &RuleSpec) -> Result<Box<dyn Rule>> {
68        // Trait object can't be a unit struct without an `impl
69        // Rule for ()` somewhere; the build path doesn't actually
70        // call this in the unknown-kind tests so we leave it
71        // unreachable on the happy path.
72        unreachable!("test should not call this builder")
73    }
74
75    #[test]
76    fn new_registry_has_no_kinds() {
77        let r = RuleRegistry::new();
78        assert_eq!(r.known_kinds().count(), 0);
79    }
80
81    #[test]
82    fn register_inserts_a_kind() {
83        let mut r = RuleRegistry::new();
84        r.register("my_kind", ok_builder);
85        let kinds: Vec<&str> = r.known_kinds().collect();
86        assert_eq!(kinds, vec!["my_kind"]);
87    }
88
89    #[test]
90    fn register_overwrites_existing_kind() {
91        // Last-registered-wins. Plugin loaders may rely on this
92        // to override a built-in's behaviour.
93        let mut r = RuleRegistry::new();
94        r.register("my_kind", ok_builder);
95        r.register("my_kind", ok_builder);
96        assert_eq!(r.known_kinds().count(), 1);
97    }
98
99    #[test]
100    fn build_rejects_unknown_kind_with_clear_error() {
101        let r = RuleRegistry::new();
102        let err = r.build(&fake_spec("not_real")).unwrap_err();
103        match err {
104            Error::UnknownRuleKind(name) => assert_eq!(name, "not_real"),
105            other => panic!("expected UnknownRuleKind, got {other:?}"),
106        }
107    }
108
109    #[test]
110    fn known_kinds_iterator_lists_all_registered() {
111        let mut r = RuleRegistry::new();
112        r.register("a", ok_builder);
113        r.register("b", ok_builder);
114        r.register("c", ok_builder);
115        let mut kinds: Vec<&str> = r.known_kinds().collect();
116        kinds.sort_unstable();
117        assert_eq!(kinds, vec!["a", "b", "c"]);
118    }
119}