Skip to main content

codama_attributes/codama_directives/
account_directive.rs

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