Repository: go-bdd/gobdd (go)

Issue:
Support data tables
Would be nice to support data tables:

https://cucumber.io/docs/gherkin/reference/#data-tables

Pull request description:
feat: Add support for step data tables
fixes #111 

Patch:
diff --git a/features/datatable.feature b/features/datatable.feature
new file mode 100644
index 0000000..24e25f7
--- /dev/null
+++ b/features/datatable.feature
@@ -0,0 +1,12 @@
+Feature: dataTable feature
+  Scenario: compare text with dataTable
+    When I concat all the columns and row together using " - " to separate the columns
+      |r1c1|r1c2|r1c3|
+      |r2c1|r2c2|r2c3|
+      |r3c1|r3c2|r3c3|
+    Then the result should equal argument:
+      """
+      r1c1 - r1c2 - r1c3
+      r2c1 - r2c2 - r2c3
+      r3c1 - r3c2 - r3c3
+      """
\ No newline at end of file
diff --git a/gobdd.go b/gobdd.go
index aab3987..407035c 100644
--- a/gobdd.go
+++ b/gobdd.go
@@ -572,9 +572,17 @@ func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.Step) {
 		t.Fatalf("cannot find step definition for step: %s%s", step.Keyword, step.Text)
 	}
 
-	params := def.expr.FindSubmatch([]byte(step.Text))[1:]
+	matches := def.expr.FindSubmatch([]byte(step.Text))[1:]
+	params := make([]interface{}, 0, len(matches)) // defining the slices capacity instead of the length to use append
+	for _, m := range matches {
+		params = append(params, m)
+	}
+
 	if step.DocString != nil {
-		params = append(params, []byte(step.DocString.Content))
+		params = append(params, step.DocString.Content)
+	}
+	if step.DataTable != nil {
+		params = append(params, *step.DataTable)
 	}
 
 	t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(step.Keyword), step.Text), func(t *testing.T) {
@@ -589,7 +597,7 @@ func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.Step) {
 	})
 }
 
-func (def *stepDef) run(ctx Context, t TestingT, params [][]byte) { // nolint:interfacer
+func (def *stepDef) run(ctx Context, t TestingT, params []interface{}) { // nolint:interfacer
 	defer func() {
 		if r := recover(); r != nil {
 			t.Errorf("%+v", r)
@@ -611,38 +619,98 @@ func (def *stepDef) run(ctx Context, t TestingT, params [][]byte) { // nolint:in
 		}
 
 		inType := d.Type().In(i + 2)
-		paramType := paramType(v, inType)
+
+		paramType, err := paramType(v, inType)
+		if err != nil {
+			t.Fatal(err)
+		}
+
 		in = append(in, paramType)
 	}
 
 	d.Call(in)
 }
 
-func paramType(param []byte, inType reflect.Type) reflect.Value {
-	paramType := reflect.ValueOf(param)
-	if inType.Kind() == reflect.String {
-		paramType = reflect.ValueOf(string(paramType.Interface().([]uint8)))
+func paramType(param interface{}, inType reflect.Type) (reflect.Value, error) {
+	switch inType.Kind() { // nolint:exhaustive - the linter does not recognize 'default:' to satisfy exhaustiveness
+	case reflect.String:
+		s, err := shouldBeString(param)
+		return reflect.ValueOf(s), err
+	case reflect.Int:
+		v, err := shouldBeInt(param)
+		return reflect.ValueOf(v), err
+	case reflect.Float32:
+		v, err := shouldBeFloat(param, 32)
+		return reflect.ValueOf(float32(v)), err
+	case reflect.Float64:
+		v, err := shouldBeFloat(param, 64)
+		return reflect.ValueOf(v), err
+	case reflect.Slice:
+		// only []byte is supported
+		if inType != reflect.TypeOf([]byte(nil)) {
+			return reflect.Value{}, fmt.Errorf("the slice argument type %s is not supported", inType.Kind())
+		}
+
+		v, err := shouldBeByteSlice(param)
+
+		return reflect.ValueOf(v), err
+	case reflect.Struct:
+		// the only struct supported is the one introduced by cucumber
+		if inType != reflect.TypeOf(msgs.DataTable{}) {
+			return reflect.Value{}, fmt.Errorf("the struct argument type %s is not supported", inType.Kind())
+		}
+
+		v, err := shouldBeDataTable(param)
+
+		return reflect.ValueOf(v), err
+	default:
+		return reflect.Value{}, fmt.Errorf("the type %s is not supported", inType.Kind())
 	}
+}
 
-	if inType.Kind() == reflect.Int {
-		s := paramType.Interface().([]uint8)
-		p, _ := strconv.Atoi(string(s))
-		paramType = reflect.ValueOf(p)
+func shouldBeDataTable(input interface{}) (msgs.DataTable, error) {
+	if v, ok := input.(msgs.DataTable); ok {
+		return v, nil
 	}
 
-	if inType.Kind() == reflect.Float32 {
-		s := paramType.Interface().([]uint8)
-		p, _ := strconv.ParseFloat(string(s), 32)
-		paramType = reflect.ValueOf(float32(p))
+	return msgs.DataTable{}, fmt.Errorf("cannot convert %v of type %T to messages.DataTable", input, input)
+}
+
+func shouldBeByteSlice(input interface{}) ([]byte, error) {
+	if v, ok := input.([]byte); ok {
+		return v, nil
 	}
 
-	if inType.Kind() == reflect.Float64 {
-		s := paramType.Interface().([]uint8)
-		p, _ := strconv.ParseFloat(string(s), 32)
-		paramType = reflect.ValueOf(p)
+	return nil, fmt.Errorf("cannot convert %v of type %T to []byte", input, input)
+}
+
+func shouldBeInt(input interface{}) (int, error) {
+	s, err := shouldBeString(input)
+	if err != nil {
+		return 0, err
 	}
 
-	return paramType
+	return strconv.Atoi(s)
+}
+
+func shouldBeFloat(input interface{}, bitSize int) (float64, error) {
+	s, err := shouldBeString(input)
+	if err != nil {
+		return 0, err
+	}
+
+	return strconv.ParseFloat(s, bitSize)
+}
+
+func shouldBeString(input interface{}) (string, error) {
+	switch v := input.(type) {
+	case string:
+		return v, nil
+	case []byte:
+		return string(v), nil
+	default:
+		return "", fmt.Errorf("cannot convert %v of type %T to string", input, input)
+	}
 }
 
 func (s *Suite) findStepDef(text string) (stepDef, error) {


Test patch:
diff --git a/gobdd_test.go b/gobdd_test.go
index 46c1366..9bf3cdc 100644
--- a/gobdd_test.go
+++ b/gobdd_test.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"regexp"
+	"strings"
 	"testing"
 
 	msgs "github.com/cucumber/messages/go/v24"
@@ -84,6 +85,14 @@ func TestArguments(t *testing.T) {
 	suite.Run()
 }
 
+func TestDatatable(t *testing.T) {
+	suite := NewSuite(t, WithFeaturesPath("features/datatable.feature"))
+	suite.AddStep(`I concat all the columns and row together using {text} to separate the columns`, concatTable)
+	suite.AddStep(`the result should equal argument:`, checkt)
+
+	suite.Run()
+}
+
 func TestScenarioOutlineExecutesAllTests(t *testing.T) {
 	c := 0
 	suite := NewSuite(t, WithFeaturesPath("features/outline.feature"))
@@ -299,6 +308,20 @@ func concat(_ StepTest, ctx Context, var1, var2 string) {
 	ctx.Set("stringRes", var1+var2)
 }
 
+func concatTable(_ StepTest, ctx Context, separator string, table msgs.DataTable) {
+	rows := make([]string, 0, len(table.Rows))
+	for _, row := range table.Rows {
+		values := make([]string, 0, len(row.Cells))
+		for _, cell := range row.Cells {
+			values = append(values, cell.Value)
+		}
+
+		rows = append(rows, strings.Join(values, separator))
+	}
+
+	ctx.Set("stringRes", strings.Join(rows, "\n"))
+}
+
 func checkt(t StepTest, ctx Context, text string) {
 	received, err := ctx.GetString("stringRes")
 	if err != nil {