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-expressions: Add support for alternative text #122

Merged
merged 1 commit into from Mar 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions cucumber-expressions/CHANGELOG.md
Expand Up @@ -11,10 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
N/A

### Added
N/A
* Alternative text: `I have a cat/dog/fish`
(by [aslakhellesoy])

### Changed
* Stricter conflict checks when defining parameters.
* Stricter conflict checks when defining parameters
([#121](https://github.com/cucumber/cucumber/pull/121)
by [aslakhellesoy])

Expand Down
16 changes: 14 additions & 2 deletions cucumber-expressions/README.md
Expand Up @@ -37,7 +37,7 @@ Let's change the parameter to a `float` instead:

Now the expression will match the text, and the number `42.5` is extracted.

## Optional Text
## Optional text

It's grammatically incorrect to say *1 cucumbers*, so we should make that `s`
optional. That can be done by surrounding the optional text with parenthesis:
Expand All @@ -52,15 +52,27 @@ It would also match this text:

I have 42 cucumbers in my belly

## Alternative text

Sometimes you want to relax your language, to make it flow better. For example:

I have {int} cucumber(s) in my belly/stomach

This would match either of those texts:

I have 42 cucumbers in my belly
I have 42 cucumbers in my stomach

## Custom Parameters {#custom-parameters}

Cucumber Expressions have built-in support for `int` and `float` parameter types
as well as other numeric types available in your programming language.

Defining your own parameter types is useful for two reasons:
Defining your own parameter types is useful for several reasons:

1. Enforce certain patterns
1. Convert to custom types
1. Document and evolve your ubiquitous domain language

Imagine we want our parameter to match the colors `red`, `blue` or `yellow`
(but nothing else). Let's assume a `Color` class is already defined.
Expand Down
Expand Up @@ -10,16 +10,26 @@ public class CucumberExpression implements Expression {
private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\\\^\\[$.|?*+\\]])");
private static final Pattern PARAMETER_PATTERN = Pattern.compile("\\{([^}:]+)(:([^}]+))?}");
private static final Pattern OPTIONAL_PATTERN = Pattern.compile("\\(([^)]+)\\)");
private static final Pattern ALTERNATIVE_WORD_REGEXP = Pattern.compile("([\\p{IsAlphabetic}]+)((/[\\p{IsAlphabetic}]+)+)");


private final Pattern pattern;
private final List<Parameter<?>> parameters = new ArrayList<>();
private final String expression;

public CucumberExpression(final String expression, List<? extends Type> types, ParameterRegistry parameterRegistry) {
public CucumberExpression(String expression, List<? extends Type> types, ParameterRegistry parameterRegistry) {
this.expression = expression;
String escapedExpression = ESCAPE_PATTERN.matcher(expression).replaceAll("\\\\$1");
String expressionWithOptionalGroups = OPTIONAL_PATTERN.matcher(escapedExpression).replaceAll("(?:$1)?");
Matcher matcher = PARAMETER_PATTERN.matcher(expressionWithOptionalGroups);
expression = ESCAPE_PATTERN.matcher(expression).replaceAll("\\\\$1");
expression = OPTIONAL_PATTERN.matcher(expression).replaceAll("(?:$1)?");

Matcher m = ALTERNATIVE_WORD_REGEXP.matcher(expression);
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, "(?:" + m.group(1) + m.group(2).replace('/', '|') + ")");
}
m.appendTail(sb);

Matcher matcher = PARAMETER_PATTERN.matcher(sb.toString());

StringBuffer regexp = new StringBuffer();
regexp.append("^");
Expand Down
Expand Up @@ -24,6 +24,15 @@ public void translates_no_args() {
);
}

@Test
public void translates_alternation() {
assertPattern(
"I had/have a great/nice/charming friend",
"^I (?:had|have) a (?:great|nice|charming) friend$",
Collections.<Type>emptyList()
);
}

@Test
public void translates_an_int_arg() {
assertPattern(
Expand Down
21 changes: 12 additions & 9 deletions cucumber-expressions/javascript/src/cucumber_expression.js
Expand Up @@ -6,9 +6,10 @@ class CucumberExpression {
* @param types Array of type name (String) or types (function). Functions can be a regular function or a constructor
* @param parameterRegistry
*/
constructor(expression, types, parameterRegistry) {
const parameterPattern = /\{([^}:]+)(:([^}]+))?}/g
const optionalPattern = /\(([^)]+)\)/g
constructor (expression, types, parameterRegistry) {
const PARAMETER_REGEXP = /\{([^}:]+)(:([^}]+))?}/g
const OPTIONAL_REGEXP = /\(([^)]+)\)/g
const ALTERNATIVE_WORD_REGEXP = /(\w+)((\/\w+)+)/g

this._expression = expression
this._parameters = []
Expand All @@ -21,9 +22,11 @@ class CucumberExpression {
expression = expression.replace(/([\\\^\[$.|?*+])/g, "\\$1")

// Create non-capturing, optional capture groups from parenthesis
expression = expression.replace(optionalPattern, '(?:$1)?')
expression = expression.replace(OPTIONAL_REGEXP, '(?:$1)?')

while ((match = parameterPattern.exec(expression)) !== null) {
expression = expression.replace(ALTERNATIVE_WORD_REGEXP, (_, p1, p2) => `(?:${p1}${p2.replace(/\//g, '|')})`)

while ((match = PARAMETER_REGEXP.exec(expression)) !== null) {
const parameterName = match[1]
const typeName = match[3]
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -51,7 +54,7 @@ class CucumberExpression {

const text = expression.slice(matchOffset, match.index)
const captureRegexp = getCaptureRegexp(parameter.captureGroupRegexps)
matchOffset = parameterPattern.lastIndex
matchOffset = PARAMETER_REGEXP.lastIndex
regexp += text
regexp += captureRegexp
}
Expand All @@ -60,16 +63,16 @@ class CucumberExpression {
this._regexp = new RegExp(regexp)
}

match(text) {
match (text) {
return matchPattern(this._regexp, text, this._parameters)
}

get source() {
get source () {
return this._expression
}
}

function getCaptureRegexp(captureGroupRegexps) {
function getCaptureRegexp (captureGroupRegexps) {
if (captureGroupRegexps.length === 1) {
return `(${captureGroupRegexps[0]})`
}
Expand Down
Expand Up @@ -12,6 +12,13 @@ describe(CucumberExpression.name, () => {
)
})

it("translates alternation", () => {
assertRegexp(
"I had/have a great/nice/charming friend",
/^I (?:had|have) a (?:great|nice|charming) friend$/
)
})

it("translates two untyped arguments", () => {
assertRegexp(
"I have {n} cukes in my {bodypart} now",
Expand Down
Expand Up @@ -4,8 +4,9 @@
module Cucumber
module CucumberExpressions
class CucumberExpression
PARAMETER_PATTERN = /\{([^}:]+)(:([^}]+))?}/
OPTIONAL_PATTERN = /\(([^)]+)\)/
PARAMETER_REGEXP = /\{([^}:]+)(:([^}]+))?}/
OPTIONAL_REGEXP = /\(([^)]+)\)/
ALTERNATIVE_WORD_REGEXP = /([[:alpha:]]+)((\/[[:alpha:]]+)+)/

attr_reader :source

Expand All @@ -14,17 +15,20 @@ def initialize(expression, types, parameter_registry)
@parameters = []
regexp = "^"
type_index = 0
match = nil
match_offset = 0

# Does not include (){} because they have special meaning
# Escape Does not include (){} because they have special meaning
expression = expression.gsub(/([\\\^\[$.|?*+\]])/, '\\\\\1')

# Create non-capturing, optional capture groups from parenthesis
expression = expression.gsub(OPTIONAL_PATTERN, '(?:\1)?')
expression = expression.gsub(OPTIONAL_REGEXP, '(?:\1)?')

expression = expression.gsub(ALTERNATIVE_WORD_REGEXP) do |_|
"(?:#{$1}#{$2.tr('/', '|')})"
end

loop do
match = PARAMETER_PATTERN.match(expression, match_offset)
match = PARAMETER_REGEXP.match(expression, match_offset)
break if match.nil?

parameter_name = match[1]
Expand Down
Expand Up @@ -17,6 +17,13 @@ def assert_regexp(expression, regexp)
)
end

it "translates alternation" do
assert_regexp(
"I had/have a great/nice/charming friend",
/^I (?:had|have) a (?:great|nice|charming) friend$/
)
end

it "translates two untyped arguments" do
assert_regexp(
"I have {n} cukes in my {bodypart} now",
Expand Down