1use 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
30pub type Step<World> =
32 for<'a> fn(&'a mut World, Context) -> LocalBoxFuture<'a, ()>;
33
34pub type WithContext<'me, World> = (
37 &'me Step<World>,
38 regex::CaptureLocations,
39 Option<Location>,
40 Context,
41);
42
43pub struct Collection<World> {
47 given: HashMap<(HashableRegex, Option<Location>), Step<World>>,
51
52 when: HashMap<(HashableRegex, Option<Location>), Step<World>>,
56
57 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
94impl<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
106impl<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 #[must_use]
121 pub fn new() -> Self {
122 Self::default()
123 }
124
125 #[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 #[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 #[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 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 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 #[allow(clippy::string_slice)] 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
236pub type CaptureName = Option<String>;
238
239#[derive(Clone, Debug)]
241pub struct Context {
242 pub step: gherkin::Step,
246
247 pub matches: Vec<(CaptureName, String)>,
251}
252
253#[derive(Clone, Debug, Error)]
256pub struct AmbiguousMatchError {
257 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#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
276#[display(fmt = "{}:{}:{}", path, line, column)]
277pub struct Location {
278 pub path: &'static str,
280
281 pub line: u32,
283
284 pub column: u32,
286}
287
288#[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}