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 go implementation #350

Merged
merged 32 commits into from
Apr 5, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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