codama_korok_plugins/
plugin.rs

1use codama_errors::CodamaResult;
2use codama_korok_visitors::KorokVisitable;
3
4pub trait KorokPlugin {
5    fn run(
6        &self,
7        visitable: &mut dyn KorokVisitable,
8        next: &dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()>,
9    ) -> CodamaResult<()>;
10}
11
12pub type ResolvePluginsResult<'a> = Box<dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()> + 'a>;
13
14/// Reduce all plugins into a single function that runs them in sequence.
15///
16/// For instance, imagine we have a list of plugins [A, B, C] implemented as:
17///
18/// ```rust
19/// use codama_errors::CodamaResult;
20/// use codama_korok_plugins::KorokPlugin;
21/// use codama_korok_visitors::KorokVisitable;
22///
23/// struct LoggingPluging;
24/// impl KorokPlugin for LoggingPluging {
25///     fn run(&self, visitable: &mut dyn KorokVisitable, next: &dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()>) -> CodamaResult<()> {
26///         println!("Plugin X - before");
27///         next(visitable)?;
28///         println!("Plugin X - after");
29///         Ok(())
30///     }
31/// }
32/// ```
33///
34/// Where `X` is `A`, `B`, or `C`. The `resolve_plugins` function will return a function that
35/// prints the following:
36///
37/// ```text
38/// Plugin C - before
39/// Plugin B - before
40/// Plugin A - before
41/// Plugin A - after
42/// Plugin B - after
43/// Plugin C - after
44/// ```
45pub fn resolve_plugins<'a>(plugins: &'a [Box<dyn KorokPlugin + 'a>]) -> ResolvePluginsResult<'a> {
46    // We fold from the left to ensure that any code before the
47    // `next` call is run before the previous plugin on the list.
48    plugins.iter().fold(
49        // Base case: a no-op `next` function.
50        Box::new(|_: &mut dyn KorokVisitable| Ok(()))
51            as Box<dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()>>,
52        // Wrap each plugin with a closure that calls the next plugin in the chain.
53        |next, plugin| {
54            Box::new(move |visitable: &mut dyn KorokVisitable| plugin.run(visitable, &next))
55                as Box<dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()>>
56        },
57    )
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use codama_korok_visitors::KorokVisitor;
64    use std::{cell::RefCell, rc::Rc};
65
66    struct LoggingPluging {
67        id: String,
68        logs: Rc<RefCell<Vec<String>>>,
69    }
70    impl LoggingPluging {
71        fn new(id: &str, logs: Rc<RefCell<Vec<String>>>) -> Self {
72            Self {
73                id: id.into(),
74                logs,
75            }
76        }
77    }
78    impl KorokPlugin for LoggingPluging {
79        fn run(
80            &self,
81            visitable: &mut dyn KorokVisitable,
82            next: &dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()>,
83        ) -> CodamaResult<()> {
84            self.logs
85                .borrow_mut()
86                .push(format!("Plugin {} - before", self.id));
87            next(visitable)?;
88            self.logs
89                .borrow_mut()
90                .push(format!("Plugin {} - after", self.id));
91            Ok(())
92        }
93    }
94
95    struct MockVisitable;
96    impl KorokVisitable for MockVisitable {
97        fn accept(&mut self, _visitor: &mut dyn KorokVisitor) -> CodamaResult<()> {
98            Ok(())
99        }
100        fn get_children(&mut self) -> Vec<&mut dyn KorokVisitable> {
101            Vec::new()
102        }
103    }
104
105    #[test]
106    fn test_resolve_plugins() -> CodamaResult<()> {
107        let logs = Rc::new(RefCell::new(Vec::new()));
108        let plugins: Vec<Box<dyn KorokPlugin>> = vec![
109            Box::new(LoggingPluging::new("A", logs.clone())),
110            Box::new(LoggingPluging::new("B", logs.clone())),
111        ];
112
113        let run_plugins = resolve_plugins(&plugins);
114        run_plugins(&mut MockVisitable)?;
115
116        assert_eq!(
117            logs.borrow().as_slice(),
118            &[
119                "Plugin B - before",
120                "Plugin A - before",
121                "Plugin A - after",
122                "Plugin B - after",
123            ]
124        );
125        Ok(())
126    }
127}