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