use crate::utils::FromMeta;
use codama_syn_helpers::Meta;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvableDirective {
pub namespace: String,
pub name: String,
pub meta: Meta,
}
impl fmt::Display for ResolvableDirective {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}::{}", self.namespace, self.name)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Resolvable<T> {
Resolved(T),
Unresolved(Box<ResolvableDirective>),
}
impl<T: FromMeta> Resolvable<T> {
pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
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<T> Resolvable<T> {
pub fn resolved(&self) -> Option<&T> {
match self {
Resolvable::Resolved(value) => Some(value),
Resolvable::Unresolved(_) => None,
}
}
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(),
})
}
}
}
pub fn try_into_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,
name: directive.name,
})
}
}
}
pub fn is_unresolved(&self) -> bool {
matches!(self, Resolvable::Unresolved(_))
}
pub fn is_resolved(&self) -> bool {
matches!(self, Resolvable::Resolved(_))
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Resolvable<U> {
match self {
Resolvable::Resolved(value) => Resolvable::Resolved(f(value)),
Resolvable::Unresolved(directive) => Resolvable::Unresolved(directive),
}
}
}
impl<T> From<T> for Resolvable<T> {
fn from(value: T) -> Self {
Resolvable::Resolved(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use codama_nodes::{InstructionInputValueNode, PublicKeyTypeNode, RegisteredTypeNode};
#[test]
fn from_meta_resolves_builtin_type() {
let meta: Meta = syn::parse_quote! { public_key };
let result = Resolvable::<RegisteredTypeNode>::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::<RegisteredTypeNode>::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::<RegisteredTypeNode>::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::<InstructionInputValueNode>::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::<InstructionInputValueNode>::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::<RegisteredTypeNode>::from_meta(&meta);
assert!(result.is_err());
}
#[test]
fn resolved_returns_some_for_resolved() {
let r: Resolvable<u32> = Resolvable::Resolved(42);
assert_eq!(r.resolved(), Some(&42));
}
#[test]
fn resolved_returns_none_for_unresolved() {
let r: Resolvable<u32> = 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<u32> = Resolvable::Resolved(42);
assert_eq!(r.try_resolved().unwrap(), &42);
}
#[test]
fn try_resolved_returns_err_for_unresolved() {
let r: Resolvable<u32> = 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<u32> = Resolvable::Resolved(42);
assert_eq!(r.try_into_resolved().unwrap(), 42);
}
#[test]
fn try_into_resolved_returns_err_for_unresolved() {
let r: Resolvable<u32> = 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<u32> = 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<u32> = Resolvable::Unresolved(Box::new(ResolvableDirective {
namespace: "foo".into(),
name: "bar".into(),
meta: syn::parse_quote! { foo::bar },
}));
let mapped: Resolvable<String> = r.map(|v| v.to_string());
assert!(mapped.is_unresolved());
}
#[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"),
}
}
#[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"),
}
}
}