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, 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 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}