cucumber/
step.rs

1// Copyright (c) 2018-2024  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//! Definitions for a [`Collection`] which is used to store [`Step`] [`Fn`]s and
12//! corresponding [`Regex`] patterns.
13//!
14//! [`Step`]: gherkin::Step
15
16use std::{
17    cmp::Ordering,
18    collections::HashMap,
19    fmt,
20    hash::{Hash, Hasher},
21    iter,
22};
23
24use derive_more::{Deref, DerefMut, Display, Error};
25use futures::future::LocalBoxFuture;
26use gherkin::StepType;
27use itertools::Itertools as _;
28use regex::Regex;
29
30/// Alias for a [`gherkin::Step`] function that returns a [`LocalBoxFuture`].
31pub type Step<World> =
32    for<'a> fn(&'a mut World, Context) -> LocalBoxFuture<'a, ()>;
33
34/// Alias for a [`Step`] with [`regex::CaptureLocations`], [`Location`] and
35/// [`Context`] returned by [`Collection::find()`].
36pub type WithContext<'me, World> = (
37    &'me Step<World>,
38    regex::CaptureLocations,
39    Option<Location>,
40    Context,
41);
42
43/// Collection of [`Step`]s.
44///
45/// Every [`Step`] has to match with exactly 1 [`Regex`].
46pub struct Collection<World> {
47    /// Collection of [Given] [`Step`]s.
48    ///
49    /// [Given]: https://cucumber.io/docs/gherkin/reference#given
50    given: HashMap<(HashableRegex, Option<Location>), Step<World>>,
51
52    /// Collection of [When] [`Step`]s.
53    ///
54    /// [When]: https://cucumber.io/docs/gherkin/reference#when
55    when: HashMap<(HashableRegex, Option<Location>), Step<World>>,
56
57    /// Collection of [Then] [`Step`]s.
58    ///
59    /// [Then]: https://cucumber.io/docs/gherkin/reference#then
60    then: HashMap<(HashableRegex, Option<Location>), Step<World>>,
61}
62
63impl<World> fmt::Debug for Collection<World> {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.debug_struct("Collection")
66            .field(
67                "given",
68                &self
69                    .given
70                    .iter()
71                    .map(|(re, step)| (re, format!("{step:p}")))
72                    .collect::<HashMap<_, _>>(),
73            )
74            .field(
75                "when",
76                &self
77                    .when
78                    .iter()
79                    .map(|(re, step)| (re, format!("{step:p}")))
80                    .collect::<HashMap<_, _>>(),
81            )
82            .field(
83                "then",
84                &self
85                    .then
86                    .iter()
87                    .map(|(re, step)| (re, format!("{step:p}")))
88                    .collect::<HashMap<_, _>>(),
89            )
90            .finish()
91    }
92}
93
94// Implemented manually to omit redundant `World: Clone` trait bound, imposed by
95// `#[derive(Clone)]`.
96impl<World> Clone for Collection<World> {
97    fn clone(&self) -> Self {
98        Self {
99            given: self.given.clone(),
100            when: self.when.clone(),
101            then: self.then.clone(),
102        }
103    }
104}
105
106// Implemented manually to omit redundant `World: Default` trait bound, imposed
107// by `#[derive(Default)]`.
108impl<World> Default for Collection<World> {
109    fn default() -> Self {
110        Self {
111            given: HashMap::new(),
112            when: HashMap::new(),
113            then: HashMap::new(),
114        }
115    }
116}
117
118impl<World> Collection<World> {
119    /// Creates a new empty [`Collection`].
120    #[must_use]
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Adds a [Given] [`Step`] matching the given `regex`.
126    ///
127    /// [Given]: https://cucumber.io/docs/gherkin/reference#given
128    #[must_use]
129    pub fn given(
130        mut self,
131        loc: Option<Location>,
132        regex: Regex,
133        step: Step<World>,
134    ) -> Self {
135        _ = self.given.insert((regex.into(), loc), step);
136        self
137    }
138
139    /// Adds a [When] [`Step`] matching the given `regex`.
140    ///
141    /// [When]: https://cucumber.io/docs/gherkin/reference#when
142    #[must_use]
143    pub fn when(
144        mut self,
145        loc: Option<Location>,
146        regex: Regex,
147        step: Step<World>,
148    ) -> Self {
149        _ = self.when.insert((regex.into(), loc), step);
150        self
151    }
152
153    /// Adds a [Then] [`Step`] matching the given `regex`.
154    ///
155    /// [Then]: https://cucumber.io/docs/gherkin/reference#then
156    #[must_use]
157    pub fn then(
158        mut self,
159        loc: Option<Location>,
160        regex: Regex,
161        step: Step<World>,
162    ) -> Self {
163        _ = self.then.insert((regex.into(), loc), step);
164        self
165    }
166
167    /// Returns a [`Step`] function matching the given [`gherkin::Step`], if
168    /// any.
169    ///
170    /// # Errors
171    ///
172    /// If the given [`gherkin::Step`] matches multiple [`Regex`]es.
173    pub fn find(
174        &self,
175        step: &gherkin::Step,
176    ) -> Result<Option<WithContext<'_, World>>, AmbiguousMatchError> {
177        let collection = match step.ty {
178            StepType::Given => &self.given,
179            StepType::When => &self.when,
180            StepType::Then => &self.then,
181        };
182
183        let mut captures = collection
184            .iter()
185            .filter_map(|((re, loc), step_fn)| {
186                let mut captures = re.capture_locations();
187                let names = re.capture_names();
188                re.captures_read(&mut captures, &step.value)
189                    .map(|m| (re, loc, m, captures, names, step_fn))
190            })
191            .collect::<Vec<_>>();
192
193        let (_, loc, whole_match, captures, names, step_fn) =
194            match captures.len() {
195                0 => return Ok(None),
196                // Instead of `.unwrap()` to avoid documenting `# Panics`.
197                1 => captures.pop().unwrap_or_else(|| unreachable!()),
198                _ => {
199                    return Err(AmbiguousMatchError {
200                        possible_matches: captures
201                            .into_iter()
202                            .map(|(re, loc, ..)| (re.clone(), *loc))
203                            .sorted()
204                            .collect(),
205                    })
206                }
207            };
208
209        // PANIC: Slicing is OK here, as all indices are obtained from the
210        //        source string.
211        #[allow(clippy::string_slice)] // intentional
212        let matches = names
213            .map(|opt| opt.map(str::to_owned))
214            .zip(iter::once(whole_match.as_str().to_owned()).chain(
215                (1..captures.len()).map(|group_id| {
216                    captures
217                        .get(group_id)
218                        .map_or("", |(s, e)| &step.value[s..e])
219                        .to_owned()
220                }),
221            ))
222            .collect();
223
224        Ok(Some((
225            step_fn,
226            captures,
227            *loc,
228            Context {
229                step: step.clone(),
230                matches,
231            },
232        )))
233    }
234}
235
236/// Name of a capturing group inside a [`regex`].
237pub type CaptureName = Option<String>;
238
239/// Context for a [`Step`] function execution.
240#[derive(Clone, Debug)]
241pub struct Context {
242    /// [`Step`] matched to a [`Step`] function.
243    ///
244    /// [`Step`]: gherkin::Step
245    pub step: gherkin::Step,
246
247    /// [`Regex`] matches of a [`Step::value`].
248    ///
249    /// [`Step::value`]: gherkin::Step::value
250    pub matches: Vec<(CaptureName, String)>,
251}
252
253/// Error of a [`gherkin::Step`] matching multiple [`Step`] [`Regex`]es inside a
254/// [`Collection`].
255#[derive(Clone, Debug, Error)]
256pub struct AmbiguousMatchError {
257    /// Possible [`Regex`]es the [`gherkin::Step`] matches.
258    pub possible_matches: Vec<(HashableRegex, Option<Location>)>,
259}
260
261impl fmt::Display for AmbiguousMatchError {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        write!(f, "Possible matches:")?;
264        for (reg, loc_opt) in &self.possible_matches {
265            write!(f, "\n{reg}")?;
266            if let Some(loc) = loc_opt {
267                write!(f, " --> {loc}")?;
268            }
269        }
270        Ok(())
271    }
272}
273
274/// Location of a [`Step`] [`fn`] automatically filled by a proc macro.
275#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
276#[display(fmt = "{}:{}:{}", path, line, column)]
277pub struct Location {
278    /// Path to the file where [`Step`] [`fn`] is located.
279    pub path: &'static str,
280
281    /// Line of the file where [`Step`] [`fn`] is located.
282    pub line: u32,
283
284    /// Column of the file where [`Step`] [`fn`] is located.
285    pub column: u32,
286}
287
288/// [`Regex`] wrapper implementing [`Eq`], [`Ord`] and [`Hash`].
289#[derive(Clone, Debug, Deref, DerefMut, Display)]
290pub struct HashableRegex(Regex);
291
292impl From<Regex> for HashableRegex {
293    fn from(re: Regex) -> Self {
294        Self(re)
295    }
296}
297
298impl Hash for HashableRegex {
299    fn hash<H: Hasher>(&self, state: &mut H) {
300        self.0.as_str().hash(state);
301    }
302}
303
304impl PartialEq for HashableRegex {
305    fn eq(&self, other: &Self) -> bool {
306        self.0.as_str() == other.0.as_str()
307    }
308}
309
310impl Eq for HashableRegex {}
311
312impl PartialOrd for HashableRegex {
313    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
314        Some(self.cmp(other))
315    }
316}
317
318impl Ord for HashableRegex {
319    fn cmp(&self, other: &Self) -> Ordering {
320        self.0.as_str().cmp(other.0.as_str())
321    }
322}