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