use std::path::PathBuf;
use ferridriver::FerriError;
use ferridriver::error::Result;
pub struct ParsedFeature {
pub path: PathBuf,
pub feature: gherkin::Feature,
}
pub struct FeatureSet {
pub features: Vec<ParsedFeature>,
}
impl FeatureSet {
pub fn discover(patterns: &[String], ignore: &[String]) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for raw_pattern in patterns {
let pattern = if std::path::Path::new(raw_pattern).is_dir() {
let trimmed = raw_pattern.trim_end_matches('/');
format!("{trimmed}/**/*.feature")
} else {
raw_pattern.clone()
};
let entries = glob::glob(&pattern)
.map_err(|e| FerriError::invalid_argument("pattern", format!("invalid glob pattern \"{pattern}\": {e}")))?;
for entry in entries {
match entry {
Ok(path) => {
if path.extension().and_then(|e| e.to_str()) == Some("feature") {
let should_ignore = ignore
.iter()
.any(|ig| glob::Pattern::new(ig).map(|p| p.matches_path(&path)).unwrap_or(false));
if !should_ignore {
files.push(path);
}
}
},
Err(e) => {
tracing::warn!("glob error: {e}");
},
}
}
}
files.sort();
files.dedup();
Ok(files)
}
pub fn parse(files: Vec<PathBuf>) -> Result<Self> {
Self::parse_with_language(files, None)
}
pub fn parse_with_language(files: Vec<PathBuf>, language: Option<&str>) -> Result<Self> {
let mut features = Vec::with_capacity(files.len());
for path in files {
let env = if let Some(lang) = language {
gherkin::GherkinEnv::new(lang)
.map_err(|e| FerriError::unsupported(format!("unsupported language \"{lang}\": {e}")))?
} else {
gherkin::GherkinEnv::default()
};
let mut feature = gherkin::Feature::parse_path(&path, env)
.map_err(|e| FerriError::backend(format!("failed to parse {}: {e}", path.display())))?;
if feature.path.is_none() {
feature.path = Some(path.clone());
}
features.push(ParsedFeature { path, feature });
}
Ok(Self { features })
}
pub fn parse_text(text: &str) -> Result<Self> {
let env = gherkin::GherkinEnv::default();
let feature = gherkin::Feature::parse(text, env)
.map_err(|e| FerriError::backend(format!("failed to parse Gherkin text: {e}")))?;
Ok(Self {
features: vec![ParsedFeature {
path: PathBuf::from("<inline>"),
feature,
}],
})
}
pub fn discover_and_parse(patterns: &[String], ignore: &[String]) -> Result<Self> {
let files = Self::discover(patterns, ignore)?;
if files.is_empty() {
tracing::warn!("no .feature files found matching patterns: {patterns:?}");
}
Self::parse(files)
}
}
pub fn extract_tags(tags: &[String]) -> Vec<String> {
tags
.iter()
.map(|t| if t.starts_with('@') { t.clone() } else { format!("@{t}") })
.collect()
}
pub fn table_to_vec(table: &gherkin::Table) -> crate::data_table::DataTable {
crate::data_table::DataTable::new(table.rows.clone())
}