Skip to main content

ferridriver_bdd/
step.rs

1//! Step definition types: `StepDef`, `StepParam`, `StepHandler`, `StepMatch`.
2
3use std::fmt;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use regex::Regex;
9
10use crate::world::BrowserWorld;
11
12// ── Step kind ──
13
14/// The Gherkin keyword associated with a step definition.
15/// `Step` is keyword-agnostic (matches any keyword).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
17pub enum StepKind {
18  Given,
19  When,
20  Then,
21  Step,
22}
23
24impl fmt::Display for StepKind {
25  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26    match self {
27      Self::Given => write!(f, "Given"),
28      Self::When => write!(f, "When"),
29      Self::Then => write!(f, "Then"),
30      Self::Step => write!(f, "Step"),
31    }
32  }
33}
34
35// ── Step parameters ──
36
37/// Typed parameter extracted from a cucumber expression match.
38#[derive(Debug, Clone, PartialEq)]
39pub enum StepParam {
40  String(String),
41  Int(i64),
42  Float(f64),
43  Word(String),
44  Custom { type_name: String, value: String },
45}
46
47impl StepParam {
48  pub fn as_string(&self) -> Option<String> {
49    match self {
50      Self::String(s) | Self::Word(s) => Some(s.clone()),
51      Self::Int(i) => Some(i.to_string()),
52      Self::Float(f) => Some(f.to_string()),
53      Self::Custom { value, .. } => Some(value.clone()),
54    }
55  }
56
57  pub fn as_int(&self) -> Option<i64> {
58    match self {
59      Self::Int(i) => Some(*i),
60      Self::String(s) | Self::Word(s) => s.parse().ok(),
61      Self::Float(f) => Some(*f as i64),
62      Self::Custom { value, .. } => value.parse().ok(),
63    }
64  }
65
66  pub fn as_float(&self) -> Option<f64> {
67    match self {
68      Self::Float(f) => Some(*f),
69      Self::Int(i) => Some(*i as f64),
70      Self::String(s) | Self::Word(s) => s.parse().ok(),
71      Self::Custom { value, .. } => value.parse().ok(),
72    }
73  }
74}
75
76// ── Data table ──
77
78pub use crate::data_table::DataTable;
79
80// ── Step error ──
81
82/// Error returned by a step handler.
83#[derive(Debug, Clone)]
84pub struct StepError {
85  pub message: String,
86  pub diff: Option<(String, String)>,
87  /// When true, the step is not yet implemented (pending) rather than broken.
88  pub pending: bool,
89}
90
91impl StepError {
92  /// Create a pending step error (step not yet implemented).
93  pub fn pending(message: impl Into<String>) -> Self {
94    Self {
95      message: message.into(),
96      diff: None,
97      pending: true,
98    }
99  }
100
101  /// Wrap a [`ferridriver::FerriError`] with a contextual prefix while
102  /// keeping the Playwright-style class name visible (`TimeoutError:` /
103  /// `TargetClosedError:`). Identical convention to
104  /// [`ferridriver_test::TestFailure::wrap`].
105  #[must_use]
106  pub fn wrap(prefix: impl std::fmt::Display, err: ferridriver::FerriError) -> Self {
107    Self {
108      message: format!("{prefix}: {}", err.display_named()),
109      diff: None,
110      pending: false,
111    }
112  }
113}
114
115impl From<ferridriver::FerriError> for StepError {
116  fn from(err: ferridriver::FerriError) -> Self {
117    Self {
118      message: err.display_named(),
119      diff: None,
120      pending: false,
121    }
122  }
123}
124
125impl From<ferridriver_expect::AssertionFailure> for StepError {
126  fn from(err: ferridriver_expect::AssertionFailure) -> Self {
127    Self {
128      message: err.message,
129      diff: err.diff.map(|d| (d, String::new())),
130      pending: false,
131    }
132  }
133}
134
135impl fmt::Display for StepError {
136  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137    write!(f, "{}", self.message)?;
138    if let Some((expected, actual)) = &self.diff {
139      write!(f, "\n  expected: {expected}\n  actual:   {actual}")?;
140    }
141    Ok(())
142  }
143}
144
145impl std::error::Error for StepError {}
146
147impl From<String> for StepError {
148  fn from(message: String) -> Self {
149    Self {
150      message,
151      diff: None,
152      pending: false,
153    }
154  }
155}
156
157impl From<&str> for StepError {
158  fn from(message: &str) -> Self {
159    Self {
160      message: message.to_string(),
161      diff: None,
162      pending: false,
163    }
164  }
165}
166
167// ── Step handler ──
168
169/// Async step handler function signature.
170pub type StepHandler = Arc<
171  dyn for<'a> Fn(
172      &'a mut BrowserWorld,
173      Vec<StepParam>,
174      Option<&'a DataTable>,
175      Option<&'a str>,
176    ) -> Pin<Box<dyn Future<Output = Result<(), StepError>> + Send + 'a>>
177    + Send
178    + Sync,
179>;
180
181// ── Step location ──
182
183/// Source location of a step definition (for diagnostics).
184#[derive(Debug, Clone)]
185pub struct StepLocation {
186  pub file: &'static str,
187  pub line: u32,
188}
189
190impl fmt::Display for StepLocation {
191  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192    write!(f, "{}:{}", self.file, self.line)
193  }
194}
195
196// ── Step definition ──
197
198/// A compiled step definition: expression + handler + metadata.
199pub struct StepDef {
200  /// The kind of step (Given/When/Then/Step).
201  pub kind: StepKind,
202  /// Original cucumber expression source string.
203  pub expression: String,
204  /// Compiled regex from the cucumber expression.
205  pub regex: Regex,
206  /// Expected parameter types extracted from the expression.
207  pub param_types: Vec<crate::expression::ParamType>,
208  /// Full parameter info (type + id) for named capture group resolution.
209  pub param_infos: Vec<crate::expression::ParamInfo>,
210  /// The async handler function.
211  pub handler: StepHandler,
212  /// Source location for diagnostics.
213  pub location: StepLocation,
214}
215
216// ── Step match result ──
217
218/// Result of successfully matching a step text against a `StepDef`.
219pub struct StepMatch<'a> {
220  pub def: &'a StepDef,
221  pub params: Vec<StepParam>,
222}
223
224// ── Step match error ──
225
226/// Error when no step definition matches, or multiple definitions match.
227#[derive(Debug)]
228pub enum MatchError {
229  /// No step definition matched the text.
230  Undefined { text: String, suggestions: Vec<String> },
231  /// Multiple step definitions matched the text (ambiguous).
232  Ambiguous {
233    text: String,
234    matches: Vec<StepLocation>,
235    expressions: Vec<String>,
236  },
237}
238
239impl fmt::Display for MatchError {
240  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241    match self {
242      Self::Undefined { text, suggestions } => {
243        write!(f, "undefined step: \"{text}\"")?;
244        if !suggestions.is_empty() {
245          write!(f, "\n  did you mean:")?;
246          for s in suggestions {
247            write!(f, "\n    - {s}")?;
248          }
249        }
250        Ok(())
251      },
252      Self::Ambiguous {
253        text,
254        matches,
255        expressions,
256      } => {
257        write!(f, "ambiguous step: \"{text}\" matched {} definitions:", matches.len())?;
258        for (i, (loc, expr)) in matches.iter().zip(expressions.iter()).enumerate() {
259          write!(f, "\n  {}. {} ({})", i + 1, expr, loc)?;
260        }
261        Ok(())
262      },
263    }
264  }
265}
266
267impl std::error::Error for MatchError {}
268
269// ── Inventory registration type ──
270
271/// What the proc macros submit via `inventory::submit!`.
272pub struct StepRegistration {
273  pub kind: StepKind,
274  pub expression: &'static str,
275  pub handler_factory: fn() -> StepHandler,
276  pub file: &'static str,
277  pub line: u32,
278  /// When true, `expression` is a raw regex pattern instead of a cucumber expression.
279  pub is_regex: bool,
280}
281
282inventory::collect!(StepRegistration);
283
284/// Convenience macro for submitting step registrations from proc macro expansion.
285#[macro_export]
286macro_rules! submit_step {
287  ($name:ident, $kind:expr, $expr:expr, $handler:ident,) => {
288    ferridriver_bdd::inventory::submit! {
289      ferridriver_bdd::step::StepRegistration {
290        kind: $kind,
291        expression: $expr,
292        handler_factory: $handler,
293        file: file!(),
294        line: line!(),
295        is_regex: false,
296      }
297    }
298  };
299  ($name:ident, $kind:expr, $expr:expr, $handler:ident, regex = $is_regex:expr,) => {
300    ferridriver_bdd::inventory::submit! {
301      ferridriver_bdd::step::StepRegistration {
302        kind: $kind,
303        expression: $expr,
304        handler_factory: $handler,
305        file: file!(),
306        line: line!(),
307        is_regex: $is_regex,
308      }
309    }
310  };
311}