codama_attributes/codama_directives/
resolvable_directive.rs1use crate::utils::FromMeta;
2use codama_syn_helpers::Meta;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq)]
9pub struct ResolvableDirective {
10 pub namespace: String,
12 pub name: String,
14 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#[derive(Debug, Clone, PartialEq)]
27pub enum Resolvable<T> {
28 Resolved(T),
30 Unresolved(Box<ResolvableDirective>),
32}
33
34impl<T: FromMeta> Resolvable<T> {
35 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 pub fn resolved(&self) -> Option<&T> {
57 match self {
58 Resolvable::Resolved(value) => Some(value),
59 Resolvable::Unresolved(_) => None,
60 }
61 }
62
63 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 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 pub fn is_unresolved(&self) -> bool {
91 matches!(self, Resolvable::Unresolved(_))
92 }
93
94 pub fn is_resolved(&self) -> bool {
96 matches!(self, Resolvable::Resolved(_))
97 }
98
99 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 #[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 #[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 #[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 #[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}