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