Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cucumber expression AST and parser (#124) #153

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ name = "wait"
harness = false

[workspace]
members = ["codegen"]
members = ["codegen", "expression"]
exclude = ["book/tests"]
6 changes: 3 additions & 3 deletions codegen/src/attribute.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2020 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
// Copyright (c) 2020-2021 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
Expand Down
6 changes: 3 additions & 3 deletions codegen/src/derive.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2020 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
// Copyright (c) 2020-2021 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
Expand Down
6 changes: 3 additions & 3 deletions codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2020 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
// Copyright (c) 2020-2021 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
Expand Down
23 changes: 23 additions & 0 deletions expression/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
`cucumber-expression` changelog
============================

All user visible changes to `cucumber-expression` crate will be documented in this file. This project uses [Semantic Versioning 2.0.0].




## [0.11.0] · 2021-??-??
[0.11.0]: /../../tree/v0.11.0/expression

[Milestone](/../../milestone/3)

### Added

- Cucumber expression AST and parser. ([#153])

[#153]: /../../pull/153




[Semantic Versioning 2.0.0]: https://semver.org
23 changes: 23 additions & 0 deletions expression/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "cucumber-expression"
version = "0.11.0-dev" # should be the same as main crate version
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this crate can be versioned separately.

edition = "2021"
rust-version = "1.56"
description = "Cucumber expression parser."
license = "MIT OR Apache-2.0"
authors = [
"Ilya Solovyiov <[email protected]>",
"Kai Ren <[email protected]>",
]
documentation = "https://docs.rs/cucumber-expression"
homepage = "https://github.com/cucumber-rs/cucumber/tree/main/expression"
repository = "https://github.com/cucumber-rs/cucumber"
readme = "README.md"
categories = ["development-tools::testing", "parser-implementations"]
keywords = ["cucumber", "codegen", "macros"]
exclude = ["/tests/"]

[dependencies]
derive_more = { version = "0.99.16", features = ["as_ref", "deref", "deref_mut", "display", "error"], default_features = false }
nom = "7.0"
nom_locate = "4.0"
29 changes: 29 additions & 0 deletions expression/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
`cucumber-expression` crate
========================

[![Documentation](https://docs.rs/cucumber-expression/badge.svg)](https://docs.rs/cucumber-expression)
[![CI](https://github.com/cucumber-rs/cucumber/workflows/CI/badge.svg?branch=master "CI")](https://github.com/cucumber-rs/cucumber/actions?query=workflow%3ACI+branch%3Amaster)
[![Rust 1.56+](https://img.shields.io/badge/rustc-1.56+-lightgray.svg "Rust 1.56+")](https://blog.rust-lang.org/2021/10/21/Rust-1.56.0.html)
[![Unsafe Forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance)

- [Changelog](https://github.com/cucumber-rs/cucumber/blob/main/expression/CHANGELOG.md)

Parser for [`cucumber expression`].




## License

This project is licensed under either of

* Apache License, Version 2.0 ([LICENSE-APACHE](https://github.com/cucumber-rs/cucumber/blob/main/LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
* MIT license ([LICENSE-MIT](https://github.com/cucumber-rs/cucumber/blob/main/LICENSE-MIT) or <http://opensource.org/licenses/MIT>)

at your option.




[`cucumber`]: https://docs.rs/cucumber
[`cucumber expression`]: https://github.com/cucumber/cucumber-expressions#readme
151 changes: 151 additions & 0 deletions expression/src/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright (c) 2021 Brendan Molloy <[email protected]>,
// Ilya Solovyiov <[email protected]>,
// Kai Ren <[email protected]>
//
// 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.

//! [Cucumber expression][1] [AST][2] definitions.
//!
//! [1]: https://github.com/cucumber/cucumber-expressions#readme
//! [2]: https://en.wikipedia.org/wiki/Abstract_syntax_tree

use derive_more::{AsRef, Deref, DerefMut};
use nom::{error::ErrorKind, Err, InputLength};
use nom_locate::LocatedSpan;

use crate::{parse, Error};

/// A set of meta information about the location of a token.
pub type Spanned<'s> = LocatedSpan<&'s str>;

/// [Cucumber expression][1].
///
/// See [`parse::expression()`] for detailed syntax and examples.
///
/// [1]: https://github.com/cucumber/cucumber-expressions#readme
#[derive(AsRef, Clone, Debug, Deref, DerefMut, Eq, PartialEq)]
pub struct Expression<Input>(pub Vec<SingleExpression<Input>>);

impl<'s> TryFrom<&'s str> for Expression<Spanned<'s>> {
type Error = Error<Spanned<'s>>;

fn try_from(value: &'s str) -> Result<Self, Self::Error> {
parse::expression(Spanned::new(value))
.map_err(|e| match e {
Err::Error(e) | Err::Failure(e) => e,
Err::Incomplete(n) => Error::Needed(n),
})
.and_then(|(rest, parsed)| {
if rest.is_empty() {
Ok(parsed)
} else {
Err(Error::Other(rest, ErrorKind::Verify))
}
})
}
}

impl<'s> Expression<Spanned<'s>> {
/// Tries to `input` into [`Expression`].
///
/// # Errors
///
/// See [`Error`] for more details.
pub fn parse<I: AsRef<str>>(
input: &'s I,
) -> Result<Self, Error<Spanned<'s>>> {
Self::try_from(input.as_ref())
}
}

/// Building block of an [`Expression`].
///
/// See [`parse::single_expression()`] for detailed syntax and examples.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SingleExpression<Input> {
/// [`Alternation`].
Alternation(Alternation<Input>),

/// [`Optional`].
Optional(Optional<Input>),

/// [`Parameter`].
Parameter(Parameter<Input>),

/// Text without whitespaces.
Text(Input),

/// Whitespaces are treated as special case to avoid lookaheads and
/// lookbehinds described in the [`architecture`][1]. This allows parser to
/// have `O(n)` complexity.
///
/// [1]: https://bit.ly/3k8DfcW
Whitespace,
}

/// Allows to match one of [`SingleAlternation`]s.
///
/// See [`parse::alternation()`] for detailed syntax and examples.
#[derive(AsRef, Clone, Debug, Deref, DerefMut, Eq, PartialEq)]
pub struct Alternation<Input>(pub Vec<SingleAlternation<Input>>);

/// Building block an [`Alternation`].
pub type SingleAlternation<Input> = Vec<Alternative<Input>>;

impl<Input: InputLength> Alternation<Input> {
/// Returns length of capture from `Input`.
pub(crate) fn span_len(&self) -> usize {
self.0
.iter()
.flatten()
.map(|alt| match alt {
Alternative::Text(t) => t.input_len(),
Alternative::Optional(opt) => opt.input_len() + 2,
})
.sum::<usize>()
+ self.len()
- 1
}

/// Indicates whether one of [`SingleAlternation`]s consists only from
/// [`Optional`]s.
pub(crate) fn contains_only_optional(&self) -> bool {
for single_alt in &**self {
if single_alt
.iter()
.all(|alt| matches!(alt, Alternative::Optional(_)))
{
return true;
}
}
false
}
}

/// Building block of a [`SingleAlternation`].
///
/// See [`parse::alternative()`] for detailed syntax and examples.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Alternative<Input> {
/// [`Optional`].
Optional(Optional<Input>),

/// Text.
Text(Input),
}

/// Allows to match optional `Input`.
///
/// See [`parse::optional()`] for detailed syntax and examples.
#[derive(AsRef, Clone, Copy, Debug, Deref, DerefMut, Eq, PartialEq)]
pub struct Optional<Input>(pub Input);

/// Allows to match some special `Input` descried by a [`Parameter`] name.
///
/// See [`parse::parameter()`] for detailed syntax and examples.
#[derive(AsRef, Clone, Copy, Debug, Deref, DerefMut, Eq, PartialEq)]
pub struct Parameter<Input>(pub Input);
Loading