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 fn resolve_type_directive(
11 &self,
12 _directive: &ResolvableDirective,
13 _resolver: &dyn DirectiveResolver,
14 ) -> Option<CodamaResult<RegisteredTypeNode>> {
15 None
16 }
17
18 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
45pub 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
91pub fn resolve_plugins<'a>(plugins: &'a [Box<dyn KorokPlugin + 'a>]) -> ResolvePluginsResult<'a> {
93 Box::new(move |visitable: &mut dyn KorokVisitable| {
94 let resolver = CompositeDirectiveResolver::new(plugins);
96 visitable.accept(&mut crate::ResolveDirectivesVisitor::new(&resolver))?;
97
98 plugins
100 .iter()
101 .try_for_each(|plugin| plugin.on_initialized(visitable))?;
102 plugins
104 .iter()
105 .try_for_each(|plugin| plugin.on_fields_set(visitable))?;
106 plugins
108 .iter()
109 .try_for_each(|plugin| plugin.on_program_items_set(visitable))?;
110 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 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 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), Box::new(MockTypePlugin), ];
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 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 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 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 let result = resolver.resolve_type_directive(&directive).unwrap();
335 assert_eq!(result, PublicKeyTypeNode::new().into());
336 }
337
338 #[test]
341 fn e2e_resolves_type_directive_on_korok() -> CodamaResult<()> {
342 use codama_attributes::{Resolvable, TryFromFilter, TypeDirective};
343
344 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 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 let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockTypePlugin)];
360 let run_plugins = resolve_plugins(&plugins);
361 run_plugins(&mut korok)?;
362
363 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 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 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 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 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 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 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 let plugins: Vec<Box<dyn KorokPlugin>> = vec![Box::new(MockValuePlugin)];
434 let run_plugins = resolve_plugins(&plugins);
435 run_plugins(&mut korok)?;
436
437 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 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}