codama_attributes/codama_directives/
account_directive.rs

1use crate::{
2    utils::{FromMeta, SetOnce},
3    Attribute, AttributeContext, CodamaAttribute, CodamaDirective,
4};
5use codama_errors::CodamaError;
6use codama_nodes::{
7    CamelCaseString, Docs, InstructionAccountNode, InstructionInputValueNode, IsAccountSigner,
8};
9use codama_syn_helpers::{extensions::*, Meta};
10
11#[derive(Debug, PartialEq)]
12pub struct AccountDirective {
13    pub account: InstructionAccountNode,
14}
15
16impl AccountDirective {
17    pub fn parse(meta: &Meta, ctx: &AttributeContext) -> syn::Result<Self> {
18        meta.assert_directive("account")?;
19        let mut name = SetOnce::<CamelCaseString>::new("name");
20        if let AttributeContext::Field(syn::Field {
21            ident: Some(ident), ..
22        }) = ctx
23        {
24            name = name.initial_value(ident.to_string().into())
25        }
26        let mut is_writable = SetOnce::<bool>::new("writable").initial_value(false);
27        let mut is_signer = SetOnce::<IsAccountSigner>::new("signer").initial_value(false.into());
28        let mut is_optional = SetOnce::<bool>::new("optional").initial_value(false);
29        let mut default_value = SetOnce::<InstructionInputValueNode>::new("default_value");
30        let mut docs = SetOnce::<Docs>::new("docs");
31        match meta.is_path_or_empty_list() {
32            true => (),
33            false => meta
34                .as_path_list()?
35                .each(|ref meta| match meta.path_str().as_str() {
36                    "name" => name.set(meta.as_value()?.as_expr()?.as_string()?.into(), meta),
37                    "writable" => is_writable.set(bool::from_meta(meta)?, meta),
38                    "signer" => is_signer.set(IsAccountSigner::from_meta(meta)?, meta),
39                    "optional" => is_optional.set(bool::from_meta(meta)?, meta),
40                    "default_value" => default_value.set(
41                        InstructionInputValueNode::from_meta(meta.as_value()?)?,
42                        meta,
43                    ),
44                    "docs" => docs.set(Docs::from_meta(meta)?, meta),
45                    _ => Err(meta.error("unrecognized attribute")),
46                })?,
47        }
48        Ok(AccountDirective {
49            account: InstructionAccountNode {
50                name: name.take(meta)?,
51                is_writable: is_writable.take(meta)?,
52                is_signer: is_signer.take(meta)?,
53                is_optional: is_optional.take(meta)?,
54                docs: docs.option().unwrap_or_default(),
55                default_value: default_value.option(),
56            },
57        })
58    }
59}
60
61impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a AccountDirective {
62    type Error = CodamaError;
63
64    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
65        match attribute.directive.as_ref() {
66            CodamaDirective::Account(ref a) => Ok(a),
67            _ => Err(CodamaError::InvalidCodamaDirective {
68                expected: "account".to_string(),
69                actual: attribute.directive.name().to_string(),
70            }),
71        }
72    }
73}
74
75impl<'a> TryFrom<&'a Attribute<'a>> for &'a AccountDirective {
76    type Error = CodamaError;
77
78    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
79        <&CodamaAttribute>::try_from(attribute)?.try_into()
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use codama_nodes::PayerValueNode;
87
88    #[test]
89    fn fully_set() {
90        let meta: Meta = syn::parse_quote! { account(name = "payer", writable, signer, optional, default_value = payer) };
91        let item = syn::parse_quote! { struct Foo; };
92        let ctx = AttributeContext::Item(&item);
93        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
94        assert_eq!(
95            directive,
96            AccountDirective {
97                account: InstructionAccountNode {
98                    name: "payer".into(),
99                    is_writable: true,
100                    is_signer: IsAccountSigner::True,
101                    is_optional: true,
102                    default_value: Some(PayerValueNode::new().into()),
103                    docs: Docs::default(),
104                },
105            }
106        );
107    }
108
109    #[test]
110    fn fully_set_with_explicit_values() {
111        let meta: Meta = syn::parse_quote! { account(
112            name = "payer",
113            writable = true,
114            signer = "either",
115            optional = false,
116            default_value = payer
117        ) };
118        let item = syn::parse_quote! { struct Foo; };
119        let ctx = AttributeContext::Item(&item);
120        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
121        assert_eq!(
122            directive,
123            AccountDirective {
124                account: InstructionAccountNode {
125                    name: "payer".into(),
126                    is_writable: true,
127                    is_signer: IsAccountSigner::Either,
128                    is_optional: false,
129                    default_value: Some(PayerValueNode::new().into()),
130                    docs: Docs::default(),
131                },
132            }
133        );
134    }
135
136    #[test]
137    fn empty_on_named_field() {
138        let meta: Meta = syn::parse_quote! { account };
139        let field = syn::parse_quote! { authority: AccountMeta };
140        let ctx = AttributeContext::Field(&field);
141        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
142        assert_eq!(
143            directive,
144            AccountDirective {
145                account: InstructionAccountNode {
146                    name: "authority".into(),
147                    is_writable: false,
148                    is_signer: IsAccountSigner::False,
149                    is_optional: false,
150                    default_value: None,
151                    docs: Docs::default(),
152                },
153            }
154        );
155    }
156
157    #[test]
158    fn empty_on_struct() {
159        let meta: Meta = syn::parse_quote! { account };
160        let item = syn::parse_quote! { struct Foo; };
161        let ctx = AttributeContext::Item(&item);
162        let error = AccountDirective::parse(&meta, &ctx).unwrap_err();
163        assert_eq!(error.to_string(), "name is missing");
164    }
165
166    #[test]
167    fn with_docs() {
168        let meta: Meta = syn::parse_quote! { account(name = "stake", writable, docs = "what this account is for") };
169        let item = syn::parse_quote! { struct Foo; };
170        let ctx = AttributeContext::Item(&item);
171        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
172        assert_eq!(
173            directive,
174            AccountDirective {
175                account: InstructionAccountNode {
176                    name: "stake".into(),
177                    is_writable: true,
178                    is_signer: IsAccountSigner::False,
179                    is_optional: false,
180                    default_value: None,
181                    docs: vec!["what this account is for".to_string()].into(),
182                },
183            }
184        );
185    }
186
187    #[test]
188    fn with_docs_array() {
189        let meta: Meta = syn::parse_quote! { account(name = "authority", signer, docs = ["Line 1", "Line 2", "Line 3"]) };
190        let item = syn::parse_quote! { struct Foo; };
191        let ctx = AttributeContext::Item(&item);
192        let directive = AccountDirective::parse(&meta, &ctx).unwrap();
193        assert_eq!(
194            directive,
195            AccountDirective {
196                account: InstructionAccountNode {
197                    name: "authority".into(),
198                    is_writable: false,
199                    is_signer: IsAccountSigner::True,
200                    is_optional: false,
201                    default_value: None,
202                    docs: vec![
203                        "Line 1".to_string(),
204                        "Line 2".to_string(),
205                        "Line 3".to_string()
206                    ]
207                    .into(),
208                },
209            }
210        );
211    }
212}