cucumber_codegen/
parameter.rs

1// Copyright (c) 2020-2025  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! `#[derive(Parameter)]` macro implementation.
12
13use inflections::case::to_lower_case;
14use proc_macro2::TokenStream;
15use quote::quote;
16use regex::Regex;
17use synthez::{ParseAttrs, Required, ToTokens};
18
19/// Expands `#[derive(Parameter)]` macro.
20///
21/// # Errors
22///
23/// If failed to parse [`Attrs`] or the user-provided [`Regex`] is invalid.
24pub(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/// Helper attributes of `#[derive(Parameter)]` macro.
32#[derive(Debug, Default, ParseAttrs)]
33struct Attrs {
34    /// Value for a `Parameter::REGEX` associated constant.
35    #[parse(value)]
36    regex: Required<syn::LitStr>,
37
38    /// Value for a `Parameter::NAME` associated constant.
39    #[parse(value)]
40    name: Option<syn::LitStr>,
41}
42
43/// Representation of a type implementing a `Parameter` trait, used for code
44/// generation.
45#[derive(Debug, ToTokens)]
46#[to_tokens(append(impl_parameter))]
47struct Definition {
48    /// Name of this type.
49    ident: syn::Ident,
50
51    /// [`syn::Generics`] of this type.
52    generics: syn::Generics,
53
54    /// Value for a `Parameter::REGEX` associated constant.
55    regex: Regex,
56
57    /// Value for a `Parameter::Name` associated constant.
58    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    /// Generates code of implementing a `Parameter` trait.
82    #[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}