Skip to main content

codama_attributes/codama_directives/
remaining_accounts_directive.rs

1use 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    /// Construct an `InstructionRemainingAccountsNode` from this directive.
55    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}