Skip to main content

typhoon_syn/
account.rs

1use {
2    crate::{constraints::Constraints, Docs},
3    proc_macro2::Span,
4    syn::{
5        parse_quote,
6        visit::{visit_path_segment, Visit},
7        Field, Ident, PathSegment,
8    },
9};
10
11#[derive(Default, Clone)]
12pub struct AccountMeta {
13    pub is_signer: bool,
14    pub is_mutable: bool,
15    pub is_optional: bool,
16}
17
18#[derive(Clone)]
19pub struct InstructionAccount {
20    pub name: Ident,
21    pub constraints: Constraints,
22    pub meta: AccountMeta,
23    pub docs: Vec<String>,
24    ty: Option<PathSegment>,
25    pub inner_ty: Ident,
26}
27
28impl InstructionAccount {
29    pub fn get_ty(&self) -> PathSegment {
30        let inner_ty = &self.inner_ty;
31        self.ty.clone().unwrap_or(parse_quote!(#inner_ty<'info>))
32    }
33}
34
35impl TryFrom<&Field> for InstructionAccount {
36    type Error = syn::Error;
37
38    fn try_from(value: &Field) -> Result<Self, Self::Error> {
39        let mut acc = InstructionAccount {
40            name: value
41                .ident
42                .clone()
43                .ok_or(syn::Error::new_spanned(value, "The field need to be named"))?,
44            docs: Docs::from(value.attrs.as_slice()).into_vec(),
45            constraints: Constraints::try_from(value.attrs.as_slice())?,
46            meta: AccountMeta::default(),
47            ty: None,
48            inner_ty: Ident::new("UncheckedAccount", Span::call_site()),
49        };
50
51        acc.visit_type(&value.ty);
52        Ok(acc)
53    }
54}
55
56impl Visit<'_> for InstructionAccount {
57    fn visit_path_segment(&mut self, i: &syn::PathSegment) {
58        let ty = i.ident.to_string();
59
60        if ty == "Option" {
61            self.meta.is_optional = true;
62            visit_path_segment(self, i);
63        } else if ty.starts_with("Mut") {
64            self.meta.is_mutable = true;
65            if self.ty.is_none() {
66                self.ty = Some(i.clone());
67            }
68            visit_path_segment(self, i);
69        } else if ty.starts_with("Signer") {
70            self.meta.is_signer = true;
71            if self.ty.is_none() {
72                self.ty = Some(i.clone());
73            }
74            visit_path_segment(self, i);
75        } else {
76            if self.ty.is_none() {
77                self.ty = Some(i.clone());
78            }
79            self.inner_ty = i.ident.clone();
80            visit_path_segment(self, i);
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use {super::*, syn::parse_quote};
88
89    #[test]
90    fn test_instruction_parse_with_context() {
91        let field: syn::Field = parse_quote!(pub random2: Option<Mut<Account<Random2>>>);
92        let account = InstructionAccount::try_from(&field).unwrap();
93        assert_eq!(account.inner_ty, "Random2");
94        assert!(account.meta.is_mutable);
95        assert!(account.meta.is_optional);
96        assert!(!account.meta.is_signer);
97
98        let field: syn::Field = parse_quote!(pub random2: SignerNoCheck<'info, Account<Random2>>);
99        let account = InstructionAccount::try_from(&field).unwrap();
100        assert_eq!(account.inner_ty, "Random2");
101        assert!(!account.meta.is_mutable);
102        assert!(!account.meta.is_optional);
103        assert!(account.meta.is_signer);
104
105        let field: syn::Field = parse_quote!(pub random2: InterfaceAccount<TokenAccount>);
106        let account = InstructionAccount::try_from(&field).unwrap();
107        assert_eq!(account.inner_ty, "TokenAccount");
108        assert!(!account.meta.is_mutable);
109        assert!(!account.meta.is_optional);
110        assert!(!account.meta.is_signer);
111
112        let field: syn::Field = parse_quote!(pub random2: UncheckedAccount);
113        let account = InstructionAccount::try_from(&field).unwrap();
114        assert_eq!(account.inner_ty, "UncheckedAccount");
115        assert!(!account.meta.is_mutable);
116        assert!(!account.meta.is_optional);
117        assert!(!account.meta.is_signer);
118    }
119}