1use std::{
14 borrow::Cow,
15 path::{Path, PathBuf},
16 str::FromStr,
17 vec,
18};
19
20use derive_more::{Display, Error};
21use futures::stream;
22use gherkin::GherkinEnv;
23use globwalk::{GlobWalker, GlobWalkerBuilder};
24use itertools::Itertools as _;
25
26use crate::feature::Ext as _;
27
28use super::{Error as ParseError, Parser};
29
30#[derive(clap::Args, Clone, Debug, Default)]
32#[group(skip)]
33pub struct Cli {
34 #[arg(
37 id = "input",
38 long = "input",
39 short = 'i',
40 value_name = "glob",
41 global = true
42 )]
43 pub features: Option<Walker>,
44}
45
46#[derive(Clone, Debug, Default)]
51pub struct Basic {
52 language: Option<Cow<'static, str>>,
56}
57
58impl<I: AsRef<Path>> Parser<I> for Basic {
59 type Cli = Cli;
60
61 type Output =
62 stream::Iter<vec::IntoIter<Result<gherkin::Feature, ParseError>>>;
63
64 fn parse(self, path: I, cli: Self::Cli) -> Self::Output {
65 let walk = |walker: GlobWalker| {
66 walker
67 .filter_map(Result::ok)
68 .sorted_by(|l, r| Ord::cmp(l.path(), r.path()))
69 .map(|file| {
70 let env = self
71 .language
72 .as_ref()
73 .and_then(|l| GherkinEnv::new(l).ok())
74 .unwrap_or_default();
75 gherkin::Feature::parse_path(file.path(), env)
76 })
77 .collect::<Vec<_>>()
78 };
79
80 let get_features_path = || {
81 let path = path.as_ref();
82 path.canonicalize()
83 .or_else(|_| {
84 let mut buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
85 buf.push(
86 path.strip_prefix("/")
87 .or_else(|_| path.strip_prefix("./"))
88 .unwrap_or(path),
89 );
90 buf.as_path().canonicalize()
91 })
92 .map_err(|e| gherkin::ParseFileError::Reading {
93 path: path.to_path_buf(),
94 source: e,
95 })
96 };
97
98 let features = || {
99 let features = if let Some(walker) = cli.features {
100 walk(globwalk::glob(walker.0).unwrap_or_else(|e| {
101 unreachable!("Invalid glob pattern: {e}")
102 }))
103 } else {
104 let feats_path = match get_features_path() {
105 Ok(p) => p,
106 Err(e) => return vec![Err(e.into())],
107 };
108
109 if feats_path.is_file() {
110 let env = self
111 .language
112 .as_ref()
113 .and_then(|l| GherkinEnv::new(l).ok())
114 .unwrap_or_default();
115 vec![gherkin::Feature::parse_path(feats_path, env)]
116 } else {
117 let w = GlobWalkerBuilder::new(feats_path, "*.feature")
118 .case_insensitive(true)
119 .build()
120 .unwrap_or_else(|e| {
121 unreachable!("`GlobWalkerBuilder` panicked: {e}")
122 });
123 walk(w)
124 }
125 };
126
127 features
128 .into_iter()
129 .map(|f| match f {
130 Ok(f) => f.expand_examples().map_err(ParseError::from),
131 Err(e) => Err(e.into()),
132 })
133 .collect()
134 };
135
136 stream::iter(features())
137 }
138}
139
140impl Basic {
141 #[must_use]
143 pub const fn new() -> Self {
144 Self { language: None }
145 }
146
147 pub fn language(
154 mut self,
155 name: impl Into<Cow<'static, str>>,
156 ) -> Result<Self, UnsupportedLanguageError> {
157 let name = name.into();
158 if !gherkin::is_language_supported(&name) {
159 return Err(UnsupportedLanguageError(name));
160 }
161 self.language = Some(name);
162 Ok(self)
163 }
164}
165
166#[derive(Clone, Debug, Display, Error)]
168#[display(fmt = "Language {} isn't supported", _0)]
169pub struct UnsupportedLanguageError(
170 #[error(not(source))] pub Cow<'static, str>,
171);
172
173#[derive(Clone, Debug)]
175pub struct Walker(String);
176
177impl FromStr for Walker {
178 type Err = globwalk::GlobError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
181 globwalk::glob(s).map(|_| Self(s.to_owned()))
182 }
183}