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            display: None,
60        })
61    }
62}
63
64impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a ArgumentDirective {
65    type Error = CodamaError;
66
67    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
68        match attribute.directive.as_ref() {
69            CodamaDirective::Argument(ref a) => Ok(a),
70            _ => Err(CodamaError::InvalidCodamaDirective {
71                expected: "argument".to_string(),
72                actual: attribute.directive.name().to_string(),
73            }),
74        }
75    }
76}
77
78impl<'a> TryFrom<&'a Attribute<'a>> for &'a ArgumentDirective {
79    type Error = CodamaError;
80
81    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
82        <&CodamaAttribute>::try_from(attribute)?.try_into()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use codama_nodes::{NumberFormat::U8, NumberTypeNode, PayerValueNode};
90
91    #[test]
92    fn ok() {
93        let meta: Meta = syn::parse_quote! { argument("age", number(u8)) };
94        let directive = ArgumentDirective::parse(&meta).unwrap();
95        assert_eq!(
96            directive,
97            ArgumentDirective {
98                after: false,
99                name: "age".into(),
100                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
101                docs: Docs::default(),
102                default_value: None,
103                default_value_strategy: None,
104            }
105        );
106    }
107
108    #[test]
109    fn after() {
110        let meta: Meta = syn::parse_quote! { argument(after, "age", number(u8)) };
111        let directive = ArgumentDirective::parse(&meta).unwrap();
112        assert_eq!(
113            directive,
114            ArgumentDirective {
115                after: true,
116                name: "age".into(),
117                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
118                docs: Docs::default(),
119                default_value: None,
120                default_value_strategy: None,
121            }
122        );
123    }
124
125    #[test]
126    fn with_default_value() {
127        let meta: Meta = syn::parse_quote! { argument("age", number(u8), default_value = payer) };
128        let directive = ArgumentDirective::parse(&meta).unwrap();
129        assert_eq!(
130            directive,
131            ArgumentDirective {
132                after: false,
133                name: "age".into(),
134                r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
135                docs: Docs::default(),
136                default_value: Some(Resolvable::Resolved(PayerValueNode::new().into())),
137                default_value_strategy: None,
138            }
139        );
140    }
141
142    #[test]
143    fn with_docs_string() {
144        let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = "The cake") };
145        let directive = ArgumentDirective::parse(&meta).unwrap();
146        assert_eq!(directive.docs, vec!["The cake".to_string()].into());
147    }
148
149    #[test]
150    fn with_docs_array() {
151        let meta: Meta = syn::parse_quote! { argument("cake", number(u8), docs = ["The cake", "must be a lie"]) };
152        let directive = ArgumentDirective::parse(&meta).unwrap();
153        assert_eq!(
154            directive.docs,
155            vec!["The cake".to_string(), "must be a lie".to_string()].into()
156        );
157    }
158}