-
-
Notifications
You must be signed in to change notification settings - Fork 690
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #350 from cucumber/cr-go2
cucumber-expressions: add go implementation
- Loading branch information
Showing
30 changed files
with
2,302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
48 changes: 48 additions & 0 deletions
48
cucumber-expressions/go/combinatorial_generated_expression_factory.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
76 changes: 76 additions & 0 deletions
76
cucumber-expressions/go/combinatorial_generated_expression_factory_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}", | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
101
cucumber-expressions/go/cucumber_expression_generator.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.