Skip to content

Commit

Permalink
cucumber-expressions: Add support for alternative text
Browse files Browse the repository at this point in the history
  • Loading branch information
aslakhellesoy committed Mar 1, 2017
1 parent 3aab706 commit a6f50fb
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 23 deletions.
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

0 comments on commit a6f50fb

Please sign in to comment.