cucumber/parser/
basic.rs

1// Copyright (c) 2018-2024  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Default [`Parser`] implementation.
12
13use 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/// CLI options of a [`Basic`] [`Parser`].
31#[derive(clap::Args, Clone, Debug, Default)]
32#[group(skip)]
33pub struct Cli {
34    /// Glob pattern to look for feature files with. By default, looks for
35    /// `*.feature`s in the path configured tests runner.
36    #[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/// Default [`Parser`].
47///
48/// As there is no async runtime-agnostic way to interact with IO, this
49/// [`Parser`] is blocking.
50#[derive(Clone, Debug, Default)]
51pub struct Basic {
52    /// Optional custom language of [`gherkin`] keywords.
53    ///
54    /// Default is English.
55    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    /// Creates a new [`Basic`] [`Parser`].
142    #[must_use]
143    pub const fn new() -> Self {
144        Self { language: None }
145    }
146
147    /// Sets the provided language to parse [`gherkin`] files with instead of
148    /// the default one (English).
149    ///
150    /// # Errors
151    ///
152    /// If the provided language isn't supported.
153    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/// Error of [`gherkin`] not supporting keywords in some language.
167#[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/// Wrapper over [`GlobWalker`] implementing a [`FromStr`].
174#[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}