Skip to main content

cucumber_codegen/
lib.rs

1// Copyright (c) 2020-2026  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#![doc(
12    html_logo_url = "https://avatars.githubusercontent.com/u/91469139?s=128",
13    html_favicon_url = "https://avatars.githubusercontent.com/u/91469139?s=256"
14)]
15#![cfg_attr(any(doc, test), doc = include_str!("../README.md"))]
16#![cfg_attr(not(any(doc, test)), doc = env!("CARGO_PKG_NAME"))]
17#![deny(nonstandard_style, rustdoc::all, trivial_casts, trivial_numeric_casts)]
18#![forbid(non_ascii_idents, unsafe_code)]
19#![warn(
20    clippy::absolute_paths,
21    clippy::allow_attributes,
22    clippy::allow_attributes_without_reason,
23    clippy::as_conversions,
24    clippy::as_pointer_underscore,
25    clippy::as_ptr_cast_mut,
26    clippy::assertions_on_result_states,
27    clippy::branches_sharing_code,
28    clippy::cfg_not_test,
29    clippy::clear_with_drain,
30    clippy::clone_on_ref_ptr,
31    clippy::coerce_container_to_any,
32    clippy::collection_is_never_read,
33    clippy::create_dir,
34    clippy::dbg_macro,
35    clippy::debug_assert_with_mut_call,
36    clippy::decimal_literal_representation,
37    clippy::default_union_representation,
38    clippy::derive_partial_eq_without_eq,
39    clippy::doc_include_without_cfg,
40    clippy::doc_paragraphs_missing_punctuation,
41    clippy::empty_drop,
42    clippy::empty_structs_with_brackets,
43    clippy::equatable_if_let,
44    clippy::empty_enum_variants_with_brackets,
45    clippy::exit,
46    clippy::expect_used,
47    clippy::fallible_impl_from,
48    clippy::filetype_is_file,
49    clippy::float_cmp_const,
50    clippy::fn_to_numeric_cast_any,
51    clippy::get_unwrap,
52    clippy::if_then_some_else_none,
53    clippy::imprecise_flops,
54    clippy::infinite_loop,
55    clippy::iter_on_empty_collections,
56    clippy::iter_on_single_items,
57    clippy::iter_over_hash_type,
58    clippy::iter_with_drain,
59    clippy::large_include_file,
60    clippy::large_stack_frames,
61    clippy::let_underscore_untyped,
62    clippy::literal_string_with_formatting_args,
63    clippy::lossy_float_literal,
64    clippy::map_err_ignore,
65    clippy::map_with_unused_argument_over_ranges,
66    clippy::mem_forget,
67    clippy::missing_assert_message,
68    clippy::missing_asserts_for_indexing,
69    clippy::missing_const_for_fn,
70    clippy::missing_docs_in_private_items,
71    clippy::module_name_repetitions,
72    clippy::multiple_inherent_impl,
73    clippy::multiple_unsafe_ops_per_block,
74    clippy::mutex_atomic,
75    clippy::mutex_integer,
76    clippy::needless_collect,
77    clippy::needless_pass_by_ref_mut,
78    clippy::needless_raw_strings,
79    clippy::needless_type_cast,
80    clippy::non_zero_suggestions,
81    clippy::nonstandard_macro_braces,
82    clippy::option_if_let_else,
83    clippy::or_fun_call,
84    clippy::panic_in_result_fn,
85    clippy::partial_pub_fields,
86    clippy::pathbuf_init_then_push,
87    clippy::pedantic,
88    clippy::precedence_bits,
89    clippy::print_stderr,
90    clippy::print_stdout,
91    clippy::pub_without_shorthand,
92    clippy::rc_buffer,
93    clippy::rc_mutex,
94    clippy::read_zero_byte_vec,
95    clippy::redundant_clone,
96    clippy::redundant_test_prefix,
97    clippy::redundant_type_annotations,
98    clippy::renamed_function_params,
99    clippy::ref_patterns,
100    clippy::rest_pat_in_fully_bound_structs,
101    clippy::return_and_then,
102    clippy::same_name_method,
103    clippy::semicolon_inside_block,
104    clippy::set_contains_or_insert,
105    clippy::shadow_unrelated,
106    clippy::significant_drop_in_scrutinee,
107    clippy::significant_drop_tightening,
108    clippy::single_option_map,
109    clippy::str_to_string,
110    clippy::string_add,
111    clippy::string_lit_as_bytes,
112    clippy::string_lit_chars_any,
113    clippy::string_slice,
114    clippy::suboptimal_flops,
115    clippy::suspicious_operation_groupings,
116    clippy::suspicious_xor_used_as_pow,
117    clippy::tests_outside_test_module,
118    clippy::todo,
119    clippy::too_long_first_doc_paragraph,
120    clippy::trailing_empty_array,
121    clippy::transmute_undefined_repr,
122    clippy::trivial_regex,
123    clippy::try_err,
124    clippy::undocumented_unsafe_blocks,
125    clippy::unimplemented,
126    clippy::uninhabited_references,
127    clippy::unnecessary_safety_comment,
128    clippy::unnecessary_safety_doc,
129    clippy::unnecessary_self_imports,
130    clippy::unnecessary_struct_initialization,
131    clippy::unused_peekable,
132    clippy::unused_result_ok,
133    clippy::unused_trait_names,
134    clippy::unwrap_in_result,
135    clippy::unwrap_used,
136    clippy::use_debug,
137    clippy::use_self,
138    clippy::useless_let_if_seq,
139    clippy::verbose_file_reads,
140    clippy::volatile_composites,
141    clippy::while_float,
142    clippy::wildcard_enum_match_arm,
143    ambiguous_negative_literals,
144    closure_returning_async_block,
145    future_incompatible,
146    impl_trait_redundant_captures,
147    let_underscore_drop,
148    macro_use_extern_crate,
149    meta_variable_misuse,
150    missing_copy_implementations,
151    missing_debug_implementations,
152    missing_docs,
153    redundant_lifetimes,
154    rust_2018_idioms,
155    single_use_lifetimes,
156    unit_bindings,
157    unnameable_types,
158    unreachable_pub,
159    unstable_features,
160    unused,
161    variant_size_differences
162)]
163
164mod attribute;
165mod parameter;
166mod world;
167
168// TODO: Remove once tests run without complains about it.
169#[cfg(test)]
170mod only_used_in_doc_tests {
171    use cucumber as _;
172    use derive_more as _;
173    use futures as _;
174    use tempfile as _;
175    use tokio as _;
176}
177
178use proc_macro::TokenStream;
179
180/// Helper macro for generating public shims for [`macro@given`], [`macro@when`]
181/// and [`macro@then`] attributes.
182macro_rules! step_attribute {
183    ($name:ident) => {
184        /// Attribute to auto-wire the test to the [`World`] implementer.
185        ///
186        /// There are 3 step-specific attributes:
187        /// - [`macro@given`]
188        /// - [`macro@when`]
189        /// - [`macro@then`]
190        ///
191        /// # Example
192        ///
193        /// ```
194        /// # use std::{convert::Infallible};
195        /// #
196        /// use cucumber::{World, given, when};
197        ///
198        /// #[derive(Debug, Default, World)]
199        /// struct MyWorld;
200        ///
201        /// #[given(regex = r"(\S+) is (\d+)")]
202        /// #[when(expr = "{word} is {int}")]
203        /// fn test(w: &mut MyWorld, param: String, num: i32) {
204        ///     assert_eq!(param, "foo");
205        ///     assert_eq!(num, 0);
206        /// }
207        ///
208        /// #[tokio::main]
209        /// async fn main() {
210        ///     MyWorld::run("./tests/features/doctests.feature").await;
211        /// }
212        /// ```
213        ///
214        /// # Attribute arguments
215        ///
216        /// - `#[given(regex = "regex")]`
217        ///
218        ///   Uses [`Regex`] for matching the step. [`Regex`] is checked at
219        ///   compile time to have valid syntax.
220        ///
221        /// - `#[given(expr = "cucumber-expression")]`
222        ///
223        ///   Uses [Cucumber Expression][1] for matching the step. It's checked
224        ///   at compile time to have valid syntax.
225        ///
226        /// - `#[given("literal")]`
227        ///
228        ///   Matches the step with an **exact** literal only. Doesn't allow any
229        ///   values capturing to use as function arguments.
230        ///
231        /// # Function arguments
232        ///
233        /// - First argument has to be mutable reference to the [`World`]
234        ///   deriver.
235        /// - Other argument's types have to implement [`FromStr`] or it has to
236        ///   be a slice where the element type also implements [`FromStr`].
237        /// - To use [`gherkin::Step`], name the argument as `step`,
238        ///   **or** mark the argument with a `#[step]` attribute.
239        ///
240        /// ```rust
241        /// # use std::convert::Infallible;
242        /// #
243        /// # use cucumber::{gherkin::Step, given, World};
244        /// #
245        /// # #[derive(Debug, Default, World)]
246        /// # struct MyWorld;
247        /// #
248        /// #[given(regex = r"(\S+) is not (\S+)")]
249        /// fn test_step(
250        ///     w: &mut MyWorld,
251        ///     #[step] s: &Step,
252        ///     matches: &[String],
253        /// ) {
254        ///     assert_eq!(matches[0], "foo");
255        ///     assert_eq!(matches[1], "bar");
256        ///     assert_eq!(s.value, "foo is not bar");
257        /// }
258        /// #
259        /// # #[tokio::main]
260        /// # async fn main() {
261        /// #     MyWorld::run("./tests/features/doctests.feature").await;
262        /// # }
263        /// ```
264        ///
265        /// # Return value
266        ///
267        /// A function may also return a [`Result`], which [`Err`] is expected
268        /// to implement [`Display`], so returning it will cause the step to
269        /// fail.
270        ///
271        /// [`Display`]: std::fmt::Display
272        /// [`FromStr`]: std::str::FromStr
273        /// [`Regex`]: regex::Regex
274        /// [`gherkin::Step`]: https://bit.ly/3j42hcd
275        /// [`World`]: https://bit.ly/3j0aWw7
276        /// [1]: cucumber_expressions
277        #[proc_macro_attribute]
278        pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream {
279            attribute::step(std::stringify!($name), args.into(), input.into())
280                .unwrap_or_else(syn::Error::into_compile_error)
281                .into()
282        }
283    };
284}
285
286/// Helper macro for generating public shim of [`macro@given`], [`macro@when`]
287/// and [`macro@then`] attributes.
288macro_rules! steps {
289    ($($name:ident),*) => {
290        $(step_attribute!($name);)*
291    }
292}
293
294steps!(given, when, then);
295
296/// Derive macro for implementing a [`World`] trait.
297///
298/// # Example
299///
300/// ```rust
301/// #[derive(cucumber::World)]
302/// #[world(init = Self::new)] // optional, uses `Default::default()` if omitted
303/// struct World(usize);
304///
305/// impl World {
306///     fn new() -> Self {
307///         Self(42)
308///     }
309/// }
310/// ```
311///
312/// # Attribute arguments
313///
314/// - `#[world(init = path::to::fn)]`
315///
316///   Path to a function to be used for a [`World`] instance construction.
317///   Specified function can be either sync or `async`, and either fallible
318///   (return [`Result`]) or infallible (return [`World`] itself). In case no
319///   function is specified, the [`Default::default()`] will be used for
320///   construction.
321#[proc_macro_derive(World, attributes(world))]
322pub fn world(input: TokenStream) -> TokenStream {
323    world::derive(input.into())
324        .unwrap_or_else(syn::Error::into_compile_error)
325        .into()
326}
327
328/// In addition to [default parameters] of [Cucumber Expressions], you may
329/// implement and use custom ones.
330///
331/// # Example
332///
333/// ```rust
334/// # use std::{convert::Infallible};
335/// #
336/// use cucumber::{Parameter, World, given, when};
337/// use derive_more::{Deref, FromStr};
338///
339/// #[derive(Debug, Default, World)]
340/// struct MyWorld;
341///
342/// #[given(regex = r"^(\S+) is (\d+)$")]
343/// #[when(expr = "{word} is {u64}")]
344/// fn test(w: &mut MyWorld, param: String, num: CustomU64) {
345///     assert_eq!(param, "foo");
346///     assert_eq!(*num, 0);
347/// }
348///
349/// #[derive(Deref, FromStr, Parameter)]
350/// #[param(regex = r"\d+", name = "u64")]
351/// struct CustomU64(u64);
352/// #
353/// # #[tokio::main]
354/// # async fn main() {
355/// #     MyWorld::run("./tests/features/doctests.feature").await;
356/// # }
357/// ```
358///
359/// # Attribute arguments
360///
361/// - `#[param(regex = "regex")]`
362///
363///   [`Regex`] to match this parameter. Usually shouldn't contain any capturing
364///   groups, but in case it requires to do so, only the first non-empty group
365///   will be matched as the result.
366///
367/// - `#[param(name = "name")]` (optional)
368///
369///   Name of this parameter to reference it by. If not specified, then
370///   lower-cased type name will be used by default.
371///
372/// [`Regex`]: regex::Regex
373/// [Cucumber Expressions]: https://cucumber.github.io/cucumber-expressions
374/// [default parameters]: cucumber_expressions::Expression#parameter-types
375#[proc_macro_derive(Parameter, attributes(param))]
376pub fn parameter(input: TokenStream) -> TokenStream {
377    parameter::derive(input.into())
378        .unwrap_or_else(syn::Error::into_compile_error)
379        .into()
380}