Skip to content

Commit

Permalink
Merge pull request #350 from cucumber/cr-go2
Browse files Browse the repository at this point in the history
cucumber-expressions: add go implementation
  • Loading branch information
aslakhellesoy authored Apr 5, 2018
2 parents 05e4544 + f25d0e2 commit 0457ad3
Show file tree
Hide file tree
Showing 30 changed files with 2,302 additions and 0 deletions.
19 changes: 19 additions & 0 deletions cucumber-expressions/go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
SHELL := /usr/bin/env bash
export GOPATH = $(realpath ./lib)

default: test
.PHONY: default

# Use env variable ARGS to pass arguments to 'go test'
# (for running only a specific test or using verbose mode)
# Example: ARGS='-v -run TestCucumberExpression' make test
test: lib/src/github.com/stretchr/testify
go test ${ARGS}
.PHONY: clean

lib/src/github.com/stretchr/testify:
go get github.com/stretchr/testify

clean:
rm -rf lib/src lib/pkg
.PHONY: clean
43 changes: 43 additions & 0 deletions cucumber-expressions/go/argument.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cucumberexpressions

import "fmt"

type Argument struct {
group *Group
parameterType *ParameterType
}

func BuildArguments(treeRegexp *TreeRegexp, text string, parameterTypes []*ParameterType) []*Argument {
group := treeRegexp.Match(text)
if group == nil {
return nil
}
argGroups := group.Children()
if len(argGroups) != len(parameterTypes) {
panic(fmt.Errorf("%s has %d capture groups (%v), but there were %d parameter types (%v)", treeRegexp.Regexp().String(), len(argGroups), argGroups, len(parameterTypes), parameterTypes))
}
arguments := make([]*Argument, len(parameterTypes))
for i, parameterType := range parameterTypes {
arguments[i] = NewArgument(argGroups[i], parameterType)
}
return arguments
}

func NewArgument(group *Group, parameterType *ParameterType) *Argument {
return &Argument{
group: group,
parameterType: parameterType,
}
}

func (a *Argument) Group() *Group {
return a.group
}

func (a *Argument) GetValue() interface{} {
return a.parameterType.Transform(a.group.Values())
}

func (a *Argument) ParameterType() *ParameterType {
return a.parameterType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cucumberexpressions

type CombinatorialGeneratedExpressionFactory struct {
expressionTemplate string
parameterTypeCombinations [][]*ParameterType
}

func NewCombinatorialGeneratedExpressionFactory(expressionTemplate string, parameterTypeCombinations [][]*ParameterType) *CombinatorialGeneratedExpressionFactory {
return &CombinatorialGeneratedExpressionFactory{
expressionTemplate: expressionTemplate,
parameterTypeCombinations: parameterTypeCombinations,
}
}

func (c *CombinatorialGeneratedExpressionFactory) GenerateExpressions() []*GeneratedExpression {
generatedExpressions := &GeneratedExpressionList{}
c.generatePermutations(generatedExpressions, 0, nil)
return generatedExpressions.ToArray()
}

func (c *CombinatorialGeneratedExpressionFactory) generatePermutations(generatedExpressions *GeneratedExpressionList, depth int, currentParameterTypes []*ParameterType) {
if depth == len(c.parameterTypeCombinations) {
generatedExpressions.Push(
NewGeneratedExpression(c.expressionTemplate, currentParameterTypes),
)
return
}

for _, parameterType := range c.parameterTypeCombinations[depth] {
c.generatePermutations(
generatedExpressions,
depth+1,
append(currentParameterTypes, parameterType),
)
}
}

type GeneratedExpressionList struct {
elements []*GeneratedExpression
}

func (g *GeneratedExpressionList) Push(expr *GeneratedExpression) {
g.elements = append(g.elements, expr)
}

func (g *GeneratedExpressionList) ToArray() []*GeneratedExpression {
return g.elements
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cucumberexpressions_test

import (
"regexp"
"testing"

cucumberexpressions "."
"github.com/stretchr/testify/require"
)

func TestCombinatorialGeneratedExpressionFactory(t *testing.T) {
t.Run("generates multiple expressions", func(t *testing.T) {
colorParameterType, err := cucumberexpressions.NewParameterType(
"color",
[]*regexp.Regexp{regexp.MustCompile("red|blue|yellow")},
"",
func(arg3 ...string) interface{} { return arg3[0] },
false,
true,
)
require.NoError(t, err)
csscolorParameterType, err := cucumberexpressions.NewParameterType(
"csscolor",
[]*regexp.Regexp{regexp.MustCompile("red|blue|yellow")},
"",
func(arg3 ...string) interface{} { return arg3[0] },
false,
true,
)
require.NoError(t, err)
dateParameterType, err := cucumberexpressions.NewParameterType(
"date",
[]*regexp.Regexp{regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)},
"",
func(arg3 ...string) interface{} { return arg3[0] },
false,
true,
)
require.NoError(t, err)
datetimeParameterType, err := cucumberexpressions.NewParameterType(
"datetime",
[]*regexp.Regexp{regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)},
"",
func(arg3 ...string) interface{} { return arg3[0] },
false,
true,
)
require.NoError(t, err)
timestampParameterType, err := cucumberexpressions.NewParameterType(
"timestamp",
[]*regexp.Regexp{regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)},
"",
func(arg3 ...string) interface{} { return arg3[0] },
false,
true,
)
require.NoError(t, err)
parameterTypeCombinations := [][]*cucumberexpressions.ParameterType{
{colorParameterType, csscolorParameterType},
{dateParameterType, datetimeParameterType, timestampParameterType},
}
factory := cucumberexpressions.NewCombinatorialGeneratedExpressionFactory("I bought a {%s} ball on {%s}", parameterTypeCombinations)
var expressions []string
for _, g := range factory.GenerateExpressions() {
expressions = append(expressions, g.Source())
}
require.Equal(t, expressions, []string{
"I bought a {color} ball on {date}",
"I bought a {color} ball on {datetime}",
"I bought a {color} ball on {timestamp}",
"I bought a {csscolor} ball on {date}",
"I bought a {csscolor} ball on {datetime}",
"I bought a {csscolor} ball on {timestamp}",
})
})
}
98 changes: 98 additions & 0 deletions cucumber-expressions/go/cucumber_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cucumberexpressions

import (
"fmt"
"regexp"
"strings"
)

var ESCAPE_REGEXP = regexp.MustCompile(`([\\^[$.|?*+])`)
var PARAMETER_REGEXP = regexp.MustCompile(`(\\\\\\\\)?{([^}]+)}`)
var OPTIONAL_REGEXP = regexp.MustCompile(`(\\\\\\\\)?\([^)]+\)`)
var ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = regexp.MustCompile(`([^\s^/]+)((\/[^\s^/]+)+)`)
var DOUBLE_ESCAPE = `\\\\`

type CucumberExpression struct {
source string
parameterTypes []*ParameterType
treeRegexp *TreeRegexp
}

func NewCucumberExpression(expression string, parameterTypeRegistry *ParameterTypeRegistry) (*CucumberExpression, error) {
result := &CucumberExpression{source: expression}

expression = result.processEscapes(expression)
expression = result.processOptional(expression)
expression = result.processAlteration(expression)
expression, err := result.processParameters(expression, parameterTypeRegistry)
if err != nil {
return nil, err
}
expression = "^" + expression + "$"

result.treeRegexp = NewTreeRegexp(regexp.MustCompile(expression))
return result, nil
}

func (c *CucumberExpression) Match(text string) ([]*Argument, error) {
return BuildArguments(c.treeRegexp, text, c.parameterTypes), nil
}

func (c *CucumberExpression) Regexp() *regexp.Regexp {
return c.treeRegexp.Regexp()
}

func (c *CucumberExpression) Source() string {
return c.source
}

func (c *CucumberExpression) processEscapes(expression string) string {
return ESCAPE_REGEXP.ReplaceAllString(expression, `\$1`)
}

func (c *CucumberExpression) processOptional(expression string) string {
return OPTIONAL_REGEXP.ReplaceAllStringFunc(expression, func(match string) string {
if strings.HasPrefix(match, DOUBLE_ESCAPE) {
return fmt.Sprintf(`\(%s\)`, match[5:len(match)-1])
}
return fmt.Sprintf("(?:%s)?", match[1:len(match)-1])
})
}

func (c *CucumberExpression) processAlteration(expression string) string {
return ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP.ReplaceAllStringFunc(expression, func(match string) string {
return fmt.Sprintf("(?:%s)", strings.Replace(match, "/", "|", -1))
})
}

func (c *CucumberExpression) processParameters(expression string, parameterTypeRegistry *ParameterTypeRegistry) (string, error) {
var err error
result := PARAMETER_REGEXP.ReplaceAllStringFunc(expression, func(match string) string {
if strings.HasPrefix(match, DOUBLE_ESCAPE) {
return fmt.Sprintf(`\{%s\}`, match[5:len(match)-1])
}

typeName := match[1 : len(match)-1]
parameterType := parameterTypeRegistry.LookupByTypeName(typeName)
if parameterType == nil {
err = NewUndefinedParameterTypeError(typeName)
return match
}
c.parameterTypes = append(c.parameterTypes, parameterType)
return buildCaptureRegexp(parameterType.regexps)
})
return result, err
}

func buildCaptureRegexp(regexps []*regexp.Regexp) string {
if len(regexps) == 1 {
return fmt.Sprintf("(%s)", regexps[0].String())
}

captureGroups := make([]string, len(regexps))
for i, r := range regexps {
captureGroups[i] = fmt.Sprintf("(?:%s)", r.String())
}

return fmt.Sprintf("(%s)", strings.Join(captureGroups, "|"))
}
101 changes: 101 additions & 0 deletions cucumber-expressions/go/cucumber_expression_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cucumberexpressions

import (
"sort"
"strings"
)

type CucumberExpressionGenerator struct {
parameterTypeRegistry *ParameterTypeRegistry
}

func NewCucumberExpressionGenerator(parameterTypeRegistry *ParameterTypeRegistry) *CucumberExpressionGenerator {
return &CucumberExpressionGenerator{
parameterTypeRegistry: parameterTypeRegistry,
}
}

func (c *CucumberExpressionGenerator) GenerateExpressions(text string) []*GeneratedExpression {
parameterTypeCombinations := [][]*ParameterType{}
parameterTypeMatchers := c.createParameterTypeMatchers(text)
expressionTemplate := ""
pos := 0

for {
matchingParameterTypeMatchers := []*ParameterTypeMatcher{}
for _, parameterTypeMatcher := range parameterTypeMatchers {
advancedParameterTypeMatcher := parameterTypeMatcher.AdvanceTo(pos)
if advancedParameterTypeMatcher.Find() {
matchingParameterTypeMatchers = append(matchingParameterTypeMatchers, advancedParameterTypeMatcher)
}
}
if len(matchingParameterTypeMatchers) > 0 {
sort.Slice(matchingParameterTypeMatchers, func(i int, j int) bool {
return CompareParameterTypeMatchers(matchingParameterTypeMatchers[i], matchingParameterTypeMatchers[j]) <= 0
})

// Find all the best parameter type matchers, they are all candidates.
bestParameterTypeMatcher := matchingParameterTypeMatchers[0]
bestParameterTypeMatchers := []*ParameterTypeMatcher{}
for _, parameterTypeMatcher := range matchingParameterTypeMatchers {
if CompareParameterTypeMatchers(parameterTypeMatcher, bestParameterTypeMatcher) == 0 {
bestParameterTypeMatchers = append(bestParameterTypeMatchers, parameterTypeMatcher)
}
}

// Build a list of parameter types without duplicates. The reason there
// might be duplicates is that some parameter types have more than one regexp,
// which means multiple ParameterTypeMatcher objects will have a reference to the
// same ParameterType.
// We're sorting the list so preferential parameter types are listed first.
// Users are most likely to want these, so they should be listed at the top.
parameterTypesMap := map[*ParameterType]bool{}
for _, parameterTypeMatcher := range bestParameterTypeMatchers {
parameterTypesMap[parameterTypeMatcher.parameterType] = true
}
parameterTypes := []*ParameterType{}
for parameterType := range parameterTypesMap {
parameterTypes = append(parameterTypes, parameterType)
}
sort.Slice(parameterTypes, func(i int, j int) bool {
return CompareParameterTypes(parameterTypes[i], parameterTypes[j]) <= 0
})

parameterTypeCombinations = append(parameterTypeCombinations, parameterTypes)
expressionTemplate += escape(text[pos:bestParameterTypeMatcher.Start()]) + "{%s}"
pos = bestParameterTypeMatcher.Start() + len(bestParameterTypeMatcher.Group())
} else {
break
}

if pos > len(text) {
break
}
}
expressionTemplate += escape(text[pos:])
return NewCombinatorialGeneratedExpressionFactory(expressionTemplate, parameterTypeCombinations).GenerateExpressions()
}

func (c *CucumberExpressionGenerator) createParameterTypeMatchers(text string) []*ParameterTypeMatcher {
result := []*ParameterTypeMatcher{}
for _, parameterType := range c.parameterTypeRegistry.ParamaterTypes() {
if parameterType.UseForSnippets() {
result = append(result, c.createParameterTypeMatchers2(parameterType, text)...)
}
}
return result
}

func (c *CucumberExpressionGenerator) createParameterTypeMatchers2(parameterType *ParameterType, text string) []*ParameterTypeMatcher {
result := make([]*ParameterTypeMatcher, len(parameterType.Regexps()))
for i, r := range parameterType.Regexps() {
result[i] = NewParameterTypeMatcher(parameterType, r, text, 0)
}
return result
}

func escape(s string) string {
result := strings.Replace(s, "%", "%%", -1)
result = strings.Replace(result, `(`, `\(`, -1)
return strings.Replace(result, `{`, `\{`, -1)
}
Loading

0 comments on commit 0457ad3

Please sign in to comment.