cucumber_codegen/
parameter.rs1use inflections::case::to_lower_case;
14use proc_macro2::TokenStream;
15use quote::quote;
16use regex::Regex;
17use synthez::{ParseAttrs, Required, ToTokens};
18
19pub(crate) fn derive(input: TokenStream) -> syn::Result<TokenStream> {
25 let input = syn::parse2::<syn::DeriveInput>(input)?;
26 let definition = Definition::try_from(input)?;
27
28 Ok(quote! { #definition })
29}
30
31#[derive(Debug, Default, ParseAttrs)]
33struct Attrs {
34 #[parse(value)]
36 regex: Required<syn::LitStr>,
37
38 #[parse(value)]
40 name: Option<syn::LitStr>,
41}
42
43#[derive(Debug, ToTokens)]
46#[to_tokens(append(impl_parameter))]
47struct Definition {
48 ident: syn::Ident,
50
51 generics: syn::Generics,
53
54 regex: Regex,
56
57 name: String,
59}
60
61impl TryFrom<syn::DeriveInput> for Definition {
62 type Error = syn::Error;
63
64 fn try_from(input: syn::DeriveInput) -> syn::Result<Self> {
65 let attrs: Attrs = Attrs::parse_attrs("param", &input)?;
66
67 let regex = Regex::new(&attrs.regex.value()).map_err(|e| {
68 syn::Error::new(attrs.regex.span(), format!("invalid regex: {e}"))
69 })?;
70
71 let name = attrs.name.as_ref().map_or_else(
72 || to_lower_case(&input.ident.to_string()),
73 syn::LitStr::value,
74 );
75
76 Ok(Self { ident: input.ident, generics: input.generics, regex, name })
77 }
78}
79
80impl Definition {
81 #[must_use]
83 fn impl_parameter(&self) -> TokenStream {
84 let ty = &self.ident;
85 let (impl_gens, ty_gens, where_clause) = self.generics.split_for_impl();
86 let (regex, name) = (self.regex.as_str(), &self.name);
87
88 quote! {
89 #[automatically_derived]
90 impl #impl_gens ::cucumber::Parameter for #ty #ty_gens
91 #where_clause
92 {
93 const REGEX: &'static str = #regex;
94 const NAME: &'static str = #name;
95 }
96 }
97 }
98}
99
100#[cfg(test)]
101mod spec {
102 use quote::quote;
103 use syn::parse_quote;
104
105 #[test]
106 fn derives_impl() {
107 let input = parse_quote! {
108 #[param(regex = "cat|dog", name = "custom")]
109 struct Parameter;
110 };
111
112 let output = quote! {
113 #[automatically_derived]
114 impl ::cucumber::Parameter for Parameter {
115 const REGEX: &'static str = "cat|dog";
116 const NAME: &'static str = "custom";
117 }
118 };
119
120 assert_eq!(
121 super::derive(input).unwrap().to_string(),
122 output.to_string(),
123 );
124 }
125
126 #[test]
127 fn derives_impl_with_default_name() {
128 let input = parse_quote! {
129 #[param(regex = "cat|dog")]
130 struct Animal;
131 };
132
133 let output = quote! {
134 #[automatically_derived]
135 impl ::cucumber::Parameter for Animal {
136 const REGEX: &'static str = "cat|dog";
137 const NAME: &'static str = "animal";
138 }
139 };
140
141 assert_eq!(
142 super::derive(input).unwrap().to_string(),
143 output.to_string(),
144 );
145 }
146
147 #[test]
148 fn derives_impl_with_capturing_group() {
149 let input = parse_quote! {
150 #[param(regex = "(cat)|(dog)")]
151 struct Animal;
152 };
153
154 let output = quote! {
155 #[automatically_derived]
156 impl ::cucumber::Parameter for Animal {
157 const REGEX: &'static str = "(cat)|(dog)";
158 const NAME: &'static str = "animal";
159 }
160 };
161
162 assert_eq!(
163 super::derive(input).unwrap().to_string(),
164 output.to_string(),
165 );
166 }
167
168 #[test]
169 fn derives_impl_with_generics() {
170 let input = parse_quote! {
171 #[param(regex = "cat|dog", name = "custom")]
172 struct Parameter<T>(T);
173 };
174
175 let output = quote! {
176 #[automatically_derived]
177 impl<T> ::cucumber::Parameter for Parameter<T> {
178 const REGEX: &'static str = "cat|dog";
179 const NAME: &'static str = "custom";
180 }
181 };
182
183 assert_eq!(
184 super::derive(input).unwrap().to_string(),
185 output.to_string(),
186 );
187 }
188
189 #[test]
190 fn derives_impl_with_non_capturing_regex_groups() {
191 let input = parse_quote! {
192 #[param(regex = "cat|dog(?:s)?", name = "custom")]
193 struct Parameter<T>(T);
194 };
195
196 let output = quote! {
197 #[automatically_derived]
198 impl<T> ::cucumber::Parameter for Parameter<T> {
199 const REGEX: &'static str = "cat|dog(?:s)?";
200 const NAME: &'static str = "custom";
201 }
202 };
203
204 assert_eq!(
205 super::derive(input).unwrap().to_string(),
206 output.to_string(),
207 );
208 }
209
210 #[test]
211 fn regex_arg_is_required() {
212 let input = parse_quote! {
213 #[param(name = "custom")]
214 struct Parameter;
215 };
216
217 let err = super::derive(input).unwrap_err();
218
219 assert_eq!(
220 err.to_string(),
221 "`regex` argument of `#[param]` attribute is expected to be \
222 present, but is absent",
223 );
224 }
225
226 #[test]
227 fn invalid_regex() {
228 let input = parse_quote! {
229 #[param(regex = "(cat|dog")]
230 struct Parameter;
231 };
232
233 let err = super::derive(input).unwrap_err();
234
235 assert_eq!(
236 err.to_string(),
237 "\
238invalid regex: regex parse error:
239 (cat|dog
240 ^
241error: unclosed group",
242 );
243 }
244}