1use 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#[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#[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
76pub use crate::data_table::DataTable;
79
80#[derive(Debug, Clone)]
84pub struct StepError {
85 pub message: String,
86 pub diff: Option<(String, String)>,
87 pub pending: bool,
89}
90
91impl StepError {
92 pub fn pending(message: impl Into<String>) -> Self {
94 Self {
95 message: message.into(),
96 diff: None,
97 pending: true,
98 }
99 }
100
101 #[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
167pub 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#[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
196pub struct StepDef {
200 pub kind: StepKind,
202 pub expression: String,
204 pub regex: Regex,
206 pub param_types: Vec<crate::expression::ParamType>,
208 pub param_infos: Vec<crate::expression::ParamInfo>,
210 pub handler: StepHandler,
212 pub location: StepLocation,
214}
215
216pub struct StepMatch<'a> {
220 pub def: &'a StepDef,
221 pub params: Vec<StepParam>,
222}
223
224#[derive(Debug)]
228pub enum MatchError {
229 Undefined { text: String, suggestions: Vec<String> },
231 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
269pub 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 pub is_regex: bool,
280}
281
282inventory::collect!(StepRegistration);
283
284#[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}