use std::{
cmp::Ordering,
collections::HashMap,
fmt,
hash::{Hash, Hasher},
iter,
};
use derive_more::{Deref, DerefMut, Display, Error};
use futures::future::LocalBoxFuture;
use gherkin::StepType;
use regex::Regex;
pub type Step<World> =
for<'a> fn(&'a mut World, Context) -> LocalBoxFuture<'a, ()>;
pub type WithContext<'me, World> = (
&'me Step<World>,
regex::CaptureLocations,
Option<Location>,
Context,
);
pub struct Collection<World> {
given: HashMap<(HashableRegex, Option<Location>), Step<World>>,
when: HashMap<(HashableRegex, Option<Location>), Step<World>>,
then: HashMap<(HashableRegex, Option<Location>), Step<World>>,
}
impl<World> fmt::Debug for Collection<World> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Collection")
.field(
"given",
&self
.given
.iter()
.map(|(re, step)| (re, format!("{step:p}")))
.collect::<HashMap<_, _>>(),
)
.field(
"when",
&self
.when
.iter()
.map(|(re, step)| (re, format!("{step:p}")))
.collect::<HashMap<_, _>>(),
)
.field(
"then",
&self
.then
.iter()
.map(|(re, step)| (re, format!("{step:p}")))
.collect::<HashMap<_, _>>(),
)
.finish()
}
}
impl<World> Clone for Collection<World> {
fn clone(&self) -> Self {
Self {
given: self.given.clone(),
when: self.when.clone(),
then: self.then.clone(),
}
}
}
impl<World> Default for Collection<World> {
fn default() -> Self {
Self {
given: HashMap::new(),
when: HashMap::new(),
then: HashMap::new(),
}
}
}
impl<World> Collection<World> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn given(
mut self,
loc: Option<Location>,
regex: Regex,
step: Step<World>,
) -> Self {
let _ = self.given.insert((regex.into(), loc), step);
self
}
#[must_use]
pub fn when(
mut self,
loc: Option<Location>,
regex: Regex,
step: Step<World>,
) -> Self {
let _ = self.when.insert((regex.into(), loc), step);
self
}
#[must_use]
pub fn then(
mut self,
loc: Option<Location>,
regex: Regex,
step: Step<World>,
) -> Self {
let _ = self.then.insert((regex.into(), loc), step);
self
}
pub fn find(
&self,
step: &gherkin::Step,
) -> Result<Option<WithContext<'_, World>>, AmbiguousMatchError> {
let collection = match step.ty {
StepType::Given => &self.given,
StepType::When => &self.when,
StepType::Then => &self.then,
};
let mut captures = collection
.iter()
.filter_map(|((re, loc), step_fn)| {
let mut captures = re.capture_locations();
let names = re.capture_names();
re.captures_read(&mut captures, &step.value)
.map(|m| (re, loc, m, captures, names, step_fn))
})
.collect::<Vec<_>>();
let (_, loc, whole_match, captures, names, step_fn) =
match captures.len() {
0 => return Ok(None),
1 => captures.pop().unwrap_or_else(|| unreachable!()),
_ => {
return Err(AmbiguousMatchError {
possible_matches: captures
.into_iter()
.map(|(re, loc, ..)| (re.clone(), *loc))
.collect(),
})
}
};
#[allow(clippy::string_slice)]
let matches = names
.map(|opt| opt.map(str::to_owned))
.zip(iter::once(whole_match.as_str().to_owned()).chain(
(1..captures.len()).map(|group_id| {
captures
.get(group_id)
.map_or("", |(s, e)| &step.value[s..e])
.to_owned()
}),
))
.collect();
Ok(Some((
step_fn,
captures,
*loc,
Context {
step: step.clone(),
matches,
},
)))
}
}
pub type CaptureName = Option<String>;
#[derive(Clone, Debug)]
pub struct Context {
pub step: gherkin::Step,
pub matches: Vec<(CaptureName, String)>,
}
#[derive(Clone, Debug, Error)]
pub struct AmbiguousMatchError {
pub possible_matches: Vec<(HashableRegex, Option<Location>)>,
}
impl fmt::Display for AmbiguousMatchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Possible matches:")?;
for (reg, loc_opt) in &self.possible_matches {
write!(f, "\n{reg}")?;
if let Some(loc) = loc_opt {
write!(f, " --> {loc}")?;
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[display(fmt = "{}:{}:{}", path, line, column)]
pub struct Location {
pub path: &'static str,
pub line: u32,
pub column: u32,
}
#[derive(Clone, Debug, Deref, DerefMut, Display)]
pub struct HashableRegex(Regex);
impl From<Regex> for HashableRegex {
fn from(re: Regex) -> Self {
Self(re)
}
}
impl Hash for HashableRegex {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.as_str().hash(state);
}
}
impl PartialEq for HashableRegex {
fn eq(&self, other: &Self) -> bool {
self.0.as_str() == other.0.as_str()
}
}
impl Eq for HashableRegex {}
impl PartialOrd for HashableRegex {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.0.as_str().partial_cmp(other.0.as_str())
}
}
impl Ord for HashableRegex {
fn cmp(&self, other: &Self) -> Ordering {
self.0.as_str().cmp(other.0.as_str())
}
}