From c330ea5c3a36d5d20b1e1d405bb383d1b003cf86 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 17 Mar 2026 11:05:24 +0000 Subject: [PATCH] Add resolvable directive system for extensible type and value directives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces a system that allows Codama plugins to contribute custom type and value directives inside `#[codama(...)]` attributes using a namespaced `prefix::name(...)` syntax. Today, all type and value directives inside `#[codama(...)]` are hardcoded. If someone wants a shorthand like `ata(...)` for Associated Token Account PDAs, it must be added to the core `codama-attributes` crate. This PR makes these directive slots extensible so that plugins can provide their own. Any directive with exactly two path segments (e.g., `token::ata(...)`) is treated as a resolvable directive. Single-segment paths (e.g., `payer`, `number(u8)`) continue to be parsed as built-in directives with hard errors on unrecognized names. ```rust // Built-in (parsed eagerly as before): // Resolvable (deferred to a plugin for resolution): // Composable (multiple plugins in one attribute): ``` Directive structs now store `Resolvable` instead of bare node types in their type/value slots. `Resolvable` is either `Resolved(T)` (a concrete node) or `Unresolved(ResolvableDirective)` (namespace + name + raw meta, waiting for a plugin to resolve it). Before any lifecycle hooks fire, the framework runs a `ResolveDirectivesVisitor` that walks the korok tree and delegates each `Unresolved` entry to the `DirectiveResolver` (built from all installed plugins). Plugins implement `resolve_type_directive` and/or `resolve_value_directive` on the `KorokPlugin` trait to claim directives matching their namespace. Nested resolution across plugins is supported — a plugin can call the resolver for directives belonging to other plugins while parsing its own arguments. - **`codama-syn-helpers`**: `Meta`, `PathValue`, `PathList` now implement `Clone` and `PartialEq`. - **`codama-errors`**: New `CodamaError::UnresolvedDirective` variant. - **`codama-attributes`**: New `Resolvable` enum and `ResolvableDirective` struct. `TypeDirective`, `DefaultValueDirective`, `AccountDirective`, `FieldDirective`, `ArgumentDirective`, and `SeedDirective` now store `Resolvable` in their type/value slots. `SeedDirectiveType::Defined` replaced by `Variable` and `Constant` variants. Directive structs that previously stored inline nodes (`AccountDirective`, `FieldDirective`, `ArgumentDirective`) now store individual fields with `to_*_node()` methods for constructing the node after resolution. - **`codama-korok-plugins`**: New `DirectiveResolver` trait, `CompositeDirectiveResolver` implementation, and `ResolveDirectivesVisitor`. `KorokPlugin` gains `resolve_type_directive` and `resolve_value_directive` methods. `resolve_plugins()` automatically runs directive resolution before `on_initialized`. - **`codama-korok-visitors`**: All visitors that consume directives now propagate `CodamaResult` errors from `try_resolved()` instead of silently skipping unresolved entries. --- Cargo.lock | 3 + .../codama_directives/account_directive.rs | 127 ++++--- .../codama_directives/argument_directive.rs | 68 +++- .../default_value_directive.rs | 16 +- .../src/codama_directives/field_directive.rs | 67 +++- .../src/codama_directives/mod.rs | 2 + .../codama_directives/resolvable_directive.rs | 353 ++++++++++++++++++ .../src/codama_directives/seed_directive.rs | 56 +-- .../src/codama_directives/type_directive.rs | 25 +- .../type_nodes/struct_field_meta_consumer.rs | 24 +- .../type_nodes/struct_field_type_node.rs | 17 +- codama-attributes/src/utils/macros.rs | 4 +- codama-errors/src/errors.rs | 3 + codama-korok-plugins/Cargo.toml | 5 + .../src/directive_resolver.rs | 22 ++ codama-korok-plugins/src/lib.rs | 4 + codama-korok-plugins/src/plugin.rs | 353 +++++++++++++++++- .../src/resolve_directives_visitor.rs | 178 +++++++++ .../src/apply_type_overrides_visitor.rs | 2 +- .../src/combine_types_visitor.rs | 10 +- .../src/set_accounts_visitor.rs | 14 +- .../src/set_default_values_visitor.rs | 28 +- .../src/set_instructions_visitors.rs | 60 +-- codama-korok-visitors/src/set_pdas_visitor.rs | 82 ++-- .../resolvable_default_value.pass.rs | 7 + .../field_directive/resolvable_type.pass.rs | 9 + .../seed_directive/resolvable_seed.pass.rs | 7 + .../type_directive/resolvable_type.pass.rs | 7 + .../values_nodes/resolvable_value.pass.rs | 7 + codama-syn-helpers/src/meta.rs | 32 +- 30 files changed, 1362 insertions(+), 230 deletions(-) create mode 100644 codama-attributes/src/codama_directives/resolvable_directive.rs create mode 100644 codama-korok-plugins/src/directive_resolver.rs create mode 100644 codama-korok-plugins/src/resolve_directives_visitor.rs create mode 100644 codama-macros/tests/codama_attribute/account_directive/resolvable_default_value.pass.rs create mode 100644 codama-macros/tests/codama_attribute/field_directive/resolvable_type.pass.rs create mode 100644 codama-macros/tests/codama_attribute/seed_directive/resolvable_seed.pass.rs create mode 100644 codama-macros/tests/codama_attribute/type_directive/resolvable_type.pass.rs create mode 100644 codama-macros/tests/codama_attribute/values_nodes/resolvable_value.pass.rs diff --git a/Cargo.lock b/Cargo.lock index 79a1378e..5ca36a71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,9 +197,12 @@ dependencies = [ name = "codama-korok-plugins" version = "0.7.4" dependencies = [ + "codama-attributes", "codama-errors", "codama-korok-visitors", "codama-koroks", + "codama-nodes", + "syn", ] [[package]] diff --git a/codama-attributes/src/codama_directives/account_directive.rs b/codama-attributes/src/codama_directives/account_directive.rs index 44f6df16..36d37602 100644 --- a/codama-attributes/src/codama_directives/account_directive.rs +++ b/codama-attributes/src/codama_directives/account_directive.rs @@ -1,8 +1,8 @@ use crate::{ utils::{FromMeta, SetOnce}, - Attribute, AttributeContext, CodamaAttribute, CodamaDirective, + Attribute, AttributeContext, CodamaAttribute, CodamaDirective, Resolvable, }; -use codama_errors::CodamaError; +use codama_errors::{CodamaError, CodamaResult}; use codama_nodes::{ CamelCaseString, Docs, InstructionAccountNode, InstructionInputValueNode, IsAccountSigner, }; @@ -10,7 +10,12 @@ use codama_syn_helpers::{extensions::*, Meta}; #[derive(Debug, PartialEq)] pub struct AccountDirective { - pub account: InstructionAccountNode, + pub name: CamelCaseString, + pub is_writable: bool, + pub is_signer: IsAccountSigner, + pub is_optional: bool, + pub docs: Docs, + pub default_value: Option>, } impl AccountDirective { @@ -26,7 +31,8 @@ impl AccountDirective { let mut is_writable = SetOnce::::new("writable").initial_value(false); let mut is_signer = SetOnce::::new("signer").initial_value(false.into()); let mut is_optional = SetOnce::::new("optional").initial_value(false); - let mut default_value = SetOnce::::new("default_value"); + let mut default_value = + SetOnce::>::new("default_value"); let mut docs = SetOnce::::new("docs"); match meta.is_path_or_empty_list() { true => (), @@ -38,7 +44,7 @@ impl AccountDirective { "signer" => is_signer.set(IsAccountSigner::from_meta(meta)?, meta), "optional" => is_optional.set(bool::from_meta(meta)?, meta), "default_value" => default_value.set( - InstructionInputValueNode::from_meta(meta.as_value()?)?, + Resolvable::::from_meta(meta.as_value()?)?, meta, ), "docs" => docs.set(Docs::from_meta(meta)?, meta), @@ -46,14 +52,29 @@ impl AccountDirective { })?, } Ok(AccountDirective { - account: InstructionAccountNode { - name: name.take(meta)?, - is_writable: is_writable.take(meta)?, - is_signer: is_signer.take(meta)?, - is_optional: is_optional.take(meta)?, - docs: docs.option().unwrap_or_default(), - default_value: default_value.option(), - }, + name: name.take(meta)?, + is_writable: is_writable.take(meta)?, + is_signer: is_signer.take(meta)?, + is_optional: is_optional.take(meta)?, + docs: docs.option().unwrap_or_default(), + default_value: default_value.option(), + }) + } + + /// Construct an `InstructionAccountNode` from this directive. + /// Returns an error if any unresolved directives remain. + pub fn to_instruction_account_node(&self) -> CodamaResult { + Ok(InstructionAccountNode { + name: self.name.clone(), + is_writable: self.is_writable, + is_signer: self.is_signer, + is_optional: self.is_optional, + docs: self.docs.clone(), + default_value: self + .default_value + .as_ref() + .map(|r| r.try_resolved().cloned()) + .transpose()?, }) } } @@ -94,14 +115,12 @@ mod tests { assert_eq!( directive, AccountDirective { - account: InstructionAccountNode { - name: "payer".into(), - is_writable: true, - is_signer: IsAccountSigner::True, - is_optional: true, - default_value: Some(PayerValueNode::new().into()), - docs: Docs::default(), - }, + name: "payer".into(), + is_writable: true, + is_signer: IsAccountSigner::True, + is_optional: true, + default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())), + docs: Docs::default(), } ); } @@ -121,14 +140,12 @@ mod tests { assert_eq!( directive, AccountDirective { - account: InstructionAccountNode { - name: "payer".into(), - is_writable: true, - is_signer: IsAccountSigner::Either, - is_optional: false, - default_value: Some(PayerValueNode::new().into()), - docs: Docs::default(), - }, + name: "payer".into(), + is_writable: true, + is_signer: IsAccountSigner::Either, + is_optional: false, + default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())), + docs: Docs::default(), } ); } @@ -142,14 +159,12 @@ mod tests { assert_eq!( directive, AccountDirective { - account: InstructionAccountNode { - name: "authority".into(), - is_writable: false, - is_signer: IsAccountSigner::False, - is_optional: false, - default_value: None, - docs: Docs::default(), - }, + name: "authority".into(), + is_writable: false, + is_signer: IsAccountSigner::False, + is_optional: false, + default_value: None, + docs: Docs::default(), } ); } @@ -172,14 +187,12 @@ mod tests { assert_eq!( directive, AccountDirective { - account: InstructionAccountNode { - name: "stake".into(), - is_writable: true, - is_signer: IsAccountSigner::False, - is_optional: false, - default_value: None, - docs: vec!["what this account is for".to_string()].into(), - }, + name: "stake".into(), + is_writable: true, + is_signer: IsAccountSigner::False, + is_optional: false, + default_value: None, + docs: vec!["what this account is for".to_string()].into(), } ); } @@ -193,19 +206,17 @@ mod tests { assert_eq!( directive, AccountDirective { - account: InstructionAccountNode { - name: "authority".into(), - is_writable: false, - is_signer: IsAccountSigner::True, - is_optional: false, - default_value: None, - docs: vec![ - "Line 1".to_string(), - "Line 2".to_string(), - "Line 3".to_string() - ] - .into(), - }, + name: "authority".into(), + is_writable: false, + is_signer: IsAccountSigner::True, + is_optional: false, + default_value: None, + docs: vec![ + "Line 1".to_string(), + "Line 2".to_string(), + "Line 3".to_string() + ] + .into(), } ); } diff --git a/codama-attributes/src/codama_directives/argument_directive.rs b/codama-attributes/src/codama_directives/argument_directive.rs index 0241511a..ef945732 100644 --- a/codama-attributes/src/codama_directives/argument_directive.rs +++ b/codama-attributes/src/codama_directives/argument_directive.rs @@ -1,16 +1,23 @@ use crate::{ codama_directives::type_nodes::StructFieldMetaConsumer, utils::{FromMeta, MetaConsumer}, - Attribute, CodamaAttribute, CodamaDirective, + Attribute, CodamaAttribute, CodamaDirective, Resolvable, +}; +use codama_errors::{CodamaError, CodamaResult}; +use codama_nodes::{ + CamelCaseString, DefaultValueStrategy, Docs, InstructionArgumentNode, + InstructionInputValueNode, TypeNode, }; -use codama_errors::CodamaError; -use codama_nodes::InstructionArgumentNode; use codama_syn_helpers::Meta; #[derive(Debug, PartialEq)] pub struct ArgumentDirective { pub after: bool, - pub argument: InstructionArgumentNode, + pub name: CamelCaseString, + pub r#type: Resolvable, + pub docs: Docs, + pub default_value: Option>, + pub default_value_strategy: Option, } impl ArgumentDirective { @@ -27,13 +34,27 @@ impl ArgumentDirective { Ok(ArgumentDirective { after: consumer.after.option().unwrap_or(false), - argument: InstructionArgumentNode { - name: consumer.name.take(meta)?, - r#type: consumer.r#type.take(meta)?, - docs: consumer.docs.option().unwrap_or_default(), - default_value, - default_value_strategy, - }, + name: consumer.name.take(meta)?, + r#type: consumer.r#type.take(meta)?, + docs: consumer.docs.option().unwrap_or_default(), + default_value, + default_value_strategy, + }) + } + + /// Construct an `InstructionArgumentNode` from this directive. + /// Returns an error if any unresolved directives remain. + pub fn to_instruction_argument_node(&self) -> CodamaResult { + Ok(InstructionArgumentNode { + name: self.name.clone(), + r#type: self.r#type.try_resolved()?.clone(), + docs: self.docs.clone(), + default_value: self + .default_value + .as_ref() + .map(|r| r.try_resolved().cloned()) + .transpose()?, + default_value_strategy: self.default_value_strategy, }) } } @@ -73,7 +94,11 @@ mod tests { directive, ArgumentDirective { after: false, - argument: InstructionArgumentNode::new("age", NumberTypeNode::le(U8)), + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: None, + default_value_strategy: None, } ); } @@ -86,7 +111,11 @@ mod tests { directive, ArgumentDirective { after: true, - argument: InstructionArgumentNode::new("age", NumberTypeNode::le(U8)), + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: None, + default_value_strategy: None, } ); } @@ -99,10 +128,11 @@ mod tests { directive, ArgumentDirective { after: false, - argument: InstructionArgumentNode { - default_value: Some(PayerValueNode::new().into()), - ..InstructionArgumentNode::new("age", NumberTypeNode::le(U8)) - }, + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())), + default_value_strategy: None, } ); } @@ -111,7 +141,7 @@ mod tests { fn with_docs_string() { let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = "The cake") }; let directive = ArgumentDirective::parse(&meta).unwrap(); - assert_eq!(directive.argument.docs, vec!["The cake".to_string()].into()); + assert_eq!(directive.docs, vec!["The cake".to_string()].into()); } #[test] @@ -119,7 +149,7 @@ mod tests { let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = ["The cake", "must be a lie"]) }; let directive = ArgumentDirective::parse(&meta).unwrap(); assert_eq!( - directive.argument.docs, + directive.docs, vec!["The cake".to_string(), "must be a lie".to_string()].into() ); } diff --git a/codama-attributes/src/codama_directives/default_value_directive.rs b/codama-attributes/src/codama_directives/default_value_directive.rs index 1e805b1b..2e99cec4 100644 --- a/codama-attributes/src/codama_directives/default_value_directive.rs +++ b/codama-attributes/src/codama_directives/default_value_directive.rs @@ -1,11 +1,11 @@ -use crate::{utils::FromMeta, Attribute, CodamaAttribute, CodamaDirective}; +use crate::{Attribute, CodamaAttribute, CodamaDirective, Resolvable}; use codama_errors::CodamaError; use codama_nodes::{DefaultValueStrategy, InstructionInputValueNode, ValueNode}; use codama_syn_helpers::{extensions::*, Meta}; #[derive(Debug, PartialEq)] pub struct DefaultValueDirective { - pub node: InstructionInputValueNode, + pub node: Resolvable, pub default_value_strategy: Option, } @@ -29,8 +29,10 @@ impl DefaultValueDirective { }; let node = match value_nodes_only { - true => ValueNode::from_meta(&pv.value)?.into(), - false => InstructionInputValueNode::from_meta(&pv.value)?, + true => { + Resolvable::::from_meta(&pv.value)?.map(InstructionInputValueNode::from) + } + false => Resolvable::::from_meta(&pv.value)?, }; let default_value_strategy = match is_value { @@ -81,7 +83,7 @@ mod tests { assert_eq!( directive, DefaultValueDirective { - node: PayerValueNode::new().into(), + node: Resolvable::Resolved(PayerValueNode::new().into()), default_value_strategy: None, } ); @@ -95,7 +97,7 @@ mod tests { assert_eq!( directive, DefaultValueDirective { - node: PayerValueNode::new().into(), + node: Resolvable::Resolved(PayerValueNode::new().into()), default_value_strategy: Some(DefaultValueStrategy::Omitted), } ); @@ -109,7 +111,7 @@ mod tests { assert_eq!( directive, DefaultValueDirective { - node: BooleanValueNode::new(true).into(), + node: Resolvable::Resolved(BooleanValueNode::new(true).into()), default_value_strategy: Some(DefaultValueStrategy::Omitted), } ); diff --git a/codama-attributes/src/codama_directives/field_directive.rs b/codama-attributes/src/codama_directives/field_directive.rs index 0c8d4ff0..399ecb25 100644 --- a/codama-attributes/src/codama_directives/field_directive.rs +++ b/codama-attributes/src/codama_directives/field_directive.rs @@ -1,16 +1,22 @@ use crate::{ codama_directives::type_nodes::StructFieldMetaConsumer, utils::{FromMeta, MetaConsumer}, - Attribute, CodamaAttribute, CodamaDirective, + Attribute, CodamaAttribute, CodamaDirective, Resolvable, +}; +use codama_errors::{CodamaError, CodamaResult}; +use codama_nodes::{ + CamelCaseString, DefaultValueStrategy, Docs, StructFieldTypeNode, TypeNode, ValueNode, }; -use codama_errors::CodamaError; -use codama_nodes::StructFieldTypeNode; use codama_syn_helpers::Meta; #[derive(Debug, PartialEq)] pub struct FieldDirective { pub after: bool, - pub field: StructFieldTypeNode, + pub name: CamelCaseString, + pub r#type: Resolvable, + pub docs: Docs, + pub default_value: Option>, + pub default_value_strategy: Option, } impl FieldDirective { @@ -27,13 +33,27 @@ impl FieldDirective { Ok(FieldDirective { after: consumer.after.option().unwrap_or(false), - field: StructFieldTypeNode { - name: consumer.name.take(meta)?, - r#type: consumer.r#type.take(meta)?, - docs: consumer.docs.option().unwrap_or_default(), - default_value, - default_value_strategy, - }, + name: consumer.name.take(meta)?, + r#type: consumer.r#type.take(meta)?, + docs: consumer.docs.option().unwrap_or_default(), + default_value, + default_value_strategy, + }) + } + + /// Construct a `StructFieldTypeNode` from this directive. + /// Returns an error if any unresolved directives remain. + pub fn to_struct_field_type_node(&self) -> CodamaResult { + Ok(StructFieldTypeNode { + name: self.name.clone(), + r#type: self.r#type.try_resolved()?.clone(), + docs: self.docs.clone(), + default_value: self + .default_value + .as_ref() + .map(|r| r.try_resolved().cloned()) + .transpose()?, + default_value_strategy: self.default_value_strategy, }) } } @@ -73,7 +93,11 @@ mod tests { directive, FieldDirective { after: false, - field: StructFieldTypeNode::new("age", NumberTypeNode::le(U8)), + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: None, + default_value_strategy: None, } ); } @@ -86,7 +110,11 @@ mod tests { directive, FieldDirective { after: true, - field: StructFieldTypeNode::new("age", NumberTypeNode::le(U8)), + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: None, + default_value_strategy: None, } ); } @@ -99,10 +127,11 @@ mod tests { directive, FieldDirective { after: false, - field: StructFieldTypeNode { - default_value: Some(NumberValueNode::new(42u8).into()), - ..StructFieldTypeNode::new("age", NumberTypeNode::le(U8)) - }, + name: "age".into(), + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + docs: Docs::default(), + default_value: Some(Resolvable::Resolved(NumberValueNode::new(42u8).into())), + default_value_strategy: None, } ); } @@ -111,7 +140,7 @@ mod tests { fn with_docs_string() { let meta: Meta = syn::parse_quote! { field("splines", number(u8), docs = "Splines") }; let directive = FieldDirective::parse(&meta).unwrap(); - assert_eq!(directive.field.docs, vec!["Splines".to_string()].into()); + assert_eq!(directive.docs, vec!["Splines".to_string()].into()); } #[test] @@ -119,7 +148,7 @@ mod tests { let meta: Meta = syn::parse_quote! { field("age", number(u8), docs = ["Splines", "Must be pre-reticulated"]) }; let directive = FieldDirective::parse(&meta).unwrap(); assert_eq!( - directive.field.docs, + directive.docs, vec!["Splines".to_string(), "Must be pre-reticulated".to_string()].into() ); } diff --git a/codama-attributes/src/codama_directives/mod.rs b/codama-attributes/src/codama_directives/mod.rs index e58f8f4f..280f8a66 100644 --- a/codama-attributes/src/codama_directives/mod.rs +++ b/codama-attributes/src/codama_directives/mod.rs @@ -14,6 +14,7 @@ mod fixed_size_directive; mod name_directive; mod pda_directive; mod program_directive; +mod resolvable_directive; mod seed_directive; mod size_prefix_directive; mod skip_directive; @@ -32,6 +33,7 @@ pub use fixed_size_directive::*; pub use name_directive::*; pub use pda_directive::*; pub use program_directive::*; +pub use resolvable_directive::*; pub use seed_directive::*; pub use size_prefix_directive::*; pub use skip_directive::*; diff --git a/codama-attributes/src/codama_directives/resolvable_directive.rs b/codama-attributes/src/codama_directives/resolvable_directive.rs new file mode 100644 index 00000000..ebf8aa2e --- /dev/null +++ b/codama-attributes/src/codama_directives/resolvable_directive.rs @@ -0,0 +1,353 @@ +use crate::utils::FromMeta; +use codama_syn_helpers::Meta; +use std::fmt; + +/// A directive that needs to be resolved by an external extension. +/// Resolvable directives are detected by the `prefix::name(...)` syntax where +/// the path contains exactly two segments separated by `::`. +#[derive(Debug, Clone, PartialEq)] +pub struct ResolvableDirective { + /// The namespace prefix, e.g. `"wellknown"` in `wellknown::ata(...)`. + pub namespace: String, + /// The directive name, e.g. `"ata"` in `wellknown::ata(...)`. + pub name: String, + /// The full Meta content for the extension to parse during resolution. + pub meta: Meta, +} + +impl fmt::Display for ResolvableDirective { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", self.namespace, self.name) + } +} + +/// A value that is either resolved to a concrete node `T` or deferred +/// to an extension for resolution. +#[derive(Debug, Clone, PartialEq)] +pub enum Resolvable { + /// The value has been resolved to a concrete node. + Resolved(T), + /// The value needs to be resolved by an extension. + Unresolved(Box), +} + +impl Resolvable { + /// Parse a `Meta` into a `Resolvable`. If the path has exactly two + /// segments (i.e. `prefix::name`), returns `Unresolved(ResolvableDirective)`. + /// Otherwise, delegates to `T::from_meta`. + pub fn from_meta(meta: &Meta) -> syn::Result { + if let Ok(path) = meta.path() { + if path.segments.len() == 2 { + let namespace = path.segments[0].ident.to_string(); + let name = path.segments[1].ident.to_string(); + return Ok(Resolvable::Unresolved(Box::new(ResolvableDirective { + namespace, + name, + meta: meta.clone(), + }))); + } + } + T::from_meta(meta).map(Resolvable::Resolved) + } +} + +impl Resolvable { + /// Returns a reference to the resolved value, or `None` if unresolved. + pub fn resolved(&self) -> Option<&T> { + match self { + Resolvable::Resolved(value) => Some(value), + Resolvable::Unresolved(_) => None, + } + } + + /// Returns the resolved value, or an error if unresolved. + pub fn try_resolved(&self) -> Result<&T, codama_errors::CodamaError> { + match self { + Resolvable::Resolved(value) => Ok(value), + Resolvable::Unresolved(directive) => { + Err(codama_errors::CodamaError::UnresolvedDirective { + namespace: directive.namespace.clone(), + name: directive.name.clone(), + }) + } + } + } + + /// Consumes self and returns the resolved value, or an error if unresolved. + pub fn try_into_resolved(self) -> Result { + match self { + Resolvable::Resolved(value) => Ok(value), + Resolvable::Unresolved(directive) => { + Err(codama_errors::CodamaError::UnresolvedDirective { + namespace: directive.namespace, + name: directive.name, + }) + } + } + } + + /// Returns `true` if this is an unresolved directive. + pub fn is_unresolved(&self) -> bool { + matches!(self, Resolvable::Unresolved(_)) + } + + /// Returns `true` if this is a resolved value. + pub fn is_resolved(&self) -> bool { + matches!(self, Resolvable::Resolved(_)) + } + + /// Maps the resolved value using the given function. + pub fn map(self, f: impl FnOnce(T) -> U) -> Resolvable { + match self { + Resolvable::Resolved(value) => Resolvable::Resolved(f(value)), + Resolvable::Unresolved(directive) => Resolvable::Unresolved(directive), + } + } +} + +impl From for Resolvable { + fn from(value: T) -> Self { + Resolvable::Resolved(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codama_nodes::{InstructionInputValueNode, PublicKeyTypeNode, RegisteredTypeNode}; + + // -- Resolvable::from_meta -- + + #[test] + fn from_meta_resolves_builtin_type() { + let meta: Meta = syn::parse_quote! { public_key }; + let result = Resolvable::::from_meta(&meta).unwrap(); + assert!(result.is_resolved()); + assert_eq!( + result, + Resolvable::Resolved(PublicKeyTypeNode::new().into()) + ); + } + + #[test] + fn from_meta_detects_resolvable_type() { + let meta: Meta = syn::parse_quote! { foo::custom_type }; + let result = Resolvable::::from_meta(&meta).unwrap(); + assert!(result.is_unresolved()); + let Resolvable::Unresolved(ref directive) = result else { + panic!("expected unresolved"); + }; + assert_eq!(directive.namespace, "foo"); + assert_eq!(directive.name, "custom_type"); + } + + #[test] + fn from_meta_detects_resolvable_type_with_args() { + let meta: Meta = syn::parse_quote! { foo::custom_type(42) }; + let result = Resolvable::::from_meta(&meta).unwrap(); + assert!(result.is_unresolved()); + let Resolvable::Unresolved(ref directive) = result else { + panic!("expected unresolved"); + }; + assert_eq!(directive.namespace, "foo"); + assert_eq!(directive.name, "custom_type"); + } + + #[test] + fn from_meta_resolves_builtin_value() { + let meta: Meta = syn::parse_quote! { payer }; + let result = Resolvable::::from_meta(&meta).unwrap(); + assert!(result.is_resolved()); + } + + #[test] + fn from_meta_detects_resolvable_value() { + let meta: Meta = syn::parse_quote! { wellknown::ata(account("owner"), account("tokenProgram"), account("mint")) }; + let result = Resolvable::::from_meta(&meta).unwrap(); + assert!(result.is_unresolved()); + let Resolvable::Unresolved(ref directive) = result else { + panic!("expected unresolved"); + }; + assert_eq!(directive.namespace, "wellknown"); + assert_eq!(directive.name, "ata"); + } + + #[test] + fn from_meta_errors_on_unrecognized_builtin() { + let meta: Meta = syn::parse_quote! { banana }; + let result = Resolvable::::from_meta(&meta); + assert!(result.is_err()); + } + + // -- Resolvable helpers -- + + #[test] + fn resolved_returns_some_for_resolved() { + let r: Resolvable = Resolvable::Resolved(42); + assert_eq!(r.resolved(), Some(&42)); + } + + #[test] + fn resolved_returns_none_for_unresolved() { + let r: Resolvable = Resolvable::Unresolved(Box::new(ResolvableDirective { + namespace: "foo".into(), + name: "bar".into(), + meta: syn::parse_quote! { foo::bar }, + })); + assert_eq!(r.resolved(), None); + } + + #[test] + fn try_resolved_returns_ok_for_resolved() { + let r: Resolvable = Resolvable::Resolved(42); + assert_eq!(r.try_resolved().unwrap(), &42); + } + + #[test] + fn try_resolved_returns_err_for_unresolved() { + let r: Resolvable = Resolvable::Unresolved(Box::new(ResolvableDirective { + namespace: "foo".into(), + name: "bar".into(), + meta: syn::parse_quote! { foo::bar }, + })); + let err = r.try_resolved().unwrap_err(); + assert!(matches!( + err, + codama_errors::CodamaError::UnresolvedDirective { .. } + )); + } + + #[test] + fn try_into_resolved_returns_value_for_resolved() { + let r: Resolvable = Resolvable::Resolved(42); + assert_eq!(r.try_into_resolved().unwrap(), 42); + } + + #[test] + fn try_into_resolved_returns_err_for_unresolved() { + let r: Resolvable = Resolvable::Unresolved(Box::new(ResolvableDirective { + namespace: "foo".into(), + name: "bar".into(), + meta: syn::parse_quote! { foo::bar }, + })); + let err = r.try_into_resolved().unwrap_err(); + assert!(matches!( + err, + codama_errors::CodamaError::UnresolvedDirective { .. } + )); + } + + #[test] + fn map_transforms_resolved() { + let r: Resolvable = Resolvable::Resolved(42); + let mapped = r.map(|v| v.to_string()); + assert_eq!(mapped, Resolvable::Resolved("42".to_string())); + } + + #[test] + fn map_preserves_unresolved() { + let r: Resolvable = Resolvable::Unresolved(Box::new(ResolvableDirective { + namespace: "foo".into(), + name: "bar".into(), + meta: syn::parse_quote! { foo::bar }, + })); + let mapped: Resolvable = r.map(|v| v.to_string()); + assert!(mapped.is_unresolved()); + } + + // -- Directive-level integration -- + + #[test] + fn type_directive_with_resolvable() { + let meta: Meta = syn::parse_quote! { type = foo::custom_type }; + let directive = crate::TypeDirective::parse(&meta).unwrap(); + assert!(directive.node.is_unresolved()); + } + + #[test] + fn default_value_directive_with_resolvable() { + let meta: Meta = syn::parse_quote! { default_value = bar::my_value(1, 2, 3) }; + let directive = crate::DefaultValueDirective::parse(&meta).unwrap(); + assert!(directive.node.is_unresolved()); + let Resolvable::Unresolved(ref d) = directive.node else { + panic!("expected unresolved"); + }; + assert_eq!(d.namespace, "bar"); + assert_eq!(d.name, "my_value"); + } + + #[test] + fn account_directive_with_resolvable_default_value() { + let meta: Meta = syn::parse_quote! { account(name = "vault", writable, default_value = wellknown::ata(account("owner"))) }; + let item = syn::parse_quote! { struct Foo; }; + let ctx = crate::AttributeContext::Item(&item); + let directive = crate::AccountDirective::parse(&meta, &ctx).unwrap(); + assert_eq!(directive.name, codama_nodes::CamelCaseString::new("vault")); + assert!(directive.is_writable); + assert!(directive.default_value.as_ref().unwrap().is_unresolved()); + } + + #[test] + fn seed_directive_with_resolvable_type() { + let meta: Meta = syn::parse_quote! { seed(name = "authority", type = foo::custom_pubkey) }; + let item = syn::parse_quote! { struct Foo; }; + let ctx = crate::AttributeContext::Item(&item); + let directive = crate::SeedDirective::parse(&meta, &ctx).unwrap(); + match &directive.seed { + crate::SeedDirectiveType::Variable { name, r#type } => { + assert_eq!(name, "authority"); + assert!(r#type.is_unresolved()); + } + _ => panic!("expected Variable seed"), + } + } + + // -- Nested resolvable directives -- + + #[test] + fn field_directive_with_resolvable_type_and_value() { + let meta: Meta = + syn::parse_quote! { field("age", foo::custom_type, default_value = bar::custom_value) }; + let directive = crate::FieldDirective::parse(&meta).unwrap(); + assert!(directive.r#type.is_unresolved()); + assert!(directive.default_value.as_ref().unwrap().is_unresolved()); + let Resolvable::Unresolved(ref t) = directive.r#type else { + panic!("expected unresolved type"); + }; + assert_eq!(t.namespace, "foo"); + assert_eq!(t.name, "custom_type"); + let Resolvable::Unresolved(ref v) = directive.default_value.as_ref().unwrap() else { + panic!("expected unresolved value"); + }; + assert_eq!(v.namespace, "bar"); + assert_eq!(v.name, "custom_value"); + } + + #[test] + fn argument_directive_with_resolvable_type() { + let meta: Meta = syn::parse_quote! { argument("age", foo::number_type) }; + let directive = crate::ArgumentDirective::parse(&meta).unwrap(); + assert!(directive.r#type.is_unresolved()); + let Resolvable::Unresolved(ref t) = directive.r#type else { + panic!("expected unresolved type"); + }; + assert_eq!(t.namespace, "foo"); + assert_eq!(t.name, "number_type"); + } + + #[test] + fn seed_directive_with_resolvable_type_and_value() { + let meta: Meta = + syn::parse_quote! { seed(type = foo::custom_type, value = bar::custom_value) }; + let item = syn::parse_quote! { struct Foo; }; + let ctx = crate::AttributeContext::Item(&item); + let directive = crate::SeedDirective::parse(&meta, &ctx).unwrap(); + match &directive.seed { + crate::SeedDirectiveType::Constant { r#type, value } => { + assert!(r#type.is_unresolved()); + assert!(value.is_unresolved()); + } + _ => panic!("expected Constant seed"), + } + } +} diff --git a/codama-attributes/src/codama_directives/seed_directive.rs b/codama-attributes/src/codama_directives/seed_directive.rs index a0bad2c4..7fb515e3 100644 --- a/codama-attributes/src/codama_directives/seed_directive.rs +++ b/codama-attributes/src/codama_directives/seed_directive.rs @@ -1,9 +1,8 @@ use crate::{ - utils::{FromMeta, SetOnce}, - Attribute, AttributeContext, CodamaAttribute, CodamaDirective, + utils::SetOnce, Attribute, AttributeContext, CodamaAttribute, CodamaDirective, Resolvable, }; use codama_errors::CodamaError; -use codama_nodes::{ConstantPdaSeedNode, PdaSeedNode, TypeNode, ValueNode, VariablePdaSeedNode}; +use codama_nodes::{TypeNode, ValueNode}; use codama_syn_helpers::{extensions::*, Meta}; #[derive(Debug, PartialEq)] @@ -13,8 +12,18 @@ pub struct SeedDirective { #[derive(Debug, PartialEq)] pub enum SeedDirectiveType { + /// A seed that references a field by name. The type is inferred from the field. Linked(String), - Defined(PdaSeedNode), + /// A variable seed with a name and type (which may be a plugin directive). + Variable { + name: String, + r#type: Resolvable, + }, + /// A constant seed with a type and value (which may be plugin directives). + Constant { + r#type: Resolvable, + value: Resolvable, + }, } impl SeedDirective { @@ -32,15 +41,17 @@ impl SeedDirective { .ok_or_else(|| meta.error("seed must at least specify `name` for variable seeds or `type` and `value` for constant seeds"))?; let mut name = SetOnce::::new("name"); - let mut r#type = SetOnce::::new("type"); - let mut value = SetOnce::::new("value"); + let mut r#type = SetOnce::>::new("type"); + let mut value = SetOnce::>::new("value"); pl.each(|ref meta| match (meta.path_str().as_str(), constant_seed) { ("name", true) => Err(meta.error("constant seeds cannot specify name")), ("name", false) => name.set(meta.as_value()?.as_expr()?.as_string()?, meta), - ("value", true) => value.set(ValueNode::from_meta(meta.as_value()?)?, meta), + ("value", true) => { + value.set(Resolvable::::from_meta(meta.as_value()?)?, meta) + } ("value", false) => Err(meta.error("variable seeds cannot specify value")), - ("type", _) => r#type.set(TypeNode::from_meta(meta.as_value()?)?, meta), + ("type", _) => r#type.set(Resolvable::::from_meta(meta.as_value()?)?, meta), _ => Err(meta.error("unrecognized attribute")), })?; @@ -58,14 +69,16 @@ impl SeedDirective { match constant_seed { true => Ok(Self { - seed: SeedDirectiveType::Defined( - ConstantPdaSeedNode::new(r#type.take(meta)?, value.take(meta)?).into(), - ), + seed: SeedDirectiveType::Constant { + r#type: r#type.take(meta)?, + value: value.take(meta)?, + }, }), false => Ok(Self { - seed: SeedDirectiveType::Defined( - VariablePdaSeedNode::new(name.take(meta)?, r#type.take(meta)?).into(), - ), + seed: SeedDirectiveType::Variable { + name: name.take(meta)?, + r#type: r#type.take(meta)?, + }, }), } } @@ -119,10 +132,10 @@ mod tests { assert_eq!( directive, SeedDirective { - seed: SeedDirectiveType::Defined( - ConstantPdaSeedNode::new(NumberTypeNode::le(U8), NumberValueNode::new(42u8)) - .into() - ), + seed: SeedDirectiveType::Constant { + r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()), + value: Resolvable::Resolved(NumberValueNode::new(42u8).into()), + }, } ); } @@ -136,9 +149,10 @@ mod tests { assert_eq!( directive, SeedDirective { - seed: SeedDirectiveType::Defined( - VariablePdaSeedNode::new("authority", PublicKeyTypeNode::new()).into() - ), + seed: SeedDirectiveType::Variable { + name: "authority".to_string(), + r#type: Resolvable::Resolved(PublicKeyTypeNode::new().into()), + }, } ); } diff --git a/codama-attributes/src/codama_directives/type_directive.rs b/codama-attributes/src/codama_directives/type_directive.rs index 40f5f939..cd726faf 100644 --- a/codama-attributes/src/codama_directives/type_directive.rs +++ b/codama-attributes/src/codama_directives/type_directive.rs @@ -1,18 +1,18 @@ -use crate::{utils::FromMeta, Attribute, CodamaAttribute, CodamaDirective}; +use crate::{Attribute, CodamaAttribute, CodamaDirective, Resolvable}; use codama_errors::CodamaError; use codama_nodes::RegisteredTypeNode; use codama_syn_helpers::Meta; #[derive(Debug, PartialEq)] pub struct TypeDirective { - pub node: RegisteredTypeNode, + pub node: Resolvable, } impl TypeDirective { pub fn parse(meta: &Meta) -> syn::Result { let value = meta.assert_directive("type")?.as_value()?; Ok(Self { - node: RegisteredTypeNode::from_meta(value)?, + node: Resolvable::::from_meta(value)?, }) } } @@ -48,9 +48,24 @@ mod tests { #[test] fn ok() { let meta: Meta = parse_quote! { type = number(u16, le) }; - let node = TypeDirective::parse(&meta).unwrap().node; + let directive = TypeDirective::parse(&meta).unwrap(); + assert_eq!( + directive.node, + Resolvable::Resolved(NumberTypeNode::le(U16).into()) + ); + } + + #[test] + fn resolvable_directive() { + let meta: Meta = parse_quote! { type = foo::custom_type }; + let directive = TypeDirective::parse(&meta).unwrap(); - assert_eq!(node, NumberTypeNode::le(U16).into()); + assert!(directive.node.is_unresolved()); + let Resolvable::Unresolved(ref d) = directive.node else { + panic!("expected unresolved directive"); + }; + assert_eq!(d.namespace, "foo"); + assert_eq!(d.name, "custom_type"); } #[test] diff --git a/codama-attributes/src/codama_directives/type_nodes/struct_field_meta_consumer.rs b/codama-attributes/src/codama_directives/type_nodes/struct_field_meta_consumer.rs index cdb294a9..fbb300b5 100644 --- a/codama-attributes/src/codama_directives/type_nodes/struct_field_meta_consumer.rs +++ b/codama-attributes/src/codama_directives/type_nodes/struct_field_meta_consumer.rs @@ -1,6 +1,6 @@ use crate::{ utils::{FromMeta, MetaConsumer, SetOnce}, - DefaultValueDirective, + DefaultValueDirective, Resolvable, }; use codama_nodes::{ CamelCaseString, DefaultValueStrategy, Docs, InstructionInputValueNode, TypeNode, ValueNode, @@ -10,7 +10,7 @@ use codama_syn_helpers::{extensions::*, Meta}; pub(crate) struct StructFieldMetaConsumer { pub metas: Vec, pub name: SetOnce, - pub r#type: SetOnce, + pub r#type: SetOnce>, pub default_value: SetOnce, pub after: SetOnce, pub docs: SetOnce, @@ -47,7 +47,7 @@ impl StructFieldMetaConsumer { } "type" => { this.r#type - .set(TypeNode::from_meta(meta.as_value()?)?, meta)?; + .set(Resolvable::::from_meta(meta.as_value()?)?, meta)?; Ok(None) } "docs" => { @@ -59,8 +59,9 @@ impl StructFieldMetaConsumer { this.name.set(value.into(), meta)?; return Ok(None); } - if let Ok(node) = TypeNode::from_meta(&meta) { - this.r#type.set(node, meta)?; + // Try parsing as a built-in type node or a resolvable directive. + if let Ok(resolvable) = Resolvable::::from_meta(&meta) { + this.r#type.set(resolvable, meta)?; return Ok(None); } Ok(Some(meta)) @@ -106,13 +107,20 @@ impl StructFieldMetaConsumer { .and_then(|directive| directive.default_value_strategy) } - pub fn default_value_node(&self) -> Option { + pub fn default_value_node(&self) -> Option> { self.default_value .option_ref() - .and_then(|directive| ValueNode::try_from(directive.node.clone()).ok()) + .and_then(|directive| match &directive.node { + Resolvable::Resolved(node) => ValueNode::try_from(node.clone()) + .ok() + .map(Resolvable::Resolved), + Resolvable::Unresolved(d) => Some(Resolvable::Unresolved(d.clone())), + }) } - pub fn default_instruction_input_value_node(&self) -> Option { + pub fn default_instruction_input_value_node( + &self, + ) -> Option> { self.default_value .option_ref() .map(|directive| directive.node.clone()) diff --git a/codama-attributes/src/codama_directives/type_nodes/struct_field_type_node.rs b/codama-attributes/src/codama_directives/type_nodes/struct_field_type_node.rs index dd96bbe8..0f083a6b 100644 --- a/codama-attributes/src/codama_directives/type_nodes/struct_field_type_node.rs +++ b/codama-attributes/src/codama_directives/type_nodes/struct_field_type_node.rs @@ -3,7 +3,7 @@ use crate::{ utils::{FromMeta, MetaConsumer}, }; use codama_nodes::{Docs, StructFieldTypeNode}; -use codama_syn_helpers::Meta; +use codama_syn_helpers::{extensions::*, Meta}; impl FromMeta for StructFieldTypeNode { fn from_meta(meta: &Meta) -> syn::Result { @@ -13,12 +13,23 @@ impl FromMeta for StructFieldTypeNode { .consume_default_value()? .assert_fully_consumed()?; - let default_value = consumer.default_value_node(); let default_value_strategy = consumer.default_value_strategy(); + let default_value = consumer + .default_value_node() + .map(|r| r.try_into_resolved().map_err(|e| meta.error(e.to_string()))) + .transpose()?; + + // When used as a type node (e.g., `type = field("age", number(u8))`), + // unresolved directives must already be resolved. + let r#type = consumer + .r#type + .take(meta)? + .try_into_resolved() + .map_err(|e| meta.error(e.to_string()))?; Ok(StructFieldTypeNode { name: consumer.name.take(meta)?, - r#type: consumer.r#type.take(meta)?, + r#type, default_value, default_value_strategy, docs: Docs::default(), diff --git a/codama-attributes/src/utils/macros.rs b/codama-attributes/src/utils/macros.rs index 7f85c0d0..e1a67b39 100644 --- a/codama-attributes/src/utils/macros.rs +++ b/codama-attributes/src/utils/macros.rs @@ -4,7 +4,7 @@ macro_rules! assert_type { { let meta: codama_syn_helpers::Meta = syn::parse_quote! { type = $($attr)* }; let node = $crate::TypeDirective::parse(&meta).unwrap().node; - assert_eq!(node, $expected); + assert_eq!(node, $crate::Resolvable::Resolved($expected)); } }; } @@ -26,7 +26,7 @@ macro_rules! assert_value { { let meta: codama_syn_helpers::Meta = syn::parse_quote! { default_value = $($attr)* }; let node = $crate::DefaultValueDirective::parse(&meta).unwrap().node; - assert_eq!(node, $expected); + assert_eq!(node, $crate::Resolvable::Resolved($expected)); } }; } diff --git a/codama-errors/src/errors.rs b/codama-errors/src/errors.rs index 29e29dd7..88a65753 100644 --- a/codama-errors/src/errors.rs +++ b/codama-errors/src/errors.rs @@ -38,6 +38,9 @@ pub enum CodamaError { #[error("Invalid Codama directive, Expected {expected}, got {actual}")] InvalidCodamaDirective { expected: String, actual: String }, + + #[error("Unresolved directive: {namespace}::{name}")] + UnresolvedDirective { namespace: String, name: String }, } pub type CodamaResult = Result; diff --git a/codama-korok-plugins/Cargo.toml b/codama-korok-plugins/Cargo.toml index 43b9da48..4b23db17 100644 --- a/codama-korok-plugins/Cargo.toml +++ b/codama-korok-plugins/Cargo.toml @@ -7,6 +7,11 @@ edition = { workspace = true } license = { workspace = true } [dependencies] +codama-attributes = { version = "0.7.4", path = "../codama-attributes" } codama-errors = { version = "0.7.4", path = "../codama-errors" } codama-korok-visitors = { version = "0.7.4", path = "../codama-korok-visitors" } codama-koroks = { version = "0.7.4", path = "../codama-koroks" } +codama-nodes = { version = "0.7.4", path = "../codama-nodes" } + +[dev-dependencies] +syn = { version = "2.0", features = ["extra-traits", "full"] } diff --git a/codama-korok-plugins/src/directive_resolver.rs b/codama-korok-plugins/src/directive_resolver.rs new file mode 100644 index 00000000..7e77f728 --- /dev/null +++ b/codama-korok-plugins/src/directive_resolver.rs @@ -0,0 +1,22 @@ +use codama_attributes::ResolvableDirective; +use codama_errors::CodamaResult; +use codama_nodes::{InstructionInputValueNode, RegisteredTypeNode}; + +/// Trait that allows plugins to resolve directives from other plugins. +/// The framework builds a `DirectiveResolver` from all installed plugins +/// and passes it to `resolve_type_directive` / `resolve_value_directive`. +pub trait DirectiveResolver { + /// Resolve a resolvable directive into a type node. + /// Returns an error if no plugin can resolve it. + fn resolve_type_directive( + &self, + directive: &ResolvableDirective, + ) -> CodamaResult; + + /// Resolve a resolvable directive into an instruction input value node. + /// Returns an error if no plugin can resolve it. + fn resolve_value_directive( + &self, + directive: &ResolvableDirective, + ) -> CodamaResult; +} diff --git a/codama-korok-plugins/src/lib.rs b/codama-korok-plugins/src/lib.rs index d0972710..1f4089a0 100644 --- a/codama-korok-plugins/src/lib.rs +++ b/codama-korok-plugins/src/lib.rs @@ -1,5 +1,9 @@ mod default_plugin; +mod directive_resolver; mod plugin; +mod resolve_directives_visitor; pub use default_plugin::*; +pub use directive_resolver::*; pub use plugin::*; +pub use resolve_directives_visitor::*; diff --git a/codama-korok-plugins/src/plugin.rs b/codama-korok-plugins/src/plugin.rs index a89e09e4..80d49a70 100644 --- a/codama-korok-plugins/src/plugin.rs +++ b/codama-korok-plugins/src/plugin.rs @@ -1,7 +1,30 @@ +use crate::DirectiveResolver; +use codama_attributes::ResolvableDirective; use codama_errors::CodamaResult; use codama_korok_visitors::KorokVisitable; +use codama_nodes::{InstructionInputValueNode, RegisteredTypeNode}; pub trait KorokPlugin { + /// Try to resolve a resolvable type directive belonging to this plugin. + /// Returns `None` if this plugin does not handle the given directive. + fn resolve_type_directive( + &self, + _directive: &ResolvableDirective, + _resolver: &dyn DirectiveResolver, + ) -> Option> { + None + } + + /// Try to resolve a resolvable value directive belonging to this plugin. + /// Returns `None` if this plugin does not handle the given directive. + fn resolve_value_directive( + &self, + _directive: &ResolvableDirective, + _resolver: &dyn DirectiveResolver, + ) -> Option> { + None + } + fn on_initialized(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> { Ok(()) } @@ -19,20 +42,72 @@ pub trait KorokPlugin { } } +/// A `DirectiveResolver` built from all installed plugins. +/// Iterates over plugins to find one that can resolve the given directive. +pub struct CompositeDirectiveResolver<'a> { + plugins: &'a [Box], +} + +impl<'a> CompositeDirectiveResolver<'a> { + pub fn new(plugins: &'a [Box]) -> Self { + Self { plugins } + } +} + +impl<'a> DirectiveResolver for CompositeDirectiveResolver<'a> { + fn resolve_type_directive( + &self, + directive: &ResolvableDirective, + ) -> CodamaResult { + for plugin in self.plugins { + if let Some(result) = plugin.resolve_type_directive(directive, self) { + return result; + } + } + Err(codama_errors::CodamaError::UnresolvedDirective { + namespace: directive.namespace.clone(), + name: directive.name.clone(), + }) + } + + fn resolve_value_directive( + &self, + directive: &ResolvableDirective, + ) -> CodamaResult { + for plugin in self.plugins { + if let Some(result) = plugin.resolve_value_directive(directive, self) { + return result; + } + } + Err(codama_errors::CodamaError::UnresolvedDirective { + namespace: directive.namespace.clone(), + name: directive.name.clone(), + }) + } +} + pub type ResolvePluginsResult<'a> = Box CodamaResult<()> + 'a>; /// Combine all plugins into a single function that runs them in sequence. pub fn resolve_plugins<'a>(plugins: &'a [Box]) -> ResolvePluginsResult<'a> { Box::new(move |visitable: &mut dyn KorokVisitable| { + // Phase 0: Resolve all resolvable directives. + let resolver = CompositeDirectiveResolver::new(plugins); + visitable.accept(&mut crate::ResolveDirectivesVisitor::new(&resolver))?; + + // Phase 1: Initialize. plugins .iter() .try_for_each(|plugin| plugin.on_initialized(visitable))?; + // Phase 2: Set fields. plugins .iter() .try_for_each(|plugin| plugin.on_fields_set(visitable))?; + // Phase 3: Set program items. plugins .iter() .try_for_each(|plugin| plugin.on_program_items_set(visitable))?; + // Phase 4: Set root node. plugins .iter() .try_for_each(|plugin| plugin.on_root_node_set(visitable))?; @@ -44,13 +119,16 @@ pub fn resolve_plugins<'a>(plugins: &'a [Box]) -> ResolveP mod tests { use super::*; use codama_korok_visitors::KorokVisitor; + use codama_nodes::PublicKeyTypeNode; use std::{cell::RefCell, rc::Rc}; - struct LoggingPluging { + // -- Lifecycle ordering tests -- + + struct LoggingPlugin { id: String, logs: Rc>>, } - impl LoggingPluging { + impl LoggingPlugin { fn new(id: &str, logs: Rc>>) -> Self { Self { id: id.into(), @@ -58,7 +136,7 @@ mod tests { } } } - impl KorokPlugin for LoggingPluging { + impl KorokPlugin for LoggingPlugin { fn on_initialized(&self, _visitable: &mut dyn KorokVisitable) -> CodamaResult<()> { self.logs .borrow_mut() @@ -99,8 +177,8 @@ mod tests { fn test_resolve_plugins() -> CodamaResult<()> { let logs = Rc::new(RefCell::new(Vec::new())); let plugins: Vec> = vec![ - Box::new(LoggingPluging::new("A", logs.clone())), - Box::new(LoggingPluging::new("B", logs.clone())), + Box::new(LoggingPlugin::new("A", logs.clone())), + Box::new(LoggingPlugin::new("B", logs.clone())), ]; let run_plugins = resolve_plugins(&plugins); @@ -121,4 +199,269 @@ mod tests { ); Ok(()) } + + // -- CompositeDirectiveResolver tests -- + + struct MockTypePlugin; + impl KorokPlugin for MockTypePlugin { + fn resolve_type_directive( + &self, + directive: &ResolvableDirective, + _resolver: &dyn DirectiveResolver, + ) -> Option> { + if directive.namespace == "mock" && directive.name == "pubkey" { + Some(Ok(PublicKeyTypeNode::new().into())) + } else { + None + } + } + } + + struct MockValuePlugin; + impl KorokPlugin for MockValuePlugin { + fn resolve_value_directive( + &self, + directive: &ResolvableDirective, + _resolver: &dyn DirectiveResolver, + ) -> Option> { + if directive.namespace == "mock" && directive.name == "payer" { + Some(Ok(codama_nodes::PayerValueNode::new().into())) + } else { + None + } + } + } + + fn make_directive(namespace: &str, name: &str) -> ResolvableDirective { + ResolvableDirective { + namespace: namespace.into(), + name: name.into(), + meta: syn::parse_quote! { foo::bar }, + } + } + + #[test] + fn composite_resolver_dispatches_type() { + let plugins: Vec> = vec![Box::new(MockTypePlugin)]; + let resolver = CompositeDirectiveResolver::new(&plugins); + let directive = make_directive("mock", "pubkey"); + let result = resolver.resolve_type_directive(&directive).unwrap(); + assert_eq!(result, PublicKeyTypeNode::new().into()); + } + + #[test] + fn composite_resolver_dispatches_value() { + let plugins: Vec> = vec![Box::new(MockValuePlugin)]; + let resolver = CompositeDirectiveResolver::new(&plugins); + let directive = make_directive("mock", "payer"); + let result = resolver.resolve_value_directive(&directive).unwrap(); + assert_eq!(result, codama_nodes::PayerValueNode::new().into()); + } + + #[test] + fn composite_resolver_returns_error_when_unresolved() { + let plugins: Vec> = vec![]; + let resolver = CompositeDirectiveResolver::new(&plugins); + let directive = make_directive("unknown", "thing"); + let err = resolver.resolve_type_directive(&directive).unwrap_err(); + assert!(matches!( + err, + codama_errors::CodamaError::UnresolvedDirective { + namespace, + name, + } if namespace == "unknown" && name == "thing" + )); + } + + #[test] + fn composite_resolver_skips_non_matching_plugins() { + let plugins: Vec> = vec![ + Box::new(MockValuePlugin), // doesn't handle types + Box::new(MockTypePlugin), // handles mock::pubkey + ]; + let resolver = CompositeDirectiveResolver::new(&plugins); + let directive = make_directive("mock", "pubkey"); + let result = resolver.resolve_type_directive(&directive).unwrap(); + assert_eq!(result, PublicKeyTypeNode::new().into()); + } + + // -- Nested resolution (two plugins) -- + + /// Plugin A resolves `a::wrapper` by calling the resolver for its inner type. + struct PluginA; + impl KorokPlugin for PluginA { + fn resolve_type_directive( + &self, + directive: &ResolvableDirective, + resolver: &dyn DirectiveResolver, + ) -> Option> { + if directive.namespace != "a" || directive.name != "wrapper" { + return None; + } + // Simulate parsing the inner directive and delegating to resolver. + let inner = ResolvableDirective { + namespace: "b".into(), + name: "inner".into(), + meta: syn::parse_quote! { b::inner }, + }; + Some(resolver.resolve_type_directive(&inner)) + } + } + + /// Plugin B resolves `b::inner` → PublicKeyTypeNode. + struct PluginB; + impl KorokPlugin for PluginB { + fn resolve_type_directive( + &self, + directive: &ResolvableDirective, + _resolver: &dyn DirectiveResolver, + ) -> Option> { + if directive.namespace == "b" && directive.name == "inner" { + Some(Ok(PublicKeyTypeNode::new().into())) + } else { + None + } + } + } + + #[test] + fn nested_resolution_across_two_plugins() { + let plugins: Vec> = vec![Box::new(PluginA), Box::new(PluginB)]; + let resolver = CompositeDirectiveResolver::new(&plugins); + let directive = make_directive("a", "wrapper"); + // PluginA resolves a::wrapper by asking the resolver for b::inner, + // which PluginB resolves to PublicKeyTypeNode. + let result = resolver.resolve_type_directive(&directive).unwrap(); + assert_eq!(result, PublicKeyTypeNode::new().into()); + } + + // -- E2e: resolve directives on a korok tree -- + + #[test] + fn e2e_resolves_type_directive_on_korok() -> CodamaResult<()> { + use codama_attributes::{Resolvable, TryFromFilter, TypeDirective}; + + // Build a korok tree from source with a resolvable type directive. + let item: syn::Item = syn::parse_quote! { + #[codama(type = mock::pubkey)] + struct MyAccount; + }; + let mut korok = codama_koroks::StructKorok::parse(&item)?; + + // Verify the directive is unresolved before resolution. + let directive_before = korok + .attributes + .get_last(TypeDirective::filter) + .expect("should have a type directive"); + assert!(directive_before.node.is_unresolved()); + + // Run resolve_plugins with MockTypePlugin. + let plugins: Vec> = vec![Box::new(MockTypePlugin)]; + let run_plugins = resolve_plugins(&plugins); + run_plugins(&mut korok)?; + + // Verify the directive is now resolved. + let directive_after = korok + .attributes + .get_last(TypeDirective::filter) + .expect("should still have a type directive"); + assert!(directive_after.node.is_resolved()); + assert_eq!( + directive_after.node, + Resolvable::Resolved(PublicKeyTypeNode::new().into()) + ); + Ok(()) + } + + #[test] + fn e2e_resolves_nested_directives_across_two_plugins() -> CodamaResult<()> { + use codama_attributes::{Resolvable, TryFromFilter, TypeDirective}; + + // Build a korok tree from source with a resolvable type directive. + // PluginA handles a::wrapper, which internally delegates to PluginB for b::inner. + let item: syn::Item = syn::parse_quote! { + #[codama(type = a::wrapper)] + struct MyAccount; + }; + let mut korok = codama_koroks::StructKorok::parse(&item)?; + + // Verify unresolved before. + let directive_before = korok + .attributes + .get_last(TypeDirective::filter) + .expect("should have a type directive"); + assert!(directive_before.node.is_unresolved()); + + // Run resolve_plugins with both plugins. + let plugins: Vec> = vec![Box::new(PluginA), Box::new(PluginB)]; + let run_plugins = resolve_plugins(&plugins); + run_plugins(&mut korok)?; + + // Verify the directive is resolved to PublicKeyTypeNode + // (PluginA delegated to PluginB which returns PublicKeyTypeNode). + let directive_after = korok + .attributes + .get_last(TypeDirective::filter) + .expect("should still have a type directive"); + assert!(directive_after.node.is_resolved()); + assert_eq!( + directive_after.node, + Resolvable::Resolved(PublicKeyTypeNode::new().into()) + ); + Ok(()) + } + + #[test] + fn e2e_resolves_value_directive_on_korok() -> CodamaResult<()> { + use codama_attributes::{DefaultValueDirective, Resolvable, TryFromFilter}; + + // Build a korok tree from source with a resolvable default value directive. + let field: syn::Field = syn::parse_quote! { + #[codama(default_value = mock::payer)] + pub authority: Pubkey + }; + let mut korok = codama_koroks::FieldKorok::parse(&field)?; + + // Verify the directive is unresolved before resolution. + let directive_before = korok + .attributes + .get_last(DefaultValueDirective::filter) + .expect("should have a default value directive"); + assert!(directive_before.node.is_unresolved()); + + // Run resolve_plugins with MockValuePlugin. + let plugins: Vec> = vec![Box::new(MockValuePlugin)]; + let run_plugins = resolve_plugins(&plugins); + run_plugins(&mut korok)?; + + // Verify the directive is now resolved. + let directive_after = korok + .attributes + .get_last(DefaultValueDirective::filter) + .expect("should still have a default value directive"); + assert!(directive_after.node.is_resolved()); + assert_eq!( + directive_after.node, + Resolvable::Resolved(codama_nodes::PayerValueNode::new().into()) + ); + Ok(()) + } + + #[test] + fn e2e_unresolved_directive_errors() { + // Build a korok with a resolvable directive but no plugin to resolve it. + let item: syn::Item = syn::parse_quote! { + #[codama(type = unknown::thing)] + struct MyAccount; + }; + let mut korok = codama_koroks::StructKorok::parse(&item).unwrap(); + + let plugins: Vec> = vec![]; + let run_plugins = resolve_plugins(&plugins); + let err = run_plugins(&mut korok).unwrap_err(); + assert!(matches!( + err, + codama_errors::CodamaError::UnresolvedDirective { .. } + )); + } } diff --git a/codama-korok-plugins/src/resolve_directives_visitor.rs b/codama-korok-plugins/src/resolve_directives_visitor.rs new file mode 100644 index 00000000..30f2612b --- /dev/null +++ b/codama-korok-plugins/src/resolve_directives_visitor.rs @@ -0,0 +1,178 @@ +use crate::DirectiveResolver; +use codama_attributes::{Attribute, CodamaDirective, Resolvable, SeedDirectiveType}; +use codama_errors::CodamaResult; +use codama_korok_visitors::KorokVisitor; +use codama_koroks::*; +use codama_nodes::{InstructionInputValueNode, ValueNode}; + +/// A visitor that resolves all `Resolvable::Unresolved` entries in the korok tree +/// by delegating to the `DirectiveResolver`. +/// +/// This visitor runs automatically as the first step in `resolve_plugins()`, +/// before any lifecycle hooks fire. +pub struct ResolveDirectivesVisitor<'a> { + resolver: &'a dyn DirectiveResolver, +} + +impl<'a> ResolveDirectivesVisitor<'a> { + pub fn new(resolver: &'a dyn DirectiveResolver) -> Self { + Self { resolver } + } + + fn resolve_attributes( + &self, + attributes: &mut codama_attributes::Attributes, + ) -> CodamaResult<()> { + for attr in attributes.iter_mut() { + let Attribute::Codama(codama_attr) = attr else { + continue; + }; + self.resolve_directive(codama_attr.directive.as_mut())?; + } + Ok(()) + } + + fn resolve_directive(&self, directive: &mut CodamaDirective) -> CodamaResult<()> { + match directive { + CodamaDirective::Type(d) => { + self.resolve_type(&mut d.node)?; + } + CodamaDirective::DefaultValue(d) => { + self.resolve_instruction_input_value(&mut d.node)?; + } + CodamaDirective::Account(d) => { + if let Some(ref mut dv) = d.default_value { + self.resolve_instruction_input_value(dv)?; + } + } + CodamaDirective::Field(d) => { + self.resolve_type_node(&mut d.r#type)?; + if let Some(ref mut dv) = d.default_value { + self.resolve_value_node(dv)?; + } + } + CodamaDirective::Argument(d) => { + self.resolve_type_node(&mut d.r#type)?; + if let Some(ref mut dv) = d.default_value { + self.resolve_instruction_input_value(dv)?; + } + } + CodamaDirective::Seed(d) => match &mut d.seed { + SeedDirectiveType::Variable { r#type, .. } => { + self.resolve_type_node(r#type)?; + } + SeedDirectiveType::Constant { r#type, value } => { + self.resolve_type_node(r#type)?; + self.resolve_value_node(value)?; + } + SeedDirectiveType::Linked(_) => {} + }, + // Other directives don't contain resolvable slots. + _ => {} + } + Ok(()) + } + + fn resolve_type( + &self, + resolvable: &mut Resolvable, + ) -> CodamaResult<()> { + if let Resolvable::Unresolved(directive) = resolvable { + let resolved = self.resolver.resolve_type_directive(directive)?; + *resolvable = Resolvable::Resolved(resolved); + } + Ok(()) + } + + fn resolve_type_node( + &self, + resolvable: &mut Resolvable, + ) -> CodamaResult<()> { + if let Resolvable::Unresolved(directive) = resolvable { + let registered = self.resolver.resolve_type_directive(directive)?; + let type_node = codama_nodes::TypeNode::try_from(registered)?; + *resolvable = Resolvable::Resolved(type_node); + } + Ok(()) + } + + fn resolve_instruction_input_value( + &self, + resolvable: &mut Resolvable, + ) -> CodamaResult<()> { + if let Resolvable::Unresolved(directive) = resolvable { + let resolved = self.resolver.resolve_value_directive(directive)?; + *resolvable = Resolvable::Resolved(resolved); + } + Ok(()) + } + + fn resolve_value_node(&self, resolvable: &mut Resolvable) -> CodamaResult<()> { + if let Resolvable::Unresolved(directive) = resolvable { + let instruction_input = self.resolver.resolve_value_directive(directive)?; + let value_node = ValueNode::try_from(instruction_input)?; + *resolvable = Resolvable::Resolved(value_node); + } + Ok(()) + } +} + +impl KorokVisitor for ResolveDirectivesVisitor<'_> { + fn visit_root(&mut self, korok: &mut RootKorok) -> CodamaResult<()> { + self.visit_children(korok) + } + + fn visit_crate(&mut self, korok: &mut CrateKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_file_module(&mut self, korok: &mut FileModuleKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_module(&mut self, korok: &mut ModuleKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_struct(&mut self, korok: &mut StructKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_enum(&mut self, korok: &mut EnumKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_enum_variant(&mut self, korok: &mut EnumVariantKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_field(&mut self, korok: &mut FieldKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes) + } + + fn visit_unsupported_item(&mut self, korok: &mut UnsupportedItemKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes) + } + + fn visit_impl(&mut self, korok: &mut ImplKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes)?; + self.visit_children(korok) + } + + fn visit_const(&mut self, korok: &mut ConstKorok) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes) + } + + fn visit_unsupported_impl_item( + &mut self, + korok: &mut UnsupportedImplItemKorok, + ) -> CodamaResult<()> { + self.resolve_attributes(&mut korok.attributes) + } +} diff --git a/codama-korok-visitors/src/apply_type_overrides_visitor.rs b/codama-korok-visitors/src/apply_type_overrides_visitor.rs index 34869e99..a437856c 100644 --- a/codama-korok-visitors/src/apply_type_overrides_visitor.rs +++ b/codama-korok-visitors/src/apply_type_overrides_visitor.rs @@ -78,7 +78,7 @@ fn apply_type_override(mut korok: KorokMut) -> CodamaResult<()> { return Ok(()); }; - let registered_type_node = directive.node.clone(); + let registered_type_node = directive.node.try_resolved()?.clone(); match (&mut korok, TypeNode::try_from(registered_type_node.clone())) { (KorokMut::Field(field_korok), Ok(type_node)) => field_korok.set_type_node(type_node), _ => korok.set_node(Some(registered_type_node.into())), diff --git a/codama-korok-visitors/src/combine_types_visitor.rs b/codama-korok-visitors/src/combine_types_visitor.rs index bd2bb073..c1a5b7a7 100644 --- a/codama-korok-visitors/src/combine_types_visitor.rs +++ b/codama-korok-visitors/src/combine_types_visitor.rs @@ -129,8 +129,14 @@ impl CombineTypesVisitor { .get_all(FieldDirective::filter) .into_iter() .partition(|attr| !attr.after); - let before = before.into_iter().map(|attr| attr.field.clone()); - let after = after.into_iter().map(|attr| attr.field.clone()); + let before = before + .into_iter() + .map(|attr| attr.to_struct_field_type_node()) + .collect::>>()?; + let after = after + .into_iter() + .map(|attr| attr.to_struct_field_type_node()) + .collect::>>()?; Ok(before.into_iter().chain(fields).chain(after).collect()) } diff --git a/codama-korok-visitors/src/set_accounts_visitor.rs b/codama-korok-visitors/src/set_accounts_visitor.rs index be9950d7..b105c6f2 100644 --- a/codama-korok-visitors/src/set_accounts_visitor.rs +++ b/codama-korok-visitors/src/set_accounts_visitor.rs @@ -63,7 +63,7 @@ impl KorokVisitor for SetAccountsVisitor { account, &korok.attributes, &korok.fields, - ); + )?; korok.node = Some(ProgramDirective::apply(&korok.attributes, node)); Ok(()) } @@ -170,7 +170,7 @@ impl KorokVisitor for SetAccountsVisitor { account, &korok.attributes, &korok.fields, - )); + )?); Ok(()) } } @@ -234,10 +234,10 @@ fn wrap_account_in_program_node_when_seeds_are_defined( account: AccountNode, attributes: &Attributes, fields: &[FieldKorok], -) -> Node { +) -> CodamaResult { // Return the AccountNode directly if there are no seed directives. if !attributes.has_codama_attribute("seed") { - return account.into(); + return Ok(account.into()); } // Create a PDA node from the seed directives. @@ -245,10 +245,10 @@ fn wrap_account_in_program_node_when_seeds_are_defined( Some(pda_link) => pda_link.name.clone(), None => account.name.clone(), }; - let pda = parse_pda_node(pda_name, attributes, fields); + let pda = parse_pda_node(pda_name, attributes, fields)?; // Add both the account and the linked PDA to a program node. - ProgramNode { + Ok(ProgramNode { accounts: vec![AccountNode { pda: account.pda.or(Some(PdaLinkNode::new(pda.name.clone()))), ..account @@ -256,5 +256,5 @@ fn wrap_account_in_program_node_when_seeds_are_defined( pdas: vec![pda], ..ProgramNode::default() } - .into() + .into()) } diff --git a/codama-korok-visitors/src/set_default_values_visitor.rs b/codama-korok-visitors/src/set_default_values_visitor.rs index 4bf0feed..8b6de991 100644 --- a/codama-korok-visitors/src/set_default_values_visitor.rs +++ b/codama-korok-visitors/src/set_default_values_visitor.rs @@ -78,7 +78,7 @@ fn set_default_values(mut korok: KorokMut) -> CodamaResult<()> { }; // Ensure there is a node to set a default value on. - let Some(node) = get_node_with_default_value(korok.node(), attributes) else { + let Some(node) = get_node_with_default_value(korok.node(), attributes)? else { return Ok(()); }; @@ -86,31 +86,37 @@ fn set_default_values(mut korok: KorokMut) -> CodamaResult<()> { Ok(()) } -fn get_node_with_default_value(node: &Option, attributes: &Attributes) -> Option { - let directive = attributes.get_last(DefaultValueDirective::filter)?; +fn get_node_with_default_value( + node: &Option, + attributes: &Attributes, +) -> CodamaResult> { + let Some(directive) = attributes.get_last(DefaultValueDirective::filter) else { + return Ok(None); + }; + let resolved_node = directive.node.try_resolved()?; match node { // Handle struct fields. Some(Node::Type(RegisteredTypeNode::StructField(field))) => { - let value = ValueNode::try_from(directive.node.clone()).ok()?; - Some( + let value = ValueNode::try_from(resolved_node.clone()).ok(); + Ok(value.map(|value| { StructFieldTypeNode { default_value: Some(value), default_value_strategy: directive.default_value_strategy, ..field.clone() } - .into(), - ) + .into() + })) } // Handle instruction arguments. - Some(Node::InstructionArgument(argument)) => Some( + Some(Node::InstructionArgument(argument)) => Ok(Some( InstructionArgumentNode { - default_value: Some(directive.node.clone()), + default_value: Some(resolved_node.clone()), default_value_strategy: directive.default_value_strategy, ..argument.clone() } .into(), - ), - _ => None, + )), + _ => Ok(None), } } diff --git a/codama-korok-visitors/src/set_instructions_visitors.rs b/codama-korok-visitors/src/set_instructions_visitors.rs index 984515a2..eccdb718 100644 --- a/codama-korok-visitors/src/set_instructions_visitors.rs +++ b/codama-korok-visitors/src/set_instructions_visitors.rs @@ -64,8 +64,8 @@ impl KorokVisitor for SetInstructionsVisitor { let (name, data) = parse_struct(korok)?; let instruction = InstructionNode { name, - accounts: parse_accounts(&korok.attributes, &korok.fields), - arguments: parse_arguments(&korok.attributes, &korok.fields, data, None), + accounts: parse_accounts(&korok.attributes, &korok.fields)?, + arguments: parse_arguments(&korok.attributes, &korok.fields, data, None)?, discriminators: DiscriminatorDirective::nodes(&korok.attributes), ..InstructionNode::default() }; @@ -156,13 +156,13 @@ impl KorokVisitor for SetInstructionsVisitor { korok.node = Some( InstructionNode { name, - accounts: parse_accounts(&korok.attributes, &korok.fields), + accounts: parse_accounts(&korok.attributes, &korok.fields)?, arguments: parse_arguments( &korok.attributes, &korok.fields, data, Some(discriminator), - ), + )?, discriminators, ..InstructionNode::default() } @@ -173,30 +173,29 @@ impl KorokVisitor for SetInstructionsVisitor { } } -fn parse_accounts(attributes: &Attributes, fields: &[FieldKorok]) -> Vec { +fn parse_accounts( + attributes: &Attributes, + fields: &[FieldKorok], +) -> CodamaResult> { // Gather the accounts from the struct attributes. let accounts_from_struct_attributes = attributes .iter() .filter_map(AccountDirective::filter) - .map(|attr| attr.account.clone()) - .collect::>(); + .map(|attr| attr.to_instruction_account_node()) + .collect::>>()?; // Gather the accounts from the fields. let accounts_from_fields = fields .iter() - .filter_map(|field| { - field - .attributes - .get_last(AccountDirective::filter) - .map(|attr| attr.account.clone()) - }) - .collect::>(); + .filter_map(|field| field.attributes.get_last(AccountDirective::filter)) + .map(|attr| attr.to_instruction_account_node()) + .collect::>>()?; // Concatenate the accounts. - accounts_from_struct_attributes + Ok(accounts_from_struct_attributes .into_iter() .chain(accounts_from_fields) - .collect::>() + .collect::>()) } fn parse_arguments( @@ -204,7 +203,7 @@ fn parse_arguments( fields: &[FieldKorok], data: StructTypeNode, discriminator: Option, -) -> Vec { +) -> CodamaResult> { // Here we must reconcile the struct fields combined in the `CombineTypesVisitor` // with their original `FieldKoroks` to check for `default_value` directives // that would have been ignored on fields but are relevant for instruction arguments. @@ -216,7 +215,7 @@ fn parse_arguments( .enumerate() .map(|(i, argument)| { if argument.default_value.is_some() { - return argument; + return Ok(argument); } let field = fields .iter() @@ -227,25 +226,32 @@ fn parse_arguments( _ => None, }); let Some(field) = field else { - return argument; + return Ok(argument); }; let Some(directive) = field.attributes.get_last(DefaultValueDirective::filter) else { - return argument; + return Ok(argument); }; - InstructionArgumentNode { - default_value: Some(directive.node.clone()), + Ok(InstructionArgumentNode { + default_value: Some(directive.node.try_resolved()?.clone()), default_value_strategy: directive.default_value_strategy, ..argument - } - }); + }) + }) + .collect::>>()?; let (before, after): (Vec<_>, Vec<_>) = attributes .get_all(ArgumentDirective::filter) .into_iter() .partition(|attr| !attr.after); - let before = before.into_iter().map(|attr| attr.argument.clone()); - let after = after.into_iter().map(|attr| attr.argument.clone()); + let before = before + .into_iter() + .map(|attr| attr.to_instruction_argument_node()) + .collect::>>()?; + let after = after + .into_iter() + .map(|attr| attr.to_instruction_argument_node()) + .collect::>>()?; let mut arguments: Vec = before .into_iter() @@ -257,7 +263,7 @@ fn parse_arguments( arguments.insert(0, discriminator); } - arguments + Ok(arguments) } fn parse_struct( diff --git a/codama-korok-visitors/src/set_pdas_visitor.rs b/codama-korok-visitors/src/set_pdas_visitor.rs index 4f762145..7e7541c5 100644 --- a/codama-korok-visitors/src/set_pdas_visitor.rs +++ b/codama-korok-visitors/src/set_pdas_visitor.rs @@ -5,8 +5,8 @@ use codama_attributes::{ use codama_errors::CodamaResult; use codama_koroks::FieldKorok; use codama_nodes::{ - CamelCaseString, Docs, Node, PdaNode, PdaSeedNode, RegisteredTypeNode, TypeNode, - VariablePdaSeedNode, + CamelCaseString, ConstantPdaSeedNode, Docs, Node, PdaNode, PdaSeedNode, RegisteredTypeNode, + TypeNode, VariablePdaSeedNode, }; #[derive(Default)] @@ -25,7 +25,7 @@ impl KorokVisitor for SetPdasVisitor { return Ok(()); }; - let pda = parse_pda_node(korok.name(), &korok.attributes, &korok.fields); + let pda = parse_pda_node(korok.name(), &korok.attributes, &korok.fields)?; korok.node = Some(ProgramDirective::apply(&korok.attributes, pda.into())); Ok(()) } @@ -36,7 +36,7 @@ impl KorokVisitor for SetPdasVisitor { return Ok(()); }; - let pda = parse_pda_node(korok.name(), &korok.attributes, &[]); + let pda = parse_pda_node(korok.name(), &korok.attributes, &[])?; korok.node = Some(ProgramDirective::apply(&korok.attributes, pda.into())); Ok(()) } @@ -46,38 +46,56 @@ pub fn parse_pda_node( name: CamelCaseString, attributes: &Attributes, fields: &[FieldKorok], -) -> PdaNode { - PdaNode { +) -> CodamaResult { + Ok(PdaNode { name, - seeds: parse_pda_seed_nodes(attributes, fields), + seeds: parse_pda_seed_nodes(attributes, fields)?, docs: Docs::default(), program_id: None, - } + }) } -pub fn parse_pda_seed_nodes(attributes: &Attributes, fields: &[FieldKorok]) -> Vec { - attributes - .iter() - .filter_map(SeedDirective::filter) - .filter_map(|directive| match &directive.seed { - SeedDirectiveType::Defined(node) => Some(node.clone()), - SeedDirectiveType::Linked(name) => fields.iter().find_map(|field| { - if field.ast.ident.as_ref().is_none_or(|ident| ident != name) { - return None; - } - let (name, type_node) = match &field.node { - Some(Node::Type(RegisteredTypeNode::StructField(struct_field))) => { - (struct_field.name.clone(), struct_field.r#type.clone()) +pub fn parse_pda_seed_nodes( + attributes: &Attributes, + fields: &[FieldKorok], +) -> CodamaResult> { + let mut seeds = Vec::new(); + for directive in attributes.iter().filter_map(SeedDirective::filter) { + match &directive.seed { + SeedDirectiveType::Variable { name, r#type } => { + let type_node = r#type.try_resolved()?.clone(); + seeds.push(VariablePdaSeedNode::new(name.as_str(), type_node).into()); + } + SeedDirectiveType::Constant { r#type, value } => { + let type_node = r#type.try_resolved()?.clone(); + let value_node = value.try_resolved()?.clone(); + seeds.push(ConstantPdaSeedNode::new(type_node, value_node).into()); + } + SeedDirectiveType::Linked(name) => { + // Linked seeds resolve by finding a matching field. + // If no field matches, the seed is silently skipped (pre-existing behavior). + let seed = fields.iter().find_map(|field| { + if field.ast.ident.as_ref().is_none_or(|ident| ident != name) { + return None; } - _ => match TypeNode::try_from(field.node.clone()) { - Ok(type_node) => (name.clone().into(), type_node), - Err(_) => return None, - }, - }; - Some(PdaSeedNode::Variable(VariablePdaSeedNode::new( - name, type_node, - ))) - }), - }) - .collect() + let (name, type_node) = match &field.node { + Some(Node::Type(RegisteredTypeNode::StructField(struct_field))) => { + (struct_field.name.clone(), struct_field.r#type.clone()) + } + _ => match TypeNode::try_from(field.node.clone()) { + Ok(type_node) => (name.clone().into(), type_node), + Err(_) => return None, + }, + }; + Some(PdaSeedNode::Variable(VariablePdaSeedNode::new( + name, type_node, + ))) + }); + if let Some(seed) = seed { + seeds.push(seed); + } + } + } + } + Ok(seeds) } diff --git a/codama-macros/tests/codama_attribute/account_directive/resolvable_default_value.pass.rs b/codama-macros/tests/codama_attribute/account_directive/resolvable_default_value.pass.rs new file mode 100644 index 00000000..3623847f --- /dev/null +++ b/codama-macros/tests/codama_attribute/account_directive/resolvable_default_value.pass.rs @@ -0,0 +1,7 @@ +use codama_macros::codama; + +// Resolvable directives nested inside an account directive should compile without error. +#[codama(account(name = "vault", writable, default_value = wellknown::ata(account("owner"))))] +pub struct Test; + +fn main() {} diff --git a/codama-macros/tests/codama_attribute/field_directive/resolvable_type.pass.rs b/codama-macros/tests/codama_attribute/field_directive/resolvable_type.pass.rs new file mode 100644 index 00000000..451825ab --- /dev/null +++ b/codama-macros/tests/codama_attribute/field_directive/resolvable_type.pass.rs @@ -0,0 +1,9 @@ +use codama_macros::codama; + +// Resolvable directives in field type and value positions should compile. +#[codama(field("age", foo::custom_type, default_value = bar::custom_value))] +pub struct Test { + name: String, +} + +fn main() {} diff --git a/codama-macros/tests/codama_attribute/seed_directive/resolvable_seed.pass.rs b/codama-macros/tests/codama_attribute/seed_directive/resolvable_seed.pass.rs new file mode 100644 index 00000000..51165fe4 --- /dev/null +++ b/codama-macros/tests/codama_attribute/seed_directive/resolvable_seed.pass.rs @@ -0,0 +1,7 @@ +use codama_macros::codama; + +// Resolvable directives in seed type and value positions should compile. +#[codama(seed(type = foo::custom_type, value = bar::custom_value))] +pub struct Test; + +fn main() {} diff --git a/codama-macros/tests/codama_attribute/type_directive/resolvable_type.pass.rs b/codama-macros/tests/codama_attribute/type_directive/resolvable_type.pass.rs new file mode 100644 index 00000000..506aa16a --- /dev/null +++ b/codama-macros/tests/codama_attribute/type_directive/resolvable_type.pass.rs @@ -0,0 +1,7 @@ +use codama_macros::codama; + +// Resolvable directives in type positions should compile without error. +#[codama(type = foo::custom_type)] +pub struct Test; + +fn main() {} diff --git a/codama-macros/tests/codama_attribute/values_nodes/resolvable_value.pass.rs b/codama-macros/tests/codama_attribute/values_nodes/resolvable_value.pass.rs new file mode 100644 index 00000000..6a7129ba --- /dev/null +++ b/codama-macros/tests/codama_attribute/values_nodes/resolvable_value.pass.rs @@ -0,0 +1,7 @@ +use codama_macros::codama; + +// Resolvable directives in value positions should compile without error. +#[codama(default_value = wellknown::ata(account("owner"), account("tokenProgram"), account("mint")))] +pub struct Test; + +fn main() {} diff --git a/codama-syn-helpers/src/meta.rs b/codama-syn-helpers/src/meta.rs index 463b323b..f3cef386 100644 --- a/codama-syn-helpers/src/meta.rs +++ b/codama-syn-helpers/src/meta.rs @@ -10,7 +10,7 @@ use syn::{ Expr, MacroDelimiter, MetaList, Path, Token, }; -#[derive(Debug, From)] +#[derive(Debug, Clone, From)] pub enum Meta { /// A path followed by an equal sign and a single Meta — e.g. `my_attribute = my_value`. PathValue(PathValue), @@ -24,14 +24,32 @@ pub enum Meta { Verbatim(TokenStream), } -#[derive(Debug)] +impl PartialEq for Meta { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Meta::PathValue(a), Meta::PathValue(b)) => a == b, + (Meta::PathList(a), Meta::PathList(b)) => a == b, + (Meta::Expr(a), Meta::Expr(b)) => a == b, + (Meta::Verbatim(a), Meta::Verbatim(b)) => a.to_string() == b.to_string(), + _ => false, + } + } +} + +#[derive(Debug, Clone)] pub struct PathValue { pub path: Path, pub eq_token: Token![=], pub value: Box, } -#[derive(Debug)] +impl PartialEq for PathValue { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && self.value == other.value + } +} + +#[derive(Debug, Clone)] pub struct PathList { pub path: Path, pub eq_token: Option, @@ -39,6 +57,14 @@ pub struct PathList { pub tokens: TokenStream, } +impl PartialEq for PathList { + fn eq(&self, other: &Self) -> bool { + self.path == other.path + && self.eq_token.is_some() == other.eq_token.is_some() + && self.tokens.to_string() == other.tokens.to_string() + } +} + impl Meta { pub fn path(&self) -> syn::Result<&Path> { match self {