use std::{
iter, mem,
path::{Path, PathBuf},
};
use derive_more::{Display, Error};
use once_cell::sync::Lazy;
use regex::Regex;
use sealed::sealed;
use crate::writer::basic::trim_path;
#[sealed]
pub trait Ext: Sized {
fn expand_examples(self) -> Result<Self, ExpandExamplesError>;
#[must_use]
fn count_scenarios(&self) -> usize;
#[must_use]
fn count_steps(&self) -> usize;
}
#[sealed]
impl Ext for gherkin::Feature {
fn expand_examples(mut self) -> Result<Self, ExpandExamplesError> {
let path = self.path.clone();
let expand = |scenarios: Vec<gherkin::Scenario>| -> Result<_, _> {
scenarios
.into_iter()
.flat_map(|s| expand_scenario(s, path.as_ref()))
.collect()
};
for r in &mut self.rules {
r.scenarios = expand(mem::take(&mut r.scenarios))?;
}
self.scenarios = expand(mem::take(&mut self.scenarios))?;
Ok(self)
}
fn count_scenarios(&self) -> usize {
self.scenarios.len()
+ self.rules.iter().map(|r| r.scenarios.len()).sum::<usize>()
}
fn count_steps(&self) -> usize {
self.scenarios.iter().map(|s| s.steps.len()).sum::<usize>()
+ self
.rules
.iter()
.flat_map(|r| &r.scenarios)
.map(|s| s.steps.len())
.sum::<usize>()
}
}
fn expand_scenario(
scenario: gherkin::Scenario,
path: Option<&PathBuf>,
) -> Vec<Result<gherkin::Scenario, ExpandExamplesError>> {
#[allow(clippy::expect_used)]
static TEMPLATE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<([^>\s]+)>").expect("incorrect Regex"));
if scenario.examples.is_empty() {
return vec![Ok(scenario)];
}
scenario
.examples
.iter()
.filter_map(|ex| {
ex.table
.as_ref()?
.rows
.split_first()
.map(|(h, v)| (h, v, ex))
})
.flat_map(|(header, vals, example)| {
vals.iter()
.map(|v| header.iter().zip(v))
.enumerate()
.zip(iter::repeat((example.position, example.tags.iter())))
})
.map(|((id, row), (position, tags))| {
let replace_templates = |str: &str, pos| {
let mut err = None;
let replaced = TEMPLATE_REGEX
.replace_all(str, |cap: ®ex::Captures<'_>| {
#[allow(clippy::unwrap_used)]
let name = cap.get(1).unwrap().as_str();
row.clone()
.find_map(|(k, v)| (name == k).then(|| v.as_str()))
.unwrap_or_else(|| {
err = Some(ExpandExamplesError {
pos,
name: name.to_owned(),
path: path.cloned(),
});
""
})
})
.into_owned();
err.map_or_else(|| Ok(replaced), Err)
};
let mut expanded = scenario.clone();
expanded.position = position;
expanded.position.line += id + 2;
expanded.tags.extend(tags.cloned());
expanded.name =
replace_templates(&expanded.name, expanded.position)?;
for s in &mut expanded.steps {
for value in iter::once(&mut s.value)
.chain(s.docstring.iter_mut())
.chain(s.table.iter_mut().flat_map(|t| {
t.rows.iter_mut().flat_map(|r| r.iter_mut())
}))
{
*value = replace_templates(value, s.position)?;
}
}
Ok(expanded)
})
.collect()
}
#[derive(Clone, Debug, Display, Error)]
#[display(
fmt = "Failed to resolve <{}> at {}:{}:{}",
name,
"path.as_deref().and_then(Path::to_str).map(trim_path).unwrap_or_default()",
"pos.line",
"pos.col"
)]
pub struct ExpandExamplesError {
pub pos: gherkin::LineCol,
pub name: String,
pub path: Option<PathBuf>,
}