Skip to main content

codama_attributes/codama_directives/
argument_directive.rs

1use crate::{
2    codama_directives::type_nodes::StructFieldMetaConsumer,
3    utils::{FromMeta, MetaConsumer},
4    Attribute, CodamaAttribute, CodamaDirective, Resolvable,
5};
6use codama_errors::{CodamaError, CodamaResult};
7use codama_nodes::{
8    CamelCaseString, DefaultValueStrategy, Docs, InstructionArgumentNode,
9    InstructionInputValueNode, TypeNode,
10};
11use codama_syn_helpers::Meta;
12
13#[derive(Debug, PartialEq)]
14pub struct ArgumentDirective {
15    pub after: bool,
16    pub name: CamelCaseString,
17    pub r#type: Resolvable<TypeNode>,
18    pub docs: Docs,
19    pub default_value: Option<Resolvable<InstructionInputValueNode>>,
20    pub default_value_strategy: Option<DefaultValueStrategy>,
21}
22
23impl ArgumentDirective {
24    pub fn parse(meta: &Meta) -> syn::Result<Self> {
25        meta.assert_directive("argument")?;
26        let consumer = StructFieldMetaConsumer::from_meta(meta)?
27            .consume_field()?
28            .consume_argument_default_value()?
29            .consume_after()?
30            .assert_fully_consumed()?;
31
32        let default_value = consumer.default_instruction_input_value_node();
33        let default_value_strategy = consumer.default_value_strategy();
34
35        Ok(ArgumentDirective {
36            after: consumer.after.option().unwrap_or(false),
37            name: consumer.name.take(meta)?,
38            r#type: consumer.r#type.take(meta)?,
39            docs: consumer.docs.option().unwrap_or_default(),
40            default_value,
41            default_value_strategy,
42        })
43    }
44
45    /// Construct an `InstructionArgumentNode` from this directive.
46    /// Returns an error if any unresolved directives remain.
47    pub fn to_instruction_argument_node(&self) -> CodamaResult<InstructionArgumentNode> {
48        Ok(InstructionArgumentNode {
49            name: self.name.clone(),
50            r#type: Box::new(self.r#type.try_resolved()?.clone()),
51            docs: self.docs.clone(),
52            default_value: Box::new(
53                self.default_value
54                    .as_ref()
55                    .map(|r| r.try_resolved().cloned())
56                    .transpose()?,
57            ),
58            default_value_strategy: self.default_value_strategy,
59        })
60    }
61}
62
63impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a ArgumentDirective {
64    type Error = CodamaError;
65
66    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
67        match attribute.directive.as_ref() {
68            CodamaDirective::Argument(ref a) => Ok(a),
69            _ => Err(CodamaError::InvalidCodamaDirective {
70                expected: "argument".to_string(),
71                actual: attribute.directive.name().to_string(),
72            }),
73        }
74    }
75}
76
77impl<'a> TryFrom<&'a Attribute<'a>> for &'a ArgumentDirective {
78    type Error = CodamaError;
79
80    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
81        <&CodamaAttribute>::try_from(attribute)?.try_into()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use codama_nodes::{NumberFormat::U8, NumberTypeNode, PayerValueNode};
89
90    #[test]
91    fn ok() {
92        let meta: Meta = syn::parse_quote! { argument("age", number(u8)) };
93        let directive = ArgumentDirective::parse(&meta).unwrap();
94        assert_eq!(
95            directive,
96            ArgumentDirective {
97                after: false,
98                name: "age".into(),
99                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
100                docs: Docs::default(),
101                default_value: None,
102                default_value_strategy: None,
103            }
104        );
105    }
106
107    #[test]
108    fn after() {
109        let meta: Meta = syn::parse_quote! { argument(after, "age", number(u8)) };
110        let directive = ArgumentDirective::parse(&meta).unwrap();
111        assert_eq!(
112            directive,
113            ArgumentDirective {
114                after: true,
115                name: "age".into(),
116                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
117                docs: Docs::default(),
118                default_value: None,
119                default_value_strategy: None,
120            }
121        );
122    }
123
124    #[test]
125    fn with_default_value() {
126        let meta: Meta = syn::parse_quote! { argument("age", number(u8), default_value = payer) };
127        let directive = ArgumentDirective::parse(&meta).unwrap();
128        assert_eq!(
129            directive,
130            ArgumentDirective {
131                after: false,
132                name: "age".into(),
133                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
134                docs: Docs::default(),
135                default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())),
136                default_value_strategy: None,
137            }
138        );
139    }
140
141    #[test]
142    fn with_docs_string() {
143        let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = "The cake") };
144        let directive = ArgumentDirective::parse(&meta).unwrap();
145        assert_eq!(directive.docs, vec!["The cake".to_string()].into());
146    }
147
148    #[test]
149    fn with_docs_array() {
150        let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = ["The cake", "must be a lie"]) };
151        let directive = ArgumentDirective::parse(&meta).unwrap();
152        assert_eq!(
153            directive.docs,
154            vec!["The cake".to_string(), "must be a lie".to_string()].into()
155        );
156    }
157}