Skip to main content

rstest_bdd/
types.rs

1//! Core types and error enums shared across the crate.
2//!
3//! The module defines lightweight wrappers for pattern and step text, the step
4//! keyword enum with parsing helpers, error types, and common type aliases used
5//! by the registry and runner.
6
7use crate::localization;
8use std::any::Any;
9use std::fmt;
10use std::future::Future;
11use std::pin::Pin;
12
13// Re-export shared keyword types from rstest-bdd-patterns.
14pub use rstest_bdd_patterns::{
15    StepKeyword, StepKeywordParseError, UnsupportedStepType as UnsupportedStepTypeBase,
16};
17
18/// Error raised when converting a parsed Gherkin [`gherkin::StepType`] into a
19/// [`StepKeyword`] fails.
20///
21/// This is a localized wrapper around [`UnsupportedStepTypeBase`] that uses the
22/// runtime localization system for user-friendly error messages.
23///
24/// # Examples
25///
26/// ```rust
27/// use gherkin::StepType;
28/// use rstest_bdd::{StepKeyword, UnsupportedStepType};
29///
30/// fn convert(ty: StepType) -> Result<StepKeyword, UnsupportedStepType> {
31///     StepKeyword::try_from(ty).map_err(UnsupportedStepType::from)
32/// }
33///
34/// match convert(StepType::Given) {
35///     Ok(keyword) => assert_eq!(keyword, StepKeyword::Given),
36///     Err(error) => {
37///         eprintln!("unsupported step type: {:?}", error.0);
38///     }
39/// }
40/// ```
41#[derive(Debug)]
42pub struct UnsupportedStepType(pub gherkin::StepType);
43
44impl fmt::Display for UnsupportedStepType {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        let message = localization::message_with_args("unsupported-step-type", |args| {
47            args.set("step_type", format!("{:?}", self.0));
48        });
49        f.write_str(&message)
50    }
51}
52
53impl std::error::Error for UnsupportedStepType {}
54
55impl From<UnsupportedStepTypeBase> for UnsupportedStepType {
56    fn from(base: UnsupportedStepTypeBase) -> Self {
57        Self(base.0)
58    }
59}
60
61/// Wrapper for step pattern strings used in matching logic.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub struct PatternStr<'a>(pub(crate) &'a str);
64
65impl<'a> PatternStr<'a> {
66    /// Construct a new `PatternStr` from a string slice.
67    #[must_use]
68    pub const fn new(s: &'a str) -> Self {
69        Self(s)
70    }
71
72    /// Access the underlying string slice.
73    #[must_use]
74    pub const fn as_str(self) -> &'a str {
75        self.0
76    }
77}
78
79impl<'a> From<&'a str> for PatternStr<'a> {
80    fn from(s: &'a str) -> Self {
81        Self::new(s)
82    }
83}
84
85/// Wrapper for step text content from scenarios.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct StepText<'a>(pub(crate) &'a str);
88
89impl<'a> StepText<'a> {
90    /// Construct a new `StepText` from a string slice.
91    #[must_use]
92    pub const fn new(s: &'a str) -> Self {
93        Self(s)
94    }
95
96    /// Access the underlying string slice.
97    #[must_use]
98    pub const fn as_str(self) -> &'a str {
99        self.0
100    }
101}
102
103impl<'a> From<&'a str> for StepText<'a> {
104    fn from(s: &'a str) -> Self {
105        Self::new(s)
106    }
107}
108
109/// Detailed information about placeholder parsing failures.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct PlaceholderSyntaxError {
112    /// Human‑readable reason for the failure.
113    pub message: String,
114    /// Zero-based byte offset in the original pattern where parsing failed.
115    pub position: usize,
116    /// Name of the placeholder, when known.
117    pub placeholder: Option<String>,
118}
119
120impl PlaceholderSyntaxError {
121    /// Construct a new syntax error with optional placeholder context.
122    #[must_use]
123    pub fn new(message: impl Into<String>, position: usize, placeholder: Option<String>) -> Self {
124        Self {
125            message: message.into(),
126            position,
127            placeholder,
128        }
129    }
130
131    /// Return the user‑facing message without the "invalid placeholder syntax" prefix.
132    #[must_use]
133    pub fn user_message(&self) -> String {
134        let suffix = self
135            .placeholder
136            .as_ref()
137            .map(|name| {
138                let detail = localization::message_with_args("placeholder-syntax-suffix", |args| {
139                    args.set("placeholder", name.clone());
140                });
141                format!(" {detail}")
142            })
143            .unwrap_or_default();
144        localization::message_with_args("placeholder-syntax-detail", |args| {
145            args.set("reason", self.message.clone());
146            args.set("position", self.position.to_string());
147            args.set("suffix", suffix);
148        })
149    }
150}
151
152impl fmt::Display for PlaceholderSyntaxError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let message = localization::message_with_args("placeholder-syntax", |args| {
155            args.set("details", self.user_message());
156        });
157        f.write_str(&message)
158    }
159}
160
161impl std::error::Error for PlaceholderSyntaxError {}
162
163/// Errors that may occur when compiling a [`StepPattern`].
164#[derive(Debug)]
165#[non_exhaustive]
166pub enum StepPatternError {
167    /// Placeholder syntax in the pattern is invalid.
168    PlaceholderSyntax(PlaceholderSyntaxError),
169    /// The generated regular expression failed to compile.
170    InvalidPattern(regex::Error),
171}
172
173impl From<PlaceholderSyntaxError> for StepPatternError {
174    fn from(err: PlaceholderSyntaxError) -> Self {
175        Self::PlaceholderSyntax(err)
176    }
177}
178
179impl From<regex::Error> for StepPatternError {
180    fn from(err: regex::Error) -> Self {
181        Self::InvalidPattern(err)
182    }
183}
184
185impl fmt::Display for StepPatternError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            Self::PlaceholderSyntax(err) => err.fmt(f),
189            Self::InvalidPattern(err) => err.fmt(f),
190        }
191    }
192}
193
194impl std::error::Error for StepPatternError {
195    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
196        match self {
197            Self::PlaceholderSyntax(err) => Some(err),
198            Self::InvalidPattern(err) => Some(err),
199        }
200    }
201}
202
203/// Error conditions that may arise when extracting placeholders.
204#[derive(Debug, Clone, PartialEq, Eq)]
205#[non_exhaustive]
206pub enum PlaceholderError {
207    /// The supplied text did not match the step pattern.
208    PatternMismatch,
209    /// The step pattern contained invalid placeholder syntax.
210    InvalidPlaceholder(String),
211    /// The step pattern could not be compiled into a regular expression.
212    InvalidPattern(String),
213}
214
215impl fmt::Display for PlaceholderError {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        let message = match self {
218            Self::PatternMismatch => localization::message("placeholder-pattern-mismatch"),
219            Self::InvalidPlaceholder(details) => {
220                localization::message_with_args("placeholder-invalid-placeholder", |args| {
221                    args.set("details", details.clone());
222                })
223            }
224            Self::InvalidPattern(pattern) => {
225                localization::message_with_args("placeholder-invalid-pattern", |args| {
226                    args.set("pattern", pattern.clone());
227                })
228            }
229        };
230        f.write_str(&message)
231    }
232}
233
234impl std::error::Error for PlaceholderError {}
235
236impl From<StepPatternError> for PlaceholderError {
237    fn from(e: StepPatternError) -> Self {
238        match e {
239            StepPatternError::PlaceholderSyntax(err) => {
240                Self::InvalidPlaceholder(err.user_message())
241            }
242            StepPatternError::InvalidPattern(err) => Self::InvalidPattern(err.to_string()),
243        }
244    }
245}
246
247/// Outcome produced by step wrappers.
248#[derive(Debug)]
249#[must_use]
250pub enum StepExecution {
251    /// The step executed successfully and may provide a value for later steps.
252    Continue {
253        /// Value returned by the step, made available to later fixtures.
254        value: Option<Box<dyn Any>>,
255    },
256    /// The step requested that the scenario should be skipped.
257    Skipped {
258        /// Optional reason describing why execution stopped.
259        message: Option<String>,
260    },
261}
262
263impl StepExecution {
264    /// Construct a successful outcome with an optional value.
265    pub fn from_value(value: Option<Box<dyn Any>>) -> Self {
266        Self::Continue { value }
267    }
268
269    /// Construct a skipped outcome with an optional reason.
270    pub fn skipped(message: impl Into<Option<String>>) -> Self {
271        Self::Skipped {
272            message: message.into(),
273        }
274    }
275}
276
277/// Declares how a step prefers to execute when both sync and async runtimes
278/// are available.
279///
280/// The registry stores both `run` and `run_async` pointers for ergonomic
281/// backwards compatibility. This enum records whether a step has a native async
282/// body, a native sync body, or can run efficiently in both contexts.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284#[expect(
285    clippy::exhaustive_enums,
286    reason = "public API; execution modes may expand over time"
287)]
288pub enum StepExecutionMode {
289    /// Step body is synchronous and should prefer the sync handler.
290    Sync,
291    /// Step body is asynchronous (`async fn`) and should prefer the async handler.
292    Async,
293    /// Step body can execute efficiently in both sync and async contexts.
294    Both,
295}
296
297/// Type alias for the stored step function pointer.
298pub type StepFn = for<'a> fn(
299    &mut crate::context::StepContext<'a>,
300    &str,
301    Option<&str>,
302    Option<&[&[&str]]>,
303) -> Result<StepExecution, crate::StepError>;
304
305/// A boxed future returned by async step wrappers.
306///
307/// The lifetime `'a` ties the future to the borrowed [`StepContext`], allowing
308/// the future to hold references to fixtures. The future is `!Send` to support
309/// Tokio current-thread mode without requiring synchronization primitives for
310/// mutable fixtures.
311///
312/// [`StepContext`]: crate::context::StepContext
313pub type StepFuture<'a> =
314    Pin<Box<dyn Future<Output = Result<StepExecution, crate::StepError>> + 'a>>;
315
316/// Function pointer type for async step wrappers.
317///
318/// Async step definitions are normalised into this interface by the
319/// macro-generated wrapper code. Sync step definitions are wrapped in
320/// immediately-ready futures when async mode is enabled, allowing mixed sync
321/// and async steps within a single scenario.
322///
323/// Most user code does not need to name this type directly. Prefer:
324///
325/// - `StepContext<'_>` in parameter positions so `'fixtures` is inferred.
326/// - [`crate::async_step::sync_to_async`] for explicit sync-to-async wrappers.
327/// - [`StepCtx`], [`StepTextRef`], [`StepDoc`], and [`StepTable`] for concise
328///   explicit signatures.
329///
330/// # Examples
331///
332/// ```rust
333/// use rstest_bdd::async_step::sync_to_async;
334/// use rstest_bdd::{StepContext, StepError, StepExecution, StepFuture};
335///
336/// fn my_sync_step(
337///     _ctx: &mut StepContext<'_>,
338///     _text: &str,
339///     _docstring: Option<&str>,
340///     _table: Option<&[&[&str]]>,
341/// ) -> Result<StepExecution, StepError> {
342///     Ok(StepExecution::from_value(None))
343/// }
344///
345/// fn my_async_step<'ctx>(
346///     ctx: &'ctx mut StepContext<'_>,
347///     text: &'ctx str,
348///     docstring: Option<&'ctx str>,
349///     table: Option<&'ctx [&'ctx [&'ctx str]]>,
350/// ) -> StepFuture<'ctx> {
351///     sync_to_async(my_sync_step)(ctx, text, docstring, table)
352/// }
353/// ```
354pub type AsyncStepFn = for<'ctx, 'fixtures> fn(
355    &'ctx mut crate::context::StepContext<'fixtures>,
356    &'ctx str,
357    Option<&'ctx str>,
358    Option<&'ctx [&'ctx [&'ctx str]]>,
359) -> StepFuture<'ctx>;
360
361/// Alias for the borrowed step context argument in async wrapper signatures.
362///
363/// This preserves the crate's two-lifetime model while shortening explicit
364/// wrapper signatures in user code.
365pub type StepCtx<'ctx, 'fixtures> = &'ctx mut crate::context::StepContext<'fixtures>;
366
367/// Alias for the step text argument in async wrapper signatures.
368///
369/// This alias is named `StepTextRef` to avoid colliding with [`StepText`], the
370/// owned step-text wrapper type used by lookup APIs.
371pub type StepTextRef<'ctx> = &'ctx str;
372
373/// Alias for the optional step docstring argument in async wrapper signatures.
374pub type StepDoc<'ctx> = Option<&'ctx str>;
375
376/// Alias for the optional step table argument in async wrapper signatures.
377pub type StepTable<'ctx> = Option<&'ctx [&'ctx [&'ctx str]]>;
378
379#[cfg(test)]
380mod tests;