Skip to main content

codama_plugin_core/
plugin.rs

1use crate::DirectiveResolver;
2use codama_attributes::ResolvableDirective;
3use codama_errors::CodamaResult;
4use codama_korok_visitors::KorokVisitable;
5use codama_nodes::{InstructionInputValueNode, RegisteredTypeNode};
6
7pub trait KorokPlugin {
8    /// Try to resolve a resolvable type directive belonging to this plugin.
9    /// Returns `None` if this plugin does not handle the given directive.
10    fn resolve_type_directive(
11        &self,
12        _directive: &ResolvableDirective,
13        _resolver: &dyn DirectiveResolver,
14    ) -> Option<CodamaResult<RegisteredTypeNode>> {
15        None
16    }
17
18    /// Try to resolve a resolvable value directive belonging to this plugin.
19    /// Returns `None` if this plugin does not handle the given directive.
20    fn resolve_value_directive(
21        &self,
22        _directive: &ResolvableDirective,
23        _resolver: &dyn DirectiveResolver,
24    ) -> Option<CodamaResult<InstructionInputValueNode>> {
25        None
26    }
27
28    fn on_initialized(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
29        Ok(())
30    }
31
32    fn on_fields_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
33        Ok(())
34    }
35
36    fn on_program_items_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
37        Ok(())
38    }
39
40    fn on_root_node_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
41        Ok(())
42    }
43}
44
45/// A `DirectiveResolver` built from all installed plugins.
46/// Iterates over plugins to find one that can resolve the given directive.
47pub struct CompositeDirectiveResolver<'a> {
48    plugins: &'a [Box<dyn KorokPlugin + 'a>],
49}
50
51impl<'a> CompositeDirectiveResolver<'a> {
52    pub fn new(plugins: &'a [Box<dyn KorokPlugin + 'a>]) -> Self {
53        Self { plugins }
54    }
55}
56
57impl<'a> DirectiveResolver for CompositeDirectiveResolver<'a> {
58    fn resolve_type_directive(
59        &self,
60        directive: &ResolvableDirective,
61    ) -> CodamaResult<RegisteredTypeNode> {
62        for plugin in self.plugins {
63            if let Some(result) = plugin.resolve_type_directive(directive, self) {
64                return result;
65            }
66        }
67        Err(codama_errors::CodamaError::UnresolvedDirective {
68            namespace: directive.namespace.clone(),
69            name: directive.name.clone(),
70        })
71    }
72
73    fn resolve_value_directive(
74        &self,
75        directive: &ResolvableDirective,
76    ) -> CodamaResult<InstructionInputValueNode> {
77        for plugin in self.plugins {
78            if let Some(result) = plugin.resolve_value_directive(directive, self) {
79                return result;
80            }
81        }
82        Err(codama_errors::CodamaError::UnresolvedDirective {
83            namespace: directive.namespace.clone(),
84            name: directive.name.clone(),
85        })
86    }
87}
88
89pub type ResolvePluginsResult<'a> = Box<dyn Fn(&mut dyn KorokVisitable) -> CodamaResult<()> + 'a>;
90
91/// Combine all plugins into a single function that runs them in sequence.
92pub fn resolve_plugins<'a>(plugins: &'a [Box<dyn KorokPlugin + 'a>]) -> ResolvePluginsResult<'a> {
93    Box::new(move |visitable: &mut dyn KorokVisitable| {
94        // Phase 0: Resolve all resolvable directives.
95        let resolver = CompositeDirectiveResolver::new(plugins);
96        visitable.accept(&mut crate::ResolveDirectivesVisitor::new(&resolver))?;
97
98        // Phase 1: Initialize.
99        plugins
100            .iter()
101            .try_for_each(|plugin| plugin.on_initialized(visitable))?;
102        // Phase 2: Set fields.
103        plugins
104            .iter()
105            .try_for_each(|plugin| plugin.on_fields_set(visitable))?;
106        // Phase 3: Set program items.
107        plugins
108            .iter()
109            .try_for_each(|plugin| plugin.on_program_items_set(visitable))?;
110        // Phase 4: Set root node.
111        plugins
112            .iter()
113            .try_for_each(|plugin| plugin.on_root_node_set(visitable))?;
114        Ok(())
115    })
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use codama_korok_visitors::KorokVisitor;
122    use codama_nodes::PublicKeyTypeNode;
123    use std::{cell::RefCell, rc::Rc};
124
125    // -- Lifecycle ordering tests --
126
127    struct LoggingPlugin {
128        id: String,
129        logs: Rc<RefCell<Vec<String>>>,
130    }
131    impl LoggingPlugin {
132        fn new(id: &str, logs: Rc<RefCell<Vec<String>>>) -> Self {
133            Self {
134                id: id.into(),
135                logs,
136            }
137        }
138    }
139    impl KorokPlugin for LoggingPlugin {
140        fn on_initialized(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
141            self.logs
142                .borrow_mut()
143                .push(format!("Plugin {} - initialized", self.id));
144            Ok(())
145        }
146        fn on_fields_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
147            self.logs
148                .borrow_mut()
149                .push(format!("Plugin {} - on_fields_set", self.id));
150            Ok(())
151        }
152        fn on_program_items_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
153            self.logs
154                .borrow_mut()
155                .push(format!("Plugin {} - on_program_items_set", self.id));
156            Ok(())
157        }
158        fn on_root_node_set(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> {
159            self.logs
160                .borrow_mut()
161                .push(format!("Plugin {} - on_root_node_set", self.id));
162            Ok(())
163        }
164    }
165
166    struct MockVisitable;
167    impl KorokVisitable for MockVisitable {
168        fn accept(&mut self, _visitor: &mut dyn KorokVisitor) -> CodamaResult<()> {
169            Ok(())
170        }
171        fn get_children(&mut self) -> Vec<&mut dyn KorokVisitable> {
172            Vec::new()
173        }
174    }
175
176    #[test]
177    fn test_resolve_plugins() -> CodamaResult<()> {
178        let logs = Rc::new(RefCell::new(Vec::new()));
179        let plugins: Vec<Box<dyn KorokPlugin>> = vec![
180            Box::new(LoggingPlugin::new("A", logs.clone())),
181            Box::new(LoggingPlugin::new("B", logs.clone())),
182        ];
183
184        let run_plugins = resolve_plugins(&plugins);
185        run_plugins(&mut MockVisitable)?;
186
187        assert_eq!(
188            logs.borrow().as_slice(),
189            &[
190                "Plugin A - initialized",
191                "Plugin B - initialized",
192                "Plugin A - on_fields_set",
193                "Plugin B - on_fields_set",
194                "Plugin A - on_program_items_set",
195                "Plugin B - on_program_items_set",
196                "Plugin A - on_root_node_set",
197                "Plugin B - on_root_node_set",
198            ]
199        );
200        Ok(())
201    }
202
203    // -- CompositeDirectiveResolver tests --
204
205    struct MockTypePlugin;
206    impl KorokPlugin for MockTypePlugin {
207        fn resolve_type_directive(
208            &self,
209            directive: &ResolvableDirective,
210            _resolver: &dyn DirectiveResolver,
211        ) -> Option<CodamaResult<RegisteredTypeNode>> {
212            if directive.namespace == "mock" && directive.name == "pubkey" {
213                Some(Ok(PublicKeyTypeNode::new().into()))
214            } else {
215                None
216            }
217        }
218    }
219
220    struct MockValuePlugin;
221    impl KorokPlugin for MockValuePlugin {
222        fn resolve_value_directive(
223            &self,
224            directive: &ResolvableDirective,
225            _resolver: &dyn DirectiveResolver,
226        ) -> Option<CodamaResult<InstructionInputValueNode>> {
227            if directive.namespace == "mock" && directive.name == "payer" {
228                Some(Ok(codama_nodes::PayerValueNode::new().into()))
229            } else {
230                None
231            }
232        }
233    }
234
235    fn make_directive(namespace: &str, name: &str) -> ResolvableDirective {
236        ResolvableDirective {
237            namespace: namespace.into(),
238            name: name.into(),
239            meta: syn::parse_quote! { foo::bar },
240        }
241    }
242
243    #[test]
244    fn composite_resolver_dispatches_type() {
245        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockTypePlugin)];
246        let resolver = CompositeDirectiveResolver::new(&plugins);
247        let directive = make_directive("mock", "pubkey");
248        let result = resolver.resolve_type_directive(&directive).unwrap();
249        assert_eq!(result, PublicKeyTypeNode::new().into());
250    }
251
252    #[test]
253    fn composite_resolver_dispatches_value() {
254        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockValuePlugin)];
255        let resolver = CompositeDirectiveResolver::new(&plugins);
256        let directive = make_directive("mock", "payer");
257        let result = resolver.resolve_value_directive(&directive).unwrap();
258        assert_eq!(result, codama_nodes::PayerValueNode::new().into());
259    }
260
261    #[test]
262    fn composite_resolver_returns_error_when_unresolved() {
263        let plugins: Vec<Box<dyn KorokPlugin>> = vec![];
264        let resolver = CompositeDirectiveResolver::new(&plugins);
265        let directive = make_directive("unknown", "thing");
266        let err = resolver.resolve_type_directive(&directive).unwrap_err();
267        assert!(matches!(
268            err,
269            codama_errors::CodamaError::UnresolvedDirective {
270                namespace,
271                name,
272            } if namespace == "unknown" && name == "thing"
273        ));
274    }
275
276    #[test]
277    fn composite_resolver_skips_non_matching_plugins() {
278        let plugins: Vec<Box<dyn KorokPlugin>> = vec![
279            Box::new(MockValuePlugin), // doesn't handle types
280            Box::new(MockTypePlugin),  // handles mock::pubkey
281        ];
282        let resolver = CompositeDirectiveResolver::new(&plugins);
283        let directive = make_directive("mock", "pubkey");
284        let result = resolver.resolve_type_directive(&directive).unwrap();
285        assert_eq!(result, PublicKeyTypeNode::new().into());
286    }
287
288    // -- Nested resolution (two plugins) --
289
290    /// Plugin A resolves `a::wrapper` by calling the resolver for its inner type.
291    struct PluginA;
292    impl KorokPlugin for PluginA {
293        fn resolve_type_directive(
294            &self,
295            directive: &ResolvableDirective,
296            resolver: &dyn DirectiveResolver,
297        ) -> Option<CodamaResult<RegisteredTypeNode>> {
298            if directive.namespace != "a" || directive.name != "wrapper" {
299                return None;
300            }
301            // Simulate parsing the inner directive and delegating to resolver.
302            let inner = ResolvableDirective {
303                namespace: "b".into(),
304                name: "inner".into(),
305                meta: syn::parse_quote! { b::inner },
306            };
307            Some(resolver.resolve_type_directive(&inner))
308        }
309    }
310
311    /// Plugin B resolves `b::inner` → PublicKeyTypeNode.
312    struct PluginB;
313    impl KorokPlugin for PluginB {
314        fn resolve_type_directive(
315            &self,
316            directive: &ResolvableDirective,
317            _resolver: &dyn DirectiveResolver,
318        ) -> Option<CodamaResult<RegisteredTypeNode>> {
319            if directive.namespace == "b" && directive.name == "inner" {
320                Some(Ok(PublicKeyTypeNode::new().into()))
321            } else {
322                None
323            }
324        }
325    }
326
327    #[test]
328    fn nested_resolution_across_two_plugins() {
329        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(PluginA), Box::new(PluginB)];
330        let resolver = CompositeDirectiveResolver::new(&plugins);
331        let directive = make_directive("a", "wrapper");
332        // PluginA resolves a::wrapper by asking the resolver for b::inner,
333        // which PluginB resolves to PublicKeyTypeNode.
334        let result = resolver.resolve_type_directive(&directive).unwrap();
335        assert_eq!(result, PublicKeyTypeNode::new().into());
336    }
337
338    // -- E2e: resolve directives on a korok tree --
339
340    #[test]
341    fn e2e_resolves_type_directive_on_korok() -> CodamaResult<()> {
342        use codama_attributes::{Resolvable, TryFromFilter, TypeDirective};
343
344        // Build a korok tree from source with a resolvable type directive.
345        let item: syn::Item = syn::parse_quote! {
346            #[codama(type = mock::pubkey)]
347            struct MyAccount;
348        };
349        let mut korok = codama_koroks::StructKorok::parse(&item)?;
350
351        // Verify the directive is unresolved before resolution.
352        let directive_before = korok
353            .attributes
354            .get_last(TypeDirective::filter)
355            .expect("should have a type directive");
356        assert!(directive_before.node.is_unresolved());
357
358        // Run resolve_plugins with MockTypePlugin.
359        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockTypePlugin)];
360        let run_plugins = resolve_plugins(&plugins);
361        run_plugins(&mut korok)?;
362
363        // Verify the directive is now resolved.
364        let directive_after = korok
365            .attributes
366            .get_last(TypeDirective::filter)
367            .expect("should still have a type directive");
368        assert!(directive_after.node.is_resolved());
369        assert_eq!(
370            directive_after.node,
371            Resolvable::Resolved(RegisteredTypeNode::from(PublicKeyTypeNode::new()).into())
372        );
373        Ok(())
374    }
375
376    #[test]
377    fn e2e_resolves_nested_directives_across_two_plugins() -> CodamaResult<()> {
378        use codama_attributes::{Resolvable, TryFromFilter, TypeDirective};
379
380        // Build a korok tree from source with a resolvable type directive.
381        // PluginA handles a::wrapper, which internally delegates to PluginB for b::inner.
382        let item: syn::Item = syn::parse_quote! {
383            #[codama(type = a::wrapper)]
384            struct MyAccount;
385        };
386        let mut korok = codama_koroks::StructKorok::parse(&item)?;
387
388        // Verify unresolved before.
389        let directive_before = korok
390            .attributes
391            .get_last(TypeDirective::filter)
392            .expect("should have a type directive");
393        assert!(directive_before.node.is_unresolved());
394
395        // Run resolve_plugins with both plugins.
396        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(PluginA), Box::new(PluginB)];
397        let run_plugins = resolve_plugins(&plugins);
398        run_plugins(&mut korok)?;
399
400        // Verify the directive is resolved to PublicKeyTypeNode
401        // (PluginA delegated to PluginB which returns PublicKeyTypeNode).
402        let directive_after = korok
403            .attributes
404            .get_last(TypeDirective::filter)
405            .expect("should still have a type directive");
406        assert!(directive_after.node.is_resolved());
407        assert_eq!(
408            directive_after.node,
409            Resolvable::Resolved(RegisteredTypeNode::from(PublicKeyTypeNode::new()).into())
410        );
411        Ok(())
412    }
413
414    #[test]
415    fn e2e_resolves_value_directive_on_korok() -> CodamaResult<()> {
416        use codama_attributes::{DefaultValueDirective, Resolvable, TryFromFilter};
417
418        // Build a korok tree from source with a resolvable default value directive.
419        let field: syn::Field = syn::parse_quote! {
420            #[codama(default_value = mock::payer)]
421            pub authority: Pubkey
422        };
423        let mut korok = codama_koroks::FieldKorok::parse(&field)?;
424
425        // Verify the directive is unresolved before resolution.
426        let directive_before = korok
427            .attributes
428            .get_last(DefaultValueDirective::filter)
429            .expect("should have a default value directive");
430        assert!(directive_before.node.is_unresolved());
431
432        // Run resolve_plugins with MockValuePlugin.
433        let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockValuePlugin)];
434        let run_plugins = resolve_plugins(&plugins);
435        run_plugins(&mut korok)?;
436
437        // Verify the directive is now resolved.
438        let directive_after = korok
439            .attributes
440            .get_last(DefaultValueDirective::filter)
441            .expect("should still have a default value directive");
442        assert!(directive_after.node.is_resolved());
443        assert_eq!(
444            directive_after.node,
445            Resolvable::Resolved(codama_nodes::PayerValueNode::new().into())
446        );
447        Ok(())
448    }
449
450    #[test]
451    fn e2e_unresolved_directive_errors() {
452        // Build a korok with a resolvable directive but no plugin to resolve it.
453        let item: syn::Item = syn::parse_quote! {
454            #[codama(type = unknown::thing)]
455            struct MyAccount;
456        };
457        let mut korok = codama_koroks::StructKorok::parse(&item).unwrap();
458
459        let plugins: Vec<Box<dyn KorokPlugin>> = vec![];
460        let run_plugins = resolve_plugins(&plugins);
461        let err = run_plugins(&mut korok).unwrap_err();
462        assert!(matches!(
463            err,
464            codama_errors::CodamaError::UnresolvedDirective { .. }
465        ));
466    }
467}