pub extern crate pest;
#[macro_use]
extern crate pest_derive;
#[macro_use]
extern crate derive_builder;
mod parser;
use pest::iterators::{Pair, Pairs};
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Background {
pub steps: Vec<Step>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Examples {
pub table: Table,
#[builder(default)]
pub tags: Option<Vec<String>>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Feature {
pub name: String,
#[builder(default)]
pub description: Option<String>,
#[builder(default)]
pub background: Option<Background>,
pub scenarios: Vec<Scenario>,
pub rules: Vec<Rule>,
#[builder(default)]
pub tags: Option<Vec<String>>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Rule {
pub name: String,
pub scenarios: Vec<Scenario>,
#[builder(default)]
pub tags: Option<Vec<String>>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Scenario {
pub name: String,
pub steps: Vec<Step>,
#[builder(default)]
pub examples: Option<Examples>,
#[builder(default)]
pub tags: Option<Vec<String>>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Step {
pub ty: StepType,
pub raw_type: String,
pub value: String,
#[builder(default)]
pub docstring: Option<String>,
#[builder(default)]
pub table: Option<Table>,
#[builder(default)]
pub position: (usize, usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub enum StepType {
Given,
When,
Then,
}
#[derive(Debug, Clone, Builder, PartialEq, Hash, Eq)]
pub struct Table {
pub header: Vec<String>,
pub rows: Vec<Vec<String>>,
#[builder(default)]
pub position: (usize, usize),
}
impl StepType {
pub fn as_str(&self) -> &str {
match self {
StepType::Given => "Given",
StepType::When => "When",
StepType::Then => "Then",
}
}
}
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,
}
}
pub fn to_string(&self) -> String {
format!("{} {}", &self.raw_type, &self.value)
}
}
fn parse_tags(outer_rule: Pair<'_, parser::Rule>) -> Vec<String> {
let mut tags = vec![];
for rule in outer_rule.into_inner() {
if rule.as_rule() == parser::Rule::tag {
let tag = rule.as_span().as_str().to_string();
tags.push(tag);
}
}
tags
}
impl<'a> std::convert::TryFrom<&'a str> for Feature {
type Error = Error;
fn try_from(s: &'a str) -> Result<Feature, Error> {
use parser::*;
use pest::Parser;
let mut pairs = FeatureParser::parse(Rule::main, &s)?;
let pair = pairs.next().expect("pair to exist");
let inner_pair = pair.into_inner().next().expect("feature to exist");
Ok(Feature::from(inner_pair))
}
}
#[derive(Debug)]
pub enum TryFromError {
Parsing(Error),
Reading(std::io::Error)
}
impl<'a> std::convert::TryFrom<&'a std::path::Path> for Feature {
type Error = TryFromError;
fn try_from(p: &'a std::path::Path) -> Result<Feature, TryFromError> {
let s = std::fs::read_to_string(p).map_err(|e| TryFromError::Reading(e))?;
Feature::try_from(&*s).map_err(|e| TryFromError::Parsing(e))
}
}
impl StepType {
pub fn new_with_context(s: &str, context: Option<StepType>) -> Self {
match (s, context) {
("Given", _) => StepType::Given,
("When", _) => StepType::When,
("Then", _) => StepType::Then,
("And", Some(v)) => v,
("But", Some(v)) => v,
_ => panic!("Invalid input: {:?}", s),
}
}
}
impl Step {
fn from_rule_with_context(
outer_rule: Pair<'_, parser::Rule>,
context: Option<StepType>,
) -> Self {
let mut builder = StepBuilder::default();
for rule in outer_rule.into_inner() {
match rule.as_rule() {
parser::Rule::step_kw => {
let span = rule.as_span();
let raw_type = span.as_str();
let ty = StepType::new_with_context(raw_type, context);
builder.ty(ty);
builder.position(span.start_pos().line_col());
builder.raw_type(raw_type.to_string());
}
parser::Rule::step_body => {
let value = rule.as_span().as_str().to_string();
builder.value(value);
}
parser::Rule::docstring => {
let r = rule
.into_inner()
.next()
.expect("docstring value")
.as_span()
.as_str();
let r = textwrap::dedent(r);
let docstring = r
.trim_end()
.trim_matches(|c| c == '\r' || c == '\n')
.to_string();
builder.docstring(Some(docstring));
}
parser::Rule::datatable => {
let datatable = Table::from(rule);
builder.table(Some(datatable));
}
parser::Rule::space | parser::Rule::nl | parser::Rule::EOI => (),
_ => panic!("unhandled rule for Step: {:?}", rule),
}
}
builder.build().expect("step to be built")
}
fn vec_from_rule(rule: Pair<'_, parser::Rule>) -> Vec<Step> {
let mut steps: Vec<Step> = vec![];
for pair in rule.into_inner() {
if pair.as_rule() == parser::Rule::step {
let s = Step::from_rule_with_context(pair, steps.last().map(|x| x.ty));
steps.push(s);
}
}
steps
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Rule {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let mut builder = RuleBuilder::default();
let mut scenarios = vec![];
for pair in rule.into_inner() {
match pair.as_rule() {
parser::Rule::rule_name => {
let span = pair.as_span();
builder.name(span.as_str().to_string());
builder.position(span.start_pos().line_col());
}
parser::Rule::scenario => {
let scenario = Scenario::from(pair);
scenarios.push(scenario);
}
parser::Rule::tags => {
let tags = parse_tags(pair);
builder.tags(Some(tags));
}
_ => {}
}
}
builder
.scenarios(scenarios)
.build()
.expect("scenario to be built")
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Background {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let pos = rule.as_span().start_pos().line_col();
Background {
steps: Step::vec_from_rule(rule),
position: pos,
}
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Feature {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let mut builder = FeatureBuilder::default();
let mut scenarios = vec![];
let mut rules = vec![];
for pair in rule.into_inner() {
match pair.as_rule() {
parser::Rule::feature_kw => {
builder.position(pair.as_span().start_pos().line_col());
}
parser::Rule::feature_body => {
builder.name(pair.as_span().as_str().to_string());
}
parser::Rule::feature_description => {
let description = textwrap::dedent(pair.as_span().as_str());
if description == "" {
builder.description(None);
} else {
builder.description(Some(description));
}
}
parser::Rule::background => {
builder.background(Some(Background::from(pair)));
}
parser::Rule::scenario => {
let scenario = Scenario::from(pair);
scenarios.push(scenario);
}
parser::Rule::rule => {
let rule = Rule::from(pair);
rules.push(rule);
}
parser::Rule::tags => {
let tags = parse_tags(pair);
builder.tags(Some(tags));
}
_ => {}
}
}
builder
.scenarios(scenarios)
.rules(rules)
.build()
.expect("feature to be built")
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Table {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let mut builder = TableBuilder::default();
let mut rows = vec![];
builder.position(rule.as_span().start_pos().line_col());
fn row_from_inner(inner: Pairs<'_, parser::Rule>) -> Vec<String> {
let mut rows = vec![];
for pair in inner {
if pair.as_rule() == parser::Rule::table_field {
rows.push(pair.as_span().as_str().trim().to_string());
}
}
rows
}
let mut header = None;
for pair in rule.into_inner() {
if pair.as_rule() == parser::Rule::table_row {
if header.is_none() {
header = Some(row_from_inner(pair.into_inner()));
} else {
rows.push(row_from_inner(pair.into_inner()));
}
}
}
if rows.is_empty() {
rows.push(header.take().unwrap());
}
if let Some(h) = header {
builder.header(h);
} else {
let mut h = Vec::new();
h.resize(rows[0].len(), String::new());
builder.header(h);
}
builder.rows(rows).build().expect("table to be build")
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Examples {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let mut builder = ExamplesBuilder::default();
builder.position(rule.as_span().start_pos().line_col());
for pair in rule.into_inner() {
match pair.as_rule() {
parser::Rule::datatable => {
let table = Table::from(pair);
builder.table(table);
}
parser::Rule::tags => {
let tags = parse_tags(pair);
builder.tags(Some(tags));
}
_ => {}
}
}
builder.build().expect("examples to be built")
}
}
impl<'a> From<Pair<'a, parser::Rule>> for Scenario {
fn from(rule: Pair<'a, parser::Rule>) -> Self {
let mut builder = ScenarioBuilder::default();
for pair in rule.into_inner() {
match pair.as_rule() {
parser::Rule::scenario_name => {
let span = pair.as_span();
builder.name(span.as_str().to_string());
builder.position(span.start_pos().line_col());
}
parser::Rule::scenario_steps => {
builder.steps(Step::vec_from_rule(pair));
}
parser::Rule::examples => {
let examples = Examples::from(pair);
builder.examples(Some(examples));
}
parser::Rule::tags => {
let tags = parse_tags(pair);
builder.tags(Some(tags));
}
_ => {}
}
}
builder.build().expect("scenario to be built")
}
}
pub type Error = pest::error::Error<parser::Rule>;
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_e2e() {
let s = include_str!("./test.feature");
let _f = Feature::try_from(s);
}
}