codama_attributes/codama_directives/
account_directive.rs1use 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 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}