codama_attributes/codama_directives/
seed_directive.rs1use 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 Linked(String),
17 Variable {
19 name: String,
20 r#type: Resolvable<TypeNode>,
21 },
22 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 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}