codama_attributes/codama_directives/
remaining_accounts_directive.rs1use crate::{
2 utils::{FromMeta, SetOnce},
3 Attribute, Attributes, CodamaAttribute, CodamaDirective, TryFromFilter,
4};
5use codama_errors::CodamaError;
6use codama_nodes::{
7 ArgumentValueNode, Docs, InstructionAccountDisplayNode, InstructionRemainingAccountsNode,
8 InstructionRemainingAccountsValue, IsSigner,
9};
10use codama_syn_helpers::{extensions::*, Meta};
11
12#[derive(Debug, PartialEq)]
13pub struct RemainingAccountsDirective {
14 pub value: InstructionRemainingAccountsValue,
15 pub is_signer: Option<IsSigner>,
16 pub is_writable: Option<bool>,
17 pub is_optional: Option<bool>,
18 pub docs: Docs,
19 pub display: Option<InstructionAccountDisplayNode>,
20}
21
22impl RemainingAccountsDirective {
23 pub fn parse(meta: &Meta) -> syn::Result<Self> {
24 let pl = meta
25 .assert_directive("remaining_accounts")?
26 .as_path_list()?;
27 let mut value = SetOnce::<InstructionRemainingAccountsValue>::new("value");
28 let mut is_signer = SetOnce::<IsSigner>::new("signer");
29 let mut is_writable = SetOnce::<bool>::new("writable");
30 let mut is_optional = SetOnce::<bool>::new("optional");
31 let mut docs = SetOnce::<Docs>::new("docs");
32 let mut display = SetOnce::<InstructionAccountDisplayNode>::new("display");
33 pl.each(|ref meta| match meta.path_str().as_str() {
34 "argument" => value.set(ArgumentValueNode::from_meta(meta)?.into(), meta),
35 "signer" => is_signer.set(IsSigner::from_meta(meta)?, meta),
36 "writable" => is_writable.set(bool::from_meta(meta)?, meta),
37 "optional" => is_optional.set(bool::from_meta(meta)?, meta),
38 "docs" => docs.set(Docs::from_meta(meta)?, meta),
39 "display" => display.set(InstructionAccountDisplayNode::from_meta(meta)?, meta),
40 _ => Err(meta.error("unrecognized attribute")),
41 })?;
42 Ok(RemainingAccountsDirective {
43 value: value
44 .option()
45 .ok_or_else(|| meta.error("remaining_accounts must specify one of: argument"))?,
46 is_signer: is_signer.option(),
47 is_writable: is_writable.option(),
48 is_optional: is_optional.option(),
49 docs: docs.option().unwrap_or_default(),
50 display: display.option(),
51 })
52 }
53
54 pub fn to_instruction_remaining_accounts_node(&self) -> InstructionRemainingAccountsNode {
56 InstructionRemainingAccountsNode {
57 is_optional: self.is_optional,
58 is_signer: self.is_signer,
59 is_writable: self.is_writable,
60 docs: self.docs.clone(),
61 value: Box::new(self.value.clone()),
62 display: self.display.clone(),
63 }
64 }
65}
66
67impl RemainingAccountsDirective {
68 pub fn nodes(attributes: &Attributes) -> Vec<InstructionRemainingAccountsNode> {
69 attributes
70 .iter()
71 .filter_map(RemainingAccountsDirective::filter)
72 .map(RemainingAccountsDirective::to_instruction_remaining_accounts_node)
73 .collect()
74 }
75}
76
77impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a RemainingAccountsDirective {
78 type Error = CodamaError;
79
80 fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
81 match attribute.directive.as_ref() {
82 CodamaDirective::RemainingAccounts(ref a) => Ok(a),
83 _ => Err(CodamaError::InvalidCodamaDirective {
84 expected: "remaining_accounts".to_string(),
85 actual: attribute.directive.name().to_string(),
86 }),
87 }
88 }
89}
90
91impl<'a> TryFrom<&'a Attribute<'a>> for &'a RemainingAccountsDirective {
92 type Error = CodamaError;
93
94 fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
95 <&CodamaAttribute>::try_from(attribute)?.try_into()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use codama_nodes::DisplaySkip;
103
104 #[test]
105 fn argument_value_only() {
106 let meta: Meta = syn::parse_quote! { remaining_accounts(argument("signers")) };
107 let directive = RemainingAccountsDirective::parse(&meta).unwrap();
108 assert_eq!(
109 directive,
110 RemainingAccountsDirective {
111 value: ArgumentValueNode::new("signers").into(),
112 is_signer: None,
113 is_writable: None,
114 is_optional: None,
115 docs: Docs::default(),
116 display: None,
117 }
118 );
119 }
120
121 #[test]
122 fn fully_set() {
123 let meta: Meta = syn::parse_quote! { remaining_accounts(
124 argument("signers"),
125 signer,
126 writable,
127 optional,
128 docs = "Additional multisig signers."
129 ) };
130 let directive = RemainingAccountsDirective::parse(&meta).unwrap();
131 assert_eq!(
132 directive,
133 RemainingAccountsDirective {
134 value: ArgumentValueNode::new("signers").into(),
135 is_signer: Some(IsSigner::True),
136 is_writable: Some(true),
137 is_optional: Some(true),
138 docs: vec!["Additional multisig signers.".to_string()].into(),
139 display: None,
140 }
141 );
142 }
143
144 #[test]
145 fn fully_set_with_explicit_values() {
146 let meta: Meta = syn::parse_quote! { remaining_accounts(
147 argument(name = "signers"),
148 signer = "either",
149 writable = false,
150 optional = true,
151 docs = ["Line 1", "Line 2"]
152 ) };
153 let directive = RemainingAccountsDirective::parse(&meta).unwrap();
154 assert_eq!(
155 directive,
156 RemainingAccountsDirective {
157 value: ArgumentValueNode::new("signers").into(),
158 is_signer: Some(IsSigner::Either),
159 is_writable: Some(false),
160 is_optional: Some(true),
161 docs: vec!["Line 1".to_string(), "Line 2".to_string()].into(),
162 display: None,
163 }
164 );
165 }
166
167 #[test]
168 fn with_display() {
169 let meta: Meta = syn::parse_quote! { remaining_accounts(
170 argument("signers"),
171 display(label = "Signers", skip = never)
172 ) };
173 let directive = RemainingAccountsDirective::parse(&meta).unwrap();
174 let expected_display = InstructionAccountDisplayNode {
175 label: Some("Signers".to_string()),
176 skip: Some(DisplaySkip::Never),
177 };
178 assert_eq!(directive.display, Some(expected_display.clone()));
179 assert_eq!(
180 directive.to_instruction_remaining_accounts_node().display,
181 Some(expected_display)
182 );
183 }
184
185 #[test]
186 fn missing_value() {
187 let meta: Meta = syn::parse_quote! { remaining_accounts(signer, optional) };
188 let error = RemainingAccountsDirective::parse(&meta).unwrap_err();
189 assert_eq!(
190 error.to_string(),
191 "remaining_accounts must specify one of: argument"
192 );
193 }
194
195 #[test]
196 fn duplicated_value() {
197 let meta: Meta = syn::parse_quote! { remaining_accounts(argument("a"), argument("b")) };
198 let error = RemainingAccountsDirective::parse(&meta).unwrap_err();
199 assert_eq!(error.to_string(), "value is already set");
200 }
201
202 #[test]
203 fn invalid_signer() {
204 let meta: Meta =
205 syn::parse_quote! { remaining_accounts(argument("signers"), signer = "maybe") };
206 let error = RemainingAccountsDirective::parse(&meta).unwrap_err();
207 assert_eq!(error.to_string(), "expected boolean or `\"either\"`");
208 }
209
210 #[test]
211 fn unrecognized_attribute() {
212 let meta: Meta = syn::parse_quote! { remaining_accounts(argument("signers"), banana) };
213 let error = RemainingAccountsDirective::parse(&meta).unwrap_err();
214 assert_eq!(error.to_string(), "unrecognized attribute");
215 }
216
217 #[test]
218 fn to_node() {
219 let meta: Meta = syn::parse_quote! { remaining_accounts(
220 argument("signers"),
221 signer = "either",
222 optional,
223 docs = "Additional multisig signers."
224 ) };
225 let directive = RemainingAccountsDirective::parse(&meta).unwrap();
226 assert_eq!(
227 directive.to_instruction_remaining_accounts_node(),
228 InstructionRemainingAccountsNode {
229 is_optional: Some(true),
230 is_signer: Some(IsSigner::Either),
231 is_writable: None,
232 docs: vec!["Additional multisig signers.".to_string()].into(),
233 value: Box::new(ArgumentValueNode::new("signers").into()),
234 display: None,
235 }
236 );
237 }
238}