1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
// Copyright (c) 2018-2020 Brendan Molloy <brendan@bbqsrc.net> // // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your // option. This file may not be copied, modified, or distributed // except according to those terms. //! A Gherkin parser for the Cucumber test framework. //! //! It is intended to parse the full gamut of Cucumber .feature files that exist in the wild, //! as there is only a _de facto_ standard for these files. //! //! ### .feature file structure //! //! The basic structure of a feature file is: //! //! - Optionally one or more tags //! - Optionally `#`-prefixed comments on their own line //! - The feature definition //! - An optional description //! - An optional background //! - One or more scenarios (also taggable), each including: //! - One or more steps //! - Optionally data tables or docstrings per step //! - Optionally examples, which can also be tagged //! - One or more rules (also taggable), each including: //! - One or more scenarios //! //! ### Unparsed elements //! //! Indentation and comments are ignored by the parser. Most other things can be accessed via //! properties of the relevant struct. mod parser; pub mod tagexpr; // Re-export for convenience pub use peg::error::ParseError; pub use peg::str::LineCol; use typed_builder::TypedBuilder; use std::path::{Path, PathBuf}; /// A feature background #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Background { /// The parsed steps from the background directive. pub steps: Vec<Step>, /// The `(start, end)` offset the background directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the background directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } /// Examples for a scenario #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Examples { /// The data table from the examples directive. pub table: Table, /// The tags for the examples directive if provided. #[builder(default)] pub tags: Vec<String>, /// The `(start, end)` offset the examples directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the examples directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } /// A feature #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Feature { /// The name of the feature. pub name: String, /// The description of the feature, if found. #[builder(default)] pub description: Option<String>, /// The background of the feature, if found. #[builder(default)] pub background: Option<Background>, /// The scenarios for the feature. #[builder(default)] pub scenarios: Vec<Scenario>, /// The rules for the feature. #[builder(default)] pub rules: Vec<Rule>, /// The tags for the feature if provided. #[builder(default)] pub tags: Vec<String>, /// The `(start, end)` offset the feature directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the feature directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), /// The path supplied for the parsed `Feature`, if known. #[builder(default)] pub path: Option<PathBuf>, } impl PartialOrd for Feature { fn partial_cmp(&self, other: &Feature) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) } } impl Ord for Feature { fn cmp(&self, other: &Feature) -> std::cmp::Ordering { self.name.cmp(&other.name) } } /// A rule, as introduced in Gherkin 6. #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Rule { /// The name of the scenario. pub name: String, /// The parsed scenarios from the rule directive. pub scenarios: Vec<Scenario>, /// The tags for the rule directive if provided. #[builder(default)] pub tags: Vec<String>, /// The `(start, end)` offset the rule directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the rule directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } /// A scenario #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Scenario { /// The name of the scenario. pub name: String, /// The parsed steps from the scenario directive. pub steps: Vec<Step>, // The parsed examples from the scenario directive if found. #[builder(default)] pub examples: Option<Examples>, /// The tags for the scenarios directive if provided. #[builder(default)] pub tags: Vec<String>, /// The `(start, end)` offset the scenario directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the scenario directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } /// A scenario step #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Step { /// The step type for the step after parsed in context. pub ty: StepType, /// The original raw step type, including `But` and `And`. pub raw_type: String, /// The value of the step after the type. pub value: String, /// A docstring, if provided. #[builder(default)] pub docstring: Option<String>, /// A data table, if provided. #[builder(default)] pub table: Option<Table>, /// The `(start, end)` offset the step directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the step directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } /// The fundamental Gherkin step type after contextually handling `But` and `And` #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] pub enum StepType { Given, When, Then, } /// A data table #[derive(Debug, Clone, TypedBuilder, PartialEq, Hash, Eq)] pub struct Table { /// The rows of the data table. Each row is always the same length as the first row. pub rows: Vec<Vec<String>>, /// The `(start, end)` offset the table directive was found in the .feature file. #[builder(default)] pub span: (usize, usize), /// The `(line, col)` position the table directive was found in the .feature file. #[builder(default)] pub position: (usize, usize), } impl Table { pub fn row_width(&self) -> usize { self.rows .iter() .next() .map(|x| x.len()) .unwrap_or_else(|| 0) } } #[derive(Debug, thiserror::Error)] pub enum ParseFileError { #[error("Could not read path: {0}")] Reading(PathBuf, #[source] std::io::Error), #[error("Could not parse feature file: {0}")] Parsing( PathBuf, #[source] peg::error::ParseError<peg::str::LineCol>, ), } impl Feature { #[inline] pub fn parse_path<P: AsRef<Path>>(path: P) -> Result<Feature, ParseFileError> { let s = std::fs::read_to_string(path.as_ref()) .map_err(|e| ParseFileError::Reading(path.as_ref().to_path_buf(), e))?; let mut feature = parser::gherkin_parser::feature(&s, &Default::default()) .map_err(|e| ParseFileError::Parsing(path.as_ref().to_path_buf(), e))?; feature.path = Some(path.as_ref().to_path_buf()); Ok(feature) } #[inline] pub fn parse<S: AsRef<str>>(input: S) -> Result<Feature, ParseError<LineCol>> { parser::gherkin_parser::feature(input.as_ref(), &Default::default()) } } impl Step { pub fn docstring(&self) -> Option<&String> { match &self.docstring { Some(v) => Some(&v), None => None, } } pub fn table(&self) -> Option<&Table> { match &self.table { Some(v) => Some(&v), None => None, } } } impl std::fmt::Display for Step { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} {}", &self.raw_type, &self.value) } }