Skip to main content

codama_attributes/codama_directives/
seed_directive.rs

1use crate::{
2    utils::SetOnce, Attribute, AttributeContext, CodamaAttribute, CodamaDirective, Resolvable,
3};
4use codama_errors::CodamaError;
5use codama_nodes::{TypeNode, ValueNode};
6use codama_syn_helpers::{extensions::*, Meta};
7
8#[derive(Debug, PartialEq)]
9pub struct SeedDirective {
10    pub seed: SeedDirectiveType,
11}
12
13#[derive(Debug, PartialEq)]
14pub enum SeedDirectiveType {
15    /// A seed that references a field by name. The type is inferred from the field.
16    Linked(String),
17    /// A variable seed with a name and type (which may be a plugin directive).
18    Variable {
19        name: String,
20        r#type: Resolvable<TypeNode>,
21    },
22    /// A constant seed with a type and value (which may be plugin directives).
23    Constant {
24        r#type: Resolvable<TypeNode>,
25        value: Resolvable<ValueNode>,
26    },
27}
28
29impl SeedDirective {
30    pub fn parse(meta: &Meta, ctx: &AttributeContext) -> syn::Result<Self> {
31        let pl = meta.assert_directive("seed")?.as_path_list()?;
32
33        let constant_seed = pl
34            .parse_metas()?
35            .iter()
36            .find_map(|m| match m.path_str().as_str() {
37                "name" => Some(false),
38                "value" => Some(true),
39                _ => None,
40            })
41            .ok_or_else(|| meta.error("seed must at least specify `name` for variable seeds or `type` and `value` for constant seeds"))?;
42
43        let mut name = SetOnce::<String>::new("name");
44        let mut r#type = SetOnce::<Resolvable<TypeNode>>::new("type");
45        let mut value = SetOnce::<Resolvable<ValueNode>>::new("value");
46
47        pl.each(|ref meta| match (meta.path_str().as_str(), constant_seed) {
48            ("name", true) => Err(meta.error("constant seeds cannot specify name")),
49            ("name", false) => name.set(meta.as_value()?.as_expr()?.as_string()?, meta),
50            ("value", true) => {
51                value.set(Resolvable::<ValueNode>::from_meta(meta.as_value()?)?, meta)
52            }
53            ("value", false) => Err(meta.error("variable seeds cannot specify value")),
54            ("type", _) => r#type.set(Resolvable::<TypeNode>::from_meta(meta.as_value()?)?, meta),
55            _ => Err(meta.error("unrecognized attribute")),
56        })?;
57
58        // Resolve linked seed if possible.
59        if !constant_seed && !r#type.is_set() {
60            let name = name.take(meta)?;
61            if !has_matching_field(ctx, &name) {
62                let message = format!("Could not find field \"{name}\". Either specify a `type` for the seed or use a name that matches a struct or variant field.");
63                return Err(meta.error(message));
64            }
65            return Ok(Self {
66                seed: SeedDirectiveType::Linked(name),
67            });
68        }
69
70        match constant_seed {
71            true => Ok(Self {
72                seed: SeedDirectiveType::Constant {
73                    r#type: r#type.take(meta)?,
74                    value: value.take(meta)?,
75                },
76            }),
77            false => Ok(Self {
78                seed: SeedDirectiveType::Variable {
79                    name: name.take(meta)?,
80                    r#type: r#type.take(meta)?,
81                },
82            }),
83        }
84    }
85}
86
87fn has_matching_field(ctx: &AttributeContext, name: &str) -> bool {
88    let Some(fields) = ctx.get_named_fields() else {
89        return false;
90    };
91
92    fields
93        .named
94        .iter()
95        .any(|f| f.ident.as_ref().is_some_and(|id| id == name))
96}
97
98impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a SeedDirective {
99    type Error = CodamaError;
100
101    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
102        match attribute.directive.as_ref() {
103            CodamaDirective::Seed(ref a) => Ok(a),
104            _ => Err(CodamaError::InvalidCodamaDirective {
105                expected: "seed".to_string(),
106                actual: attribute.directive.name().to_string(),
107            }),
108        }
109    }
110}
111
112impl<'a> TryFrom<&'a Attribute<'a>> for &'a SeedDirective {
113    type Error = CodamaError;
114
115    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
116        <&CodamaAttribute>::try_from(attribute)?.try_into()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use codama_nodes::{NumberFormat::U8, NumberTypeNode, NumberValueNode, PublicKeyTypeNode};
123
124    use super::*;
125
126    #[test]
127    fn defined_constant() {
128        let meta: Meta = syn::parse_quote! { seed(type = number(u8), value = 42) };
129        let item = syn::parse_quote! { struct Foo; };
130        let ctx = AttributeContext::Item(&item);
131        let directive = SeedDirective::parse(&meta, &ctx).unwrap();
132        assert_eq!(
133            directive,
134            SeedDirective {
135                seed: SeedDirectiveType::Constant {
136                    r#type: Resolvable::Resolved(NumberTypeNode::le(U8).into()),
137                    value: Resolvable::Resolved(NumberValueNode::new(42u8).into()),
138                },
139            }
140        );
141    }
142
143    #[test]
144    fn defined_variable() {
145        let meta: Meta = syn::parse_quote! { seed(name = "authority", type = public_key) };
146        let item = syn::parse_quote! { struct Foo; };
147        let ctx = AttributeContext::Item(&item);
148        let directive = SeedDirective::parse(&meta, &ctx).unwrap();
149        assert_eq!(
150            directive,
151            SeedDirective {
152                seed: SeedDirectiveType::Variable {
153                    name: "authority".to_string(),
154                    r#type: Resolvable::Resolved(PublicKeyTypeNode::new().into()),
155                },
156            }
157        );
158    }
159
160    #[test]
161    fn linked_seed() {
162        let meta: Meta = syn::parse_quote! { seed(name = "authority") };
163        let item = syn::parse_quote! { struct Foo { authority: PubKey } };
164        let ctx = AttributeContext::Item(&item);
165        let directive = SeedDirective::parse(&meta, &ctx).unwrap();
166        assert_eq!(
167            directive,
168            SeedDirective {
169                seed: SeedDirectiveType::Linked("authority".to_string()),
170            }
171        );
172    }
173
174    #[test]
175    fn linked_seed_in_variant() {
176        let meta: Meta = syn::parse_quote! { seed(name = "authority") };
177        let item: syn::Variant = syn::parse_quote! { Foo { authority: PubKey } };
178        let ctx = AttributeContext::Variant(&item);
179        let directive = SeedDirective::parse(&meta, &ctx).unwrap();
180        assert_eq!(
181            directive,
182            SeedDirective {
183                seed: SeedDirectiveType::Linked("authority".to_string()),
184            }
185        );
186    }
187
188    #[test]
189    fn cannot_identify_seed_type() {
190        let meta: Meta = syn::parse_quote! { seed(type = public_key) };
191        let item = syn::parse_quote! { struct Foo; };
192        let ctx = AttributeContext::Item(&item);
193        let error = SeedDirective::parse(&meta, &ctx).unwrap_err();
194        assert_eq!(error.to_string(), "seed must at least specify `name` for variable seeds or `type` and `value` for constant seeds");
195    }
196
197    #[test]
198    fn cannot_find_linked_field() {
199        let meta: Meta = syn::parse_quote! { seed(name = "authority") };
200        let item = syn::parse_quote! { struct Foo { owner: PubKey } };
201        let ctx = AttributeContext::Item(&item);
202        let error = SeedDirective::parse(&meta, &ctx).unwrap_err();
203        assert_eq!(
204            error.to_string(),
205            "Could not find field \"authority\". Either specify a `type` for the seed or use a name that matches a struct or variant field."
206        );
207    }
208
209    #[test]
210    fn value_with_name() {
211        let meta: Meta = syn::parse_quote! { seed(name = "amount", value = 42) };
212        let item = syn::parse_quote! { struct Foo; };
213        let ctx = AttributeContext::Item(&item);
214        let error = SeedDirective::parse(&meta, &ctx).unwrap_err();
215        assert_eq!(error.to_string(), "variable seeds cannot specify value");
216    }
217
218    #[test]
219    fn name_with_value() {
220        let meta: Meta = syn::parse_quote! { seed(value = 42, name = "amount") };
221        let item = syn::parse_quote! { struct Foo; };
222        let ctx = AttributeContext::Item(&item);
223        let error = SeedDirective::parse(&meta, &ctx).unwrap_err();
224        assert_eq!(error.to_string(), "constant seeds cannot specify name");
225    }
226}