use std::{
borrow::Cow,
path::{Path, PathBuf},
str::FromStr,
vec,
};
use derive_more::with_trait::{Display, Error};
use futures::stream;
use gherkin::GherkinEnv;
use globwalk::{GlobWalker, GlobWalkerBuilder};
use itertools::Itertools as _;
use super::{Error as ParseError, Parser};
use crate::feature::Ext as _;
#[derive(Clone, Debug, Default, clap::Args)]
#[group(skip)]
pub struct Cli {
#[arg(
id = "input",
long = "input",
short = 'i',
value_name = "glob",
global = true
)]
pub features: Option<Walker>,
}
#[derive(Clone, Debug, Default)]
pub struct Basic {
language: Option<Cow<'static, str>>,
}
impl<I: AsRef<Path>> Parser<I> for Basic {
type Cli = Cli;
type Output =
stream::Iter<vec::IntoIter<Result<gherkin::Feature, ParseError>>>;
fn parse(self, input: I, cli: Self::Cli) -> Self::Output {
let walk = |walker: GlobWalker| {
walker
.filter_map(Result::ok)
.sorted_by(|l, r| Ord::cmp(l.path(), r.path()))
.map(|file| {
let env = self
.language
.as_ref()
.and_then(|l| GherkinEnv::new(l).ok())
.unwrap_or_default();
gherkin::Feature::parse_path(file.path(), env)
})
.collect::<Vec<_>>()
};
let get_features_path = || {
let path = input.as_ref();
path.canonicalize()
.or_else(|_| {
let buf = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
path.strip_prefix("/")
.or_else(|_| path.strip_prefix("./"))
.unwrap_or(path),
);
buf.as_path().canonicalize()
})
.map_err(|e| gherkin::ParseFileError::Reading {
path: path.to_path_buf(),
source: e,
})
};
let features = || {
let features = if let Some(walker) = cli.features {
walk(globwalk::glob(walker.0).unwrap_or_else(|e| {
unreachable!("invalid glob pattern: {e}")
}))
} else {
let feats_path = match get_features_path() {
Ok(p) => p,
Err(e) => return vec![Err(e.into())],
};
if feats_path.is_file() {
let env = self
.language
.as_ref()
.and_then(|l| GherkinEnv::new(l).ok())
.unwrap_or_default();
vec![gherkin::Feature::parse_path(feats_path, env)]
} else {
let w = GlobWalkerBuilder::new(feats_path, "*.feature")
.case_insensitive(true)
.build()
.unwrap_or_else(|e| {
unreachable!("`GlobWalkerBuilder` panicked: {e}")
});
walk(w)
}
};
features
.into_iter()
.map(|f| match f {
Ok(f) => f.expand_examples().map_err(ParseError::from),
Err(e) => Err(e.into()),
})
.collect()
};
stream::iter(features())
}
}
impl Basic {
#[must_use]
pub const fn new() -> Self {
Self { language: None }
}
pub fn language(
mut self,
name: impl Into<Cow<'static, str>>,
) -> Result<Self, UnsupportedLanguageError> {
let name = name.into();
if !gherkin::is_language_supported(&name) {
return Err(UnsupportedLanguageError(name));
}
self.language = Some(name);
Ok(self)
}
}
#[derive(Clone, Debug, Display, Error)]
#[display("Language {_0} isn't supported")]
pub struct UnsupportedLanguageError(
#[error(not(source))] pub Cow<'static, str>,
);
#[derive(Clone, Debug)]
pub struct Walker(String);
impl FromStr for Walker {
type Err = globwalk::GlobError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
globwalk::glob(s).map(|_| Self(s.to_owned()))
}
}