Skip to main content

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