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;