#[cfg(feature = "parser")]
mod parser;
#[cfg(feature = "parser")]
mod keywords;
#[cfg(feature = "parser")]
pub mod tagexpr;
pub use parser::EnvError;
#[cfg(feature = "parser")]
use typed_builder::TypedBuilder;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::HashSet,
fmt::{self, Display},
path::{Path, PathBuf},
};
pub use parser::GherkinEnv;
#[cfg(feature = "parser")]
pub fn is_language_supported(lang: &str) -> bool {
keywords::Keywords::get(lang).is_some()
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub struct Span {
pub start: usize,
pub end: usize,
}
#[cfg(feature = "juniper")]
#[cfg_attr(feature = "juniper", juniper::graphql_object)]
impl Span {
pub fn start(&self) -> i32 {
self.start as i32
}
pub fn end(&self) -> i32 {
self.end as i32
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub struct LineCol {
pub line: usize,
pub col: usize,
}
#[cfg(feature = "juniper")]
#[cfg_attr(feature = "juniper", juniper::graphql_object)]
impl LineCol {
pub fn line(&self) -> i32 {
self.line as i32
}
pub fn col(&self) -> i32 {
self.col as i32
}
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Background {
pub keyword: String,
pub steps: Vec<Step>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Examples {
pub keyword: String,
pub table: Table,
#[cfg_attr(feature = "parser", builder(default))]
pub tags: Vec<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Feature {
pub keyword: String,
pub name: String,
#[cfg_attr(feature = "parser", builder(default))]
pub description: Option<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub background: Option<Background>,
#[cfg_attr(feature = "parser", builder(default))]
pub scenarios: Vec<Scenario>,
#[cfg_attr(feature = "parser", builder(default))]
pub rules: Vec<Rule>,
#[cfg_attr(feature = "parser", builder(default))]
pub tags: Vec<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
#[cfg_attr(feature = "parser", builder(default))]
#[cfg_attr(feature = "juniper", graphql(skip))]
pub path: Option<PathBuf>,
}
#[cfg(feature = "parser")]
impl Feature {
#[inline]
pub fn parse_path<P: AsRef<Path>>(path: P, env: GherkinEnv) -> Result<Feature, ParseFileError> {
let mut s =
std::fs::read_to_string(path.as_ref()).map_err(|e| ParseFileError::Reading {
path: path.as_ref().to_path_buf(),
source: e,
})?;
if !s.ends_with("\n") {
s.push('\n');
}
let mut feature =
parser::gherkin_parser::feature(&s, &env).map_err(|e| ParseFileError::Parsing {
path: path.as_ref().to_path_buf(),
error: env.last_error.borrow_mut().take(),
source: ParseError {
position: LineCol {
line: e.location.line,
col: e.location.column,
},
expected: e.expected.tokens().collect(),
},
})?;
feature.path = Some(path.as_ref().to_path_buf());
Ok(feature)
}
#[inline]
pub fn parse<S: AsRef<str>>(input: S, env: GherkinEnv) -> Result<Feature, ParseError> {
let input: Cow<'_, str> = match input.as_ref().ends_with("\n") {
true => Cow::Borrowed(input.as_ref()),
false => Cow::Owned(format!("{}\n", input.as_ref())),
};
parser::gherkin_parser::feature(&input, &env).map_err(|e| ParseError {
position: LineCol {
line: e.location.line,
col: e.location.column,
},
expected: e.expected.tokens().collect(),
})
}
}
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)
}
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Rule {
pub keyword: String,
pub name: String,
#[cfg_attr(feature = "parser", builder(default))]
pub background: Option<Background>,
pub scenarios: Vec<Scenario>,
#[cfg_attr(feature = "parser", builder(default))]
pub tags: Vec<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Scenario {
pub keyword: String,
pub name: String,
pub steps: Vec<Step>,
#[cfg_attr(feature = "parser", builder(default))]
pub examples: Option<Examples>,
#[cfg_attr(feature = "parser", builder(default))]
pub tags: Vec<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Step {
pub keyword: String,
pub ty: StepType,
pub value: String,
#[cfg_attr(feature = "parser", builder(default))]
pub docstring: Option<String>,
#[cfg_attr(feature = "parser", builder(default))]
pub table: Option<Table>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
impl Step {
pub fn docstring(&self) -> Option<&String> {
self.docstring.as_ref()
}
pub fn table(&self) -> Option<&Table> {
self.table.as_ref()
}
}
impl Display for Step {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", &self.keyword, &self.value)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum StepType {
Given,
When,
Then,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "parser", derive(TypedBuilder))]
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Table {
pub rows: Vec<Vec<String>>,
#[cfg_attr(feature = "parser", builder(default))]
pub span: Span,
#[cfg_attr(feature = "parser", builder(default))]
pub position: LineCol,
}
impl Table {
pub fn row_width(&self) -> usize {
self.rows
.iter()
.next()
.map(|x| x.len())
.unwrap_or_else(|| 0)
}
}
#[derive(Debug, thiserror::Error)]
#[error("Error at {}:{}: {expected:?}", .position.line, .position.col)]
pub struct ParseError {
position: LineCol,
expected: HashSet<&'static str>,
}
#[derive(Debug, thiserror::Error)]
pub enum ParseFileError {
#[error("Could not read path: {path}")]
Reading {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Could not parse feature file: {path}")]
Parsing {
path: PathBuf,
error: Option<parser::EnvError>,
#[source]
source: ParseError,
},
}