Skip to main content

codama_attributes/codama_directives/
resolvable_directive.rs

1use crate::utils::FromMeta;
2use codama_syn_helpers::Meta;
3use std::fmt;
4
5/// A directive that needs to be resolved by an external extension.
6/// Resolvable directives are detected by the `prefix::name(...)` syntax where
7/// the path contains exactly two segments separated by `::`.
8#[derive(Debug, Clone, PartialEq)]
9pub struct ResolvableDirective {
10    /// The namespace prefix, e.g. `"wellknown"` in `wellknown::ata(...)`.
11    pub namespace: String,
12    /// The directive name, e.g. `"ata"` in `wellknown::ata(...)`.
13    pub name: String,
14    /// The full Meta content for the extension to parse during resolution.
15    pub meta: Meta,
16}
17
18impl fmt::Display for ResolvableDirective {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "{}::{}", self.namespace, self.name)
21    }
22}
23
24/// A value that is either resolved to a concrete node `T` or deferred
25/// to an extension for resolution.
26#[derive(Debug, Clone, PartialEq)]
27pub enum Resolvable<T> {
28    /// The value has been resolved to a concrete node.
29    Resolved(T),
30    /// The value needs to be resolved by an extension.
31    Unresolved(Box<ResolvableDirective>),
32}
33
34impl<T: FromMeta> Resolvable<T> {
35    /// Parse a `Meta` into a `Resolvable<T>`. If the path has exactly two
36    /// segments (i.e. `prefix::name`), returns `Unresolved(ResolvableDirective)`.
37    /// Otherwise, delegates to `T::from_meta`.
38    pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
39        if let Ok(path) = meta.path() {
40            if path.segments.len() == 2 {
41                let namespace = path.segments[0].ident.to_string();
42                let name = path.segments[1].ident.to_string();
43                return Ok(Resolvable::Unresolved(Box::new(ResolvableDirective {
44                    namespace,
45                    name,
46                    meta: meta.clone(),
47                })));
48            }
49        }
50        T::from_meta(meta).map(Resolvable::Resolved)
51    }
52}
53
54impl<T> Resolvable<T> {
55    /// Returns a reference to the resolved value, or `None` if unresolved.
56    pub fn resolved(&self) -> Option<&T> {
57        match self {
58            Resolvable::Resolved(value) => Some(value),
59            Resolvable::Unresolved(_) => None,
60        }
61    }
62
63    /// Returns the resolved value, or an error if unresolved.
64    pub fn try_resolved(&self) -> Result<&T, codama_errors::CodamaError> {
65        match self {
66            Resolvable::Resolved(value) => Ok(value),
67            Resolvable::Unresolved(directive) => {
68                Err(codama_errors::CodamaError::UnresolvedDirective {
69                    namespace: directive.namespace.clone(),
70                    name: directive.name.clone(),
71                })
72            }
73        }
74    }
75
76    /// Consumes self and returns the resolved value, or an error if unresolved.
77    pub fn try_into_resolved(self) -> Result<T, codama_errors::CodamaError> {
78        match self {
79            Resolvable::Resolved(value) => Ok(value),
80            Resolvable::Unresolved(directive) => {
81                Err(codama_errors::CodamaError::UnresolvedDirective {
82                    namespace: directive.namespace,
83                    name: directive.name,
84                })
85            }
86        }
87    }
88
89    /// Returns `true` if this is an unresolved directive.
90    pub fn is_unresolved(&self) -> bool {
91        matches!(self, Resolvable::Unresolved(_))
92    }
93
94    /// Returns `true` if this is a resolved value.
95    pub fn is_resolved(&self) -> bool {
96        matches!(self, Resolvable::Resolved(_))
97    }
98
99    /// Maps the resolved value using the given function.
100    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Resolvable<U> {
101        match self {
102            Resolvable::Resolved(value) => Resolvable::Resolved(f(value)),
103            Resolvable::Unresolved(directive) => Resolvable::Unresolved(directive),
104        }
105    }
106}
107
108impl<T> From<T> for Resolvable<T> {
109    fn from(value: T) -> Self {
110        Resolvable::Resolved(value)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use codama_nodes::{InstructionInputValueNode, PublicKeyTypeNode, RegisteredTypeNode};
118
119    // -- Resolvable::from_meta --
120
121    #[test]
122    fn from_meta_resolves_builtin_type() {
123        let meta: Meta = syn::parse_quote! { public_key };
124        let result = Resolvable::<RegisteredTypeNode>::from_meta(&meta).unwrap();
125        assert!(result.is_resolved());
126        assert_eq!(
127            result,
128            Resolvable::Resolved(PublicKeyTypeNode::new().into())
129        );
130    }
131
132    #[test]
133    fn from_meta_detects_resolvable_type() {
134        let meta: Meta = syn::parse_quote! { foo::custom_type };
135        let result = Resolvable::<RegisteredTypeNode>::from_meta(&meta).unwrap();
136        assert!(result.is_unresolved());
137        let Resolvable::Unresolved(ref directive) = result else {
138            panic!("expected unresolved");
139        };
140        assert_eq!(directive.namespace, "foo");
141        assert_eq!(directive.name, "custom_type");
142    }
143
144    #[test]
145    fn from_meta_detects_resolvable_type_with_args() {
146        let meta: Meta = syn::parse_quote! { foo::custom_type(42) };
147        let result = Resolvable::<RegisteredTypeNode>::from_meta(&meta).unwrap();
148        assert!(result.is_unresolved());
149        let Resolvable::Unresolved(ref directive) = result else {
150            panic!("expected unresolved");
151        };
152        assert_eq!(directive.namespace, "foo");
153        assert_eq!(directive.name, "custom_type");
154    }
155
156    #[test]
157    fn from_meta_resolves_builtin_value() {
158        let meta: Meta = syn::parse_quote! { payer };
159        let result = Resolvable::<InstructionInputValueNode>::from_meta(&meta).unwrap();
160        assert!(result.is_resolved());
161    }
162
163    #[test]
164    fn from_meta_detects_resolvable_value() {
165        let meta: Meta = syn::parse_quote! { wellknown::ata(account("owner"), account("tokenProgram"), account("mint")) };
166        let result = Resolvable::<InstructionInputValueNode>::from_meta(&meta).unwrap();
167        assert!(result.is_unresolved());
168        let Resolvable::Unresolved(ref directive) = result else {
169            panic!("expected unresolved");
170        };
171        assert_eq!(directive.namespace, "wellknown");
172        assert_eq!(directive.name, "ata");
173    }
174
175    #[test]
176    fn from_meta_errors_on_unrecognized_builtin() {
177        let meta: Meta = syn::parse_quote! { banana };
178        let result = Resolvable::<RegisteredTypeNode>::from_meta(&meta);
179        assert!(result.is_err());
180    }
181
182    // -- Resolvable helpers --
183
184    #[test]
185    fn resolved_returns_some_for_resolved() {
186        let r: Resolvable<u32> = Resolvable::Resolved(42);
187        assert_eq!(r.resolved(), Some(&42));
188    }
189
190    #[test]
191    fn resolved_returns_none_for_unresolved() {
192        let r: Resolvable<u32> = Resolvable::Unresolved(Box::new(ResolvableDirective {
193            namespace: "foo".into(),
194            name: "bar".into(),
195            meta: syn::parse_quote! { foo::bar },
196        }));
197        assert_eq!(r.resolved(), None);
198    }
199
200    #[test]
201    fn try_resolved_returns_ok_for_resolved() {
202        let r: Resolvable<u32> = Resolvable::Resolved(42);
203        assert_eq!(r.try_resolved().unwrap(), &42);
204    }
205
206    #[test]
207    fn try_resolved_returns_err_for_unresolved() {
208        let r: Resolvable<u32> = Resolvable::Unresolved(Box::new(ResolvableDirective {
209            namespace: "foo".into(),
210            name: "bar".into(),
211            meta: syn::parse_quote! { foo::bar },
212        }));
213        let err = r.try_resolved().unwrap_err();
214        assert!(matches!(
215            err,
216            codama_errors::CodamaError::UnresolvedDirective { .. }
217        ));
218    }
219
220    #[test]
221    fn try_into_resolved_returns_value_for_resolved() {
222        let r: Resolvable<u32> = Resolvable::Resolved(42);
223        assert_eq!(r.try_into_resolved().unwrap(), 42);
224    }
225
226    #[test]
227    fn try_into_resolved_returns_err_for_unresolved() {
228        let r: Resolvable<u32> = Resolvable::Unresolved(Box::new(ResolvableDirective {
229            namespace: "foo".into(),
230            name: "bar".into(),
231            meta: syn::parse_quote! { foo::bar },
232        }));
233        let err = r.try_into_resolved().unwrap_err();
234        assert!(matches!(
235            err,
236            codama_errors::CodamaError::UnresolvedDirective { .. }
237        ));
238    }
239
240    #[test]
241    fn map_transforms_resolved() {
242        let r: Resolvable<u32> = Resolvable::Resolved(42);
243        let mapped = r.map(|v| v.to_string());
244        assert_eq!(mapped, Resolvable::Resolved("42".to_string()));
245    }
246
247    #[test]
248    fn map_preserves_unresolved() {
249        let r: Resolvable<u32> = Resolvable::Unresolved(Box::new(ResolvableDirective {
250            namespace: "foo".into(),
251            name: "bar".into(),
252            meta: syn::parse_quote! { foo::bar },
253        }));
254        let mapped: Resolvable<String> = r.map(|v| v.to_string());
255        assert!(mapped.is_unresolved());
256    }
257
258    // -- Directive-level integration --
259
260    #[test]
261    fn type_directive_with_resolvable() {
262        let meta: Meta = syn::parse_quote! { type = foo::custom_type };
263        let directive = crate::TypeDirective::parse(&meta).unwrap();
264        assert!(directive.node.is_unresolved());
265    }
266
267    #[test]
268    fn default_value_directive_with_resolvable() {
269        let meta: Meta = syn::parse_quote! { default_value = bar::my_value(1, 2, 3) };
270        let directive = crate::DefaultValueDirective::parse(&meta).unwrap();
271        assert!(directive.node.is_unresolved());
272        let Resolvable::Unresolved(ref d) = directive.node else {
273            panic!("expected unresolved");
274        };
275        assert_eq!(d.namespace, "bar");
276        assert_eq!(d.name, "my_value");
277    }
278
279    #[test]
280    fn account_directive_with_resolvable_default_value() {
281        let meta: Meta = syn::parse_quote! { account(name = "vault", writable, default_value = wellknown::ata(account("owner"))) };
282        let item = syn::parse_quote! { struct Foo; };
283        let ctx = crate::AttributeContext::Item(&item);
284        let directive = crate::AccountDirective::parse(&meta, &ctx).unwrap();
285        assert_eq!(directive.name, codama_nodes::CamelCaseString::new("vault"));
286        assert!(directive.is_writable);
287        assert!(directive.default_value.as_ref().unwrap().is_unresolved());
288    }
289
290    #[test]
291    fn seed_directive_with_resolvable_type() {
292        let meta: Meta = syn::parse_quote! { seed(name = "authority", type = foo::custom_pubkey) };
293        let item = syn::parse_quote! { struct Foo; };
294        let ctx = crate::AttributeContext::Item(&item);
295        let directive = crate::SeedDirective::parse(&meta, &ctx).unwrap();
296        match &directive.seed {
297            crate::SeedDirectiveType::Variable { name, r#type } => {
298                assert_eq!(name, "authority");
299                assert!(r#type.is_unresolved());
300            }
301            _ => panic!("expected Variable seed"),
302        }
303    }
304
305    // -- Nested resolvable directives --
306
307    #[test]
308    fn field_directive_with_resolvable_type_and_value() {
309        let meta: Meta =
310            syn::parse_quote! { field("age", foo::custom_type, default_value = bar::custom_value) };
311        let directive = crate::FieldDirective::parse(&meta).unwrap();
312        assert!(directive.r#type.is_unresolved());
313        assert!(directive.default_value.as_ref().unwrap().is_unresolved());
314        let Resolvable::Unresolved(ref t) = directive.r#type else {
315            panic!("expected unresolved type");
316        };
317        assert_eq!(t.namespace, "foo");
318        assert_eq!(t.name, "custom_type");
319        let Resolvable::Unresolved(ref v) = directive.default_value.as_ref().unwrap() else {
320            panic!("expected unresolved value");
321        };
322        assert_eq!(v.namespace, "bar");
323        assert_eq!(v.name, "custom_value");
324    }
325
326    #[test]
327    fn argument_directive_with_resolvable_type() {
328        let meta: Meta = syn::parse_quote! { argument("age", foo::number_type) };
329        let directive = crate::ArgumentDirective::parse(&meta).unwrap();
330        assert!(directive.r#type.is_unresolved());
331        let Resolvable::Unresolved(ref t) = directive.r#type else {
332            panic!("expected unresolved type");
333        };
334        assert_eq!(t.namespace, "foo");
335        assert_eq!(t.name, "number_type");
336    }
337
338    #[test]
339    fn seed_directive_with_resolvable_type_and_value() {
340        let meta: Meta =
341            syn::parse_quote! { seed(type = foo::custom_type, value = bar::custom_value) };
342        let item = syn::parse_quote! { struct Foo; };
343        let ctx = crate::AttributeContext::Item(&item);
344        let directive = crate::SeedDirective::parse(&meta, &ctx).unwrap();
345        match &directive.seed {
346            crate::SeedDirectiveType::Constant { r#type, value } => {
347                assert!(r#type.is_unresolved());
348                assert!(value.is_unresolved());
349            }
350            _ => panic!("expected Constant seed"),
351        }
352    }
353}