use std::sync::Arc;
use crate::expression;
use crate::filter::TagExpression;
use crate::hook::{Hook, HookHandler, HookRegistration, HookRegistry};
use crate::param_type::{CustomParamType, ParameterTypeRegistration, ParameterTypeRegistry};
use crate::step::{MatchError, StepDef, StepHandler, StepLocation, StepMatch, StepParam, StepRegistration};
pub struct StepRegistry {
steps: Vec<StepDef>,
hooks: HookRegistry,
param_types: ParameterTypeRegistry,
}
impl StepRegistry {
pub fn build() -> Self {
let mut registry = Self {
steps: Vec::new(),
hooks: HookRegistry::new(),
param_types: ParameterTypeRegistry::new(),
};
for reg in inventory::iter::<ParameterTypeRegistration> {
let transformer = reg.transformer_factory.map(|f| f());
registry.param_types.register(CustomParamType {
name: reg.name.to_string(),
regex: reg.regex.to_string(),
transformer,
});
}
for reg in inventory::iter::<StepRegistration> {
if reg.is_regex {
match regex::Regex::new(reg.expression) {
Ok(regex) => {
let num_groups = regex.captures_len().saturating_sub(1);
registry.steps.push(StepDef {
kind: reg.kind,
expression: reg.expression.to_string(),
regex,
param_types: vec![expression::ParamType::Word; num_groups],
param_infos: (0..num_groups)
.map(|i| expression::ParamInfo {
ty: expression::ParamType::Word,
id: i,
})
.collect(),
handler: (reg.handler_factory)(),
location: StepLocation {
file: reg.file,
line: reg.line,
},
});
},
Err(e) => {
tracing::error!(
"failed to compile regex step \"{}\" at {}:{}: {}",
reg.expression,
reg.file,
reg.line,
e
);
},
}
} else {
match expression::compile_with_custom(reg.expression, ®istry.param_types) {
Ok(compiled) => {
registry.steps.push(StepDef {
kind: reg.kind,
expression: reg.expression.to_string(),
regex: compiled.regex,
param_types: compiled.param_types,
param_infos: compiled.param_infos,
handler: (reg.handler_factory)(),
location: StepLocation {
file: reg.file,
line: reg.line,
},
});
},
Err(e) => {
tracing::error!(
"failed to compile step expression \"{}\" at {}:{}: {}",
reg.expression,
reg.file,
reg.line,
e
);
},
}
}
}
for reg in inventory::iter::<HookRegistration> {
let tag_filter = reg.tag_filter.as_ref().and_then(|s| {
TagExpression::parse(s)
.map_err(|e| {
tracing::error!(
"failed to parse hook tag filter \"{}\" at {}:{}: {}",
s,
reg.file,
reg.line,
e
);
})
.ok()
});
registry.hooks.register(Hook {
point: reg.point,
tag_filter,
order: reg.order,
handler: (reg.handler_factory)(),
location: StepLocation {
file: reg.file,
line: reg.line,
},
});
}
registry
}
pub fn register_step(&mut self, def: StepDef) {
self.steps.push(def);
}
pub fn register(
&mut self,
kind: crate::step::StepKind,
expr: &str,
handler: StepHandler,
location: StepLocation,
) -> ferridriver::error::Result<()> {
let compiled = expression::compile_with_custom(expr, &self.param_types)?;
self.steps.push(StepDef {
kind,
expression: expr.to_string(),
regex: compiled.regex,
param_types: compiled.param_types,
param_infos: compiled.param_infos,
handler,
location,
});
Ok(())
}
pub fn register_regex(
&mut self,
kind: crate::step::StepKind,
pattern: &str,
handler: StepHandler,
location: StepLocation,
) -> ferridriver::error::Result<()> {
let regex = regex::Regex::new(pattern)
.map_err(|e| ferridriver::FerriError::invalid_argument("regex", format!("invalid regex \"{pattern}\": {e}")))?;
let num_groups = regex.captures_len().saturating_sub(1);
self.steps.push(StepDef {
kind,
expression: pattern.to_string(),
regex,
param_types: vec![expression::ParamType::Word; num_groups],
param_infos: (0..num_groups)
.map(|i| expression::ParamInfo {
ty: expression::ParamType::Word,
id: i,
})
.collect(),
handler,
location,
});
Ok(())
}
pub fn register_param_type(&mut self, param_type: CustomParamType) {
self.param_types.register(param_type);
}
pub fn hooks(&self) -> &HookRegistry {
&self.hooks
}
pub fn hooks_mut(&mut self) -> &mut HookRegistry {
&mut self.hooks
}
pub fn find_match(&self, text: &str) -> Result<StepMatch<'_>, MatchError> {
let mut first_match: Option<(&StepDef, Vec<StepParam>)> = None;
for def in &self.steps {
if let Some(captures) = def.regex.captures(text) {
match expression::extract_params_with_custom(
&captures,
&def.param_types,
&def.param_infos,
Some(&self.param_types),
) {
Ok(params) => {
if let Some((first_def, _)) = &first_match {
let mut all = vec![(first_def.location.clone(), first_def.expression.clone())];
all.push((def.location.clone(), def.expression.clone()));
for remaining in self
.steps
.iter()
.skip(self.steps.iter().position(|d| std::ptr::eq(d, def)).unwrap_or(0) + 1)
{
if let Some(caps) = remaining.regex.captures(text) {
if expression::extract_params_with_custom(
&caps,
&remaining.param_types,
&remaining.param_infos,
Some(&self.param_types),
)
.is_ok()
{
all.push((remaining.location.clone(), remaining.expression.clone()));
}
}
}
tracing::warn!(target: "ferridriver::bdd::step", text, count = all.len(), "step AMBIGUOUS");
return Err(MatchError::Ambiguous {
text: text.to_string(),
matches: all.iter().map(|(loc, _)| loc.clone()).collect(),
expressions: all.iter().map(|(_, expr)| expr.clone()).collect(),
});
}
first_match = Some((def, params));
},
Err(_) => continue,
}
}
}
match first_match {
Some((def, params)) => {
tracing::debug!(target: "ferridriver::bdd::step", text, expression = def.expression, "step matched");
Ok(StepMatch { def, params })
},
None => {
tracing::debug!(target: "ferridriver::bdd::step", text, "step UNDEFINED — no match");
Err(MatchError::Undefined {
text: text.to_string(),
suggestions: self.suggest(text),
})
},
}
}
pub fn steps(&self) -> &[StepDef] {
&self.steps
}
fn suggest(&self, text: &str) -> Vec<String> {
let text_lower = text.to_lowercase();
let words: Vec<&str> = text_lower.split_whitespace().collect();
let mut scored: Vec<(usize, &str)> = self
.steps
.iter()
.map(|def| {
let expr_lower = def.expression.to_lowercase();
let score = words.iter().filter(|w| expr_lower.contains(**w)).count();
(score, def.expression.as_str())
})
.filter(|(score, _)| *score > 0)
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0));
scored.truncate(5);
scored.into_iter().map(|(_, expr)| expr.to_string()).collect()
}
pub fn reference(&self) -> String {
let mut output = String::from("# Step Reference\n\n");
for kind in &[
crate::step::StepKind::Given,
crate::step::StepKind::When,
crate::step::StepKind::Then,
crate::step::StepKind::Step,
] {
let steps: Vec<&StepDef> = self.steps.iter().filter(|s| s.kind == *kind).collect();
if steps.is_empty() {
continue;
}
output.push_str(&format!("## {kind}\n\n"));
for step in steps {
output.push_str(&format!("- `{}`\n", step.expression));
}
output.push('\n');
}
output
}
}