From 06b871a418dfc78d10f01be76f653c8502eb449a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Vin=C3=ADcius=20Garcia?= <vingarcia00@gmail.com>
Date: Tue, 22 Feb 2022 22:41:15 -0300
Subject: [PATCH] Deprecate kstructs in favor of ksqltest

---
 .../example_service/example_service_test.go   |  14 +-
 ksql.go                                       |  10 +-
 ksqltest/testhelpers.go                       | 144 +++++++
 ksqltest/testhelpers_test.go                  | 395 ++++++++++++++++++
 kstructs/testhelpers.go                       |   1 +
 5 files changed, 552 insertions(+), 12 deletions(-)
 create mode 100644 ksqltest/testhelpers.go
 create mode 100644 ksqltest/testhelpers_test.go

diff --git a/examples/example_service/example_service_test.go b/examples/example_service/example_service_test.go
index 1cddac4..1f690c1 100644
--- a/examples/example_service/example_service_test.go
+++ b/examples/example_service/example_service_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/ditointernet/go-assert"
 	gomock "github.com/golang/mock/gomock"
 	"github.com/vingarcia/ksql"
-	"github.com/vingarcia/ksql/kstructs"
+	"github.com/vingarcia/ksql/ksqltest"
 	"github.com/vingarcia/ksql/nullable"
 )
 
@@ -58,7 +58,7 @@ func TestCreateUser(t *testing.T) {
 					//
 					// If you are inserting an anonymous struct (not usual) this function
 					// can make your tests shorter:
-					uMap, err := kstructs.StructToMap(record)
+					uMap, err := ksqltest.StructToMap(record)
 					if err != nil {
 						return err
 					}
@@ -95,7 +95,7 @@ func TestUpdateUserScore(t *testing.T) {
 				DoAndReturn(func(ctx context.Context, result interface{}, query string, params ...interface{}) error {
 					// This function will use reflection to fill the
 					// struct fields with the values from the map
-					return kstructs.FillStructWith(result, map[string]interface{}{
+					return ksqltest.FillStructWith(result, map[string]interface{}{
 						// Use int this map the keys you set on the ksql tags, e.g. `ksql:"score"`
 						// Each of these fields represent the database rows returned
 						// by the query.
@@ -138,7 +138,7 @@ func TestListUsers(t *testing.T) {
 				DoAndReturn(func(ctx context.Context, result interface{}, query string, params ...interface{}) error {
 					// This function will use reflection to fill the
 					// struct fields with the values from the map
-					return kstructs.FillStructWith(result, map[string]interface{}{
+					return ksqltest.FillStructWith(result, map[string]interface{}{
 						// Use int this map the keys you set on the ksql tags, e.g. `ksql:"score"`
 						// Each of these fields represent the database rows returned
 						// by the query.
@@ -147,7 +147,7 @@ func TestListUsers(t *testing.T) {
 				}),
 			mockDB.EXPECT().Query(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
 				DoAndReturn(func(ctx context.Context, results interface{}, query string, params ...interface{}) error {
-					return kstructs.FillSliceWith(results, []map[string]interface{}{
+					return ksqltest.FillSliceWith(results, []map[string]interface{}{
 						{
 							"id":   1,
 							"name": "fake name",
@@ -198,7 +198,7 @@ func TestStreamAllUsers(t *testing.T) {
 		mockDB.EXPECT().QueryChunks(gomock.Any(), gomock.Any()).
 			DoAndReturn(func(ctx context.Context, parser ksql.ChunkParser) error {
 				// Chunk 1:
-				err := kstructs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
+				err := ksqltest.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
 					{
 						"id":   1,
 						"name": "fake name",
@@ -215,7 +215,7 @@ func TestStreamAllUsers(t *testing.T) {
 				}
 
 				// Chunk 2:
-				err = kstructs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
+				err = ksqltest.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{
 					{
 						"id":   3,
 						"name": "yet another fake name",
diff --git a/ksql.go b/ksql.go
index 66260b5..501352a 100644
--- a/ksql.go
+++ b/ksql.go
@@ -9,7 +9,7 @@ import (
 
 	"github.com/pkg/errors"
 	"github.com/vingarcia/ksql/internal/structs"
-	"github.com/vingarcia/ksql/kstructs"
+	"github.com/vingarcia/ksql/ksqltest"
 )
 
 var selectQueryCache = map[string]map[reflect.Type]string{}
@@ -593,7 +593,7 @@ func normalizeIDsAsMap(idNames []string, idOrMap interface{}) (idMap map[string]
 
 	switch t.Kind() {
 	case reflect.Struct:
-		idMap, err = kstructs.StructToMap(idOrMap)
+		idMap, err = ksqltest.StructToMap(idOrMap)
 		if err != nil {
 			return nil, errors.Wrapf(err, "could not get ID(s) from input record")
 		}
@@ -691,7 +691,7 @@ func buildInsertQuery(
 	info structs.StructInfo,
 	record interface{},
 ) (query string, params []interface{}, scanValues []interface{}, err error) {
-	recordMap, err := kstructs.StructToMap(record)
+	recordMap, err := ksqltest.StructToMap(record)
 	if err != nil {
 		return "", nil, nil, err
 	}
@@ -785,7 +785,7 @@ func buildUpdateQuery(
 	record interface{},
 	idFieldNames ...string,
 ) (query string, args []interface{}, err error) {
-	recordMap, err := kstructs.StructToMap(record)
+	recordMap, err := ksqltest.StructToMap(record)
 	if err != nil {
 		return "", nil, err
 	}
@@ -927,7 +927,7 @@ func scanRowsFromType(
 	if info.IsNestedStruct {
 		// This version is positional meaning that it expect the arguments
 		// to follow an specific order. It's ok because we don't allow the
-		// user to type the "SELECT" part of the query for nested kstructs.
+		// user to type the "SELECT" part of the query for nested ksqltest.
 		scanArgs, err = getScanArgsForNestedStructs(dialect, rows, t, v, info)
 		if err != nil {
 			return err
diff --git a/ksqltest/testhelpers.go b/ksqltest/testhelpers.go
new file mode 100644
index 0000000..c9f0099
--- /dev/null
+++ b/ksqltest/testhelpers.go
@@ -0,0 +1,144 @@
+package ksqltest
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/pkg/errors"
+
+	"github.com/vingarcia/ksql/internal/structs"
+)
+
+// StructToMap converts any struct type to a map based on
+// the tag named `ksql`, i.e. `ksql:"map_key_name"`
+//
+// Valid pointers are dereferenced and copied to the map,
+// null pointers are ignored.
+//
+// This function is efficient in the fact that it caches
+// the slower steps of the reflection required to perform
+// this task.
+func StructToMap(obj interface{}) (map[string]interface{}, error) {
+	return structs.StructToMap(obj)
+}
+
+// FillStructWith is meant to be used on unit tests to mock
+// the response from the database.
+//
+// The first argument is any struct you are passing to a ksql func,
+// and the second is a map representing a database row you want
+// to use to update this struct.
+func FillStructWith(record interface{}, dbRow map[string]interface{}) error {
+	v := reflect.ValueOf(record)
+	t := v.Type()
+
+	if t.Kind() != reflect.Ptr {
+		return fmt.Errorf(
+			"FillStructWith: expected input to be a pointer to struct but got %T",
+			record,
+		)
+	}
+
+	t = t.Elem()
+	v = v.Elem()
+
+	if t.Kind() != reflect.Struct {
+		return fmt.Errorf(
+			"FillStructWith: expected input to be a pointer to a struct, but got %T",
+			record,
+		)
+	}
+
+	info, err := structs.GetTagInfo(t)
+	if err != nil {
+		return err
+	}
+
+	for colName, rawSrc := range dbRow {
+		fieldInfo := info.ByName(colName)
+		if !fieldInfo.Valid {
+			// Ignore columns not tagged with `ksql:"..."`
+			continue
+		}
+
+		src := structs.NewPtrConverter(rawSrc)
+		dest := v.Field(fieldInfo.Index)
+		destType := t.Field(fieldInfo.Index).Type
+
+		destValue, err := src.Convert(destType)
+		if err != nil {
+			return errors.Wrap(err, fmt.Sprintf("FillStructWith: error on field `%s`", colName))
+		}
+
+		dest.Set(destValue)
+	}
+
+	return nil
+}
+
+// FillSliceWith is meant to be used on unit tests to mock
+// the response from the database.
+//
+// The first argument is any slice of structs you are passing to a ksql func,
+// and the second is a slice of maps representing the database rows you want
+// to use to update this struct.
+func FillSliceWith(entities interface{}, dbRows []map[string]interface{}) error {
+	sliceRef := reflect.ValueOf(entities)
+	sliceType := sliceRef.Type()
+	if sliceType.Kind() != reflect.Ptr {
+		return fmt.Errorf(
+			"FillSliceWith: expected input to be a pointer to a slice of structs but got %v",
+			sliceType,
+		)
+	}
+
+	structType, isSliceOfPtrs, err := structs.DecodeAsSliceOfStructs(sliceType.Elem())
+	if err != nil {
+		return errors.Wrap(err, "FillSliceWith")
+	}
+
+	slice := sliceRef.Elem()
+	for idx, row := range dbRows {
+		if slice.Len() <= idx {
+			var elemValue reflect.Value
+			elemValue = reflect.New(structType)
+			if !isSliceOfPtrs {
+				elemValue = elemValue.Elem()
+			}
+			slice = reflect.Append(slice, elemValue)
+		}
+
+		err := FillStructWith(slice.Index(idx).Addr().Interface(), row)
+		if err != nil {
+			return errors.Wrap(err, "FillSliceWith")
+		}
+	}
+
+	sliceRef.Elem().Set(slice)
+
+	return nil
+}
+
+// CallFunctionWithRows was created for helping test the QueryChunks method
+func CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) error {
+	fnValue := reflect.ValueOf(fn)
+	chunkType, err := structs.ParseInputFunc(fn)
+	if err != nil {
+		return err
+	}
+
+	chunk := reflect.MakeSlice(chunkType, 0, len(rows))
+
+	// Create a pointer to a slice (required by FillSliceWith)
+	chunkPtr := reflect.New(chunkType)
+	chunkPtr.Elem().Set(chunk)
+
+	err = FillSliceWith(chunkPtr.Interface(), rows)
+	if err != nil {
+		return err
+	}
+
+	err, _ = fnValue.Call([]reflect.Value{chunkPtr.Elem()})[0].Interface().(error)
+
+	return err
+}
diff --git a/ksqltest/testhelpers_test.go b/ksqltest/testhelpers_test.go
new file mode 100644
index 0000000..a678177
--- /dev/null
+++ b/ksqltest/testhelpers_test.go
@@ -0,0 +1,395 @@
+package ksqltest
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/ditointernet/go-assert"
+	tt "github.com/vingarcia/ksql/internal/testtools"
+	"github.com/vingarcia/ksql/nullable"
+)
+
+func TestStructToMap(t *testing.T) {
+	type S1 struct {
+		Name string `ksql:"name_attr"`
+		Age  int    `ksql:"age_attr"`
+	}
+	t.Run("should convert plain structs to maps", func(t *testing.T) {
+		m, err := StructToMap(S1{
+			Name: "my name",
+			Age:  22,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, map[string]interface{}{
+			"name_attr": "my name",
+			"age_attr":  22,
+		}, m)
+	})
+
+	t.Run("should not ignore zero value attrs, if they are not pointers", func(t *testing.T) {
+		m, err := StructToMap(S1{
+			Name: "",
+			Age:  0,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, map[string]interface{}{
+			"name_attr": "",
+			"age_attr":  0,
+		}, m)
+	})
+
+	type S2 struct {
+		Name *string `ksql:"name"`
+		Age  *int    `ksql:"age"`
+	}
+
+	t.Run("should not ignore not nil pointers", func(t *testing.T) {
+		str := ""
+		age := 0
+		m, err := StructToMap(S2{
+			Name: &str,
+			Age:  &age,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, map[string]interface{}{
+			"name": "",
+			"age":  0,
+		}, m)
+	})
+
+	t.Run("should ignore nil pointers", func(t *testing.T) {
+		m, err := StructToMap(S2{
+			Name: nil,
+			Age:  nil,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, map[string]interface{}{}, m)
+	})
+
+	t.Run("should ignore fields not tagged with ksql", func(t *testing.T) {
+		m, err := StructToMap(struct {
+			Name              string `ksql:"name_attr"`
+			Age               int    `ksql:"age_attr"`
+			NotPartOfTheQuery int
+		}{
+			Name:              "fake-name",
+			Age:               42,
+			NotPartOfTheQuery: 42,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, map[string]interface{}{
+			"name_attr": "fake-name",
+			"age_attr":  42,
+		}, m)
+	})
+
+	t.Run("should return error for duplicated ksql tag names", func(t *testing.T) {
+		_, err := StructToMap(struct {
+			Name           string `ksql:"name_attr"`
+			DuplicatedName string `ksql:"name_attr"`
+			Age            int    `ksql:"age_attr"`
+		}{
+			Name:           "fake-name",
+			Age:            42,
+			DuplicatedName: "fake-duplicated-name",
+		})
+
+		assert.NotEqual(t, nil, err)
+	})
+
+	t.Run("should return error for structs with no ksql tags", func(t *testing.T) {
+		_, err := StructToMap(struct {
+			Name string
+			Age  int `json:"age"`
+		}{
+			Name: "fake-name",
+			Age:  42,
+		})
+
+		assert.NotEqual(t, nil, err)
+	})
+}
+
+func TestFillStructWith(t *testing.T) {
+	t.Run("should fill a struct correctly", func(t *testing.T) {
+		var user struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": "Breno",
+			"age":  22,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, "Breno", user.Name)
+		assert.Equal(t, 22, user.Age)
+	})
+
+	t.Run("should fill ptr fields with ptr values", func(t *testing.T) {
+		var user struct {
+			Name *string `ksql:"name"`
+			Age  *int    `ksql:"age"`
+		}
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": nullable.String("Breno"),
+			"age":  nullable.Int(22),
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, nullable.String("Breno"), user.Name)
+		assert.Equal(t, nullable.Int(22), user.Age)
+	})
+
+	t.Run("should fill ptr fields with non-ptr values", func(t *testing.T) {
+		var user struct {
+			Name *string `ksql:"name"`
+			Age  *int    `ksql:"age"`
+		}
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": "Breno",
+			"age":  22,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, nullable.String("Breno"), user.Name)
+		assert.Equal(t, nullable.Int(22), user.Age)
+	})
+
+	t.Run("should fill non ptr fields with ptr values", func(t *testing.T) {
+		var user struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": nullable.String("Breno"),
+			"age":  nullable.Int(22),
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, "Breno", user.Name)
+		assert.Equal(t, 22, user.Age)
+	})
+
+	t.Run("should fill ptr fields with nil when necessary", func(t *testing.T) {
+		var user struct {
+			Name *string `ksql:"name"`
+			Age  *int    `ksql:"age"`
+		}
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": nil,
+			"age":  nil,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, (*string)(nil), user.Name)
+		assert.Equal(t, (*int)(nil), user.Age)
+	})
+
+	t.Run("should interpret nil fields as zero values when necessary", func(t *testing.T) {
+		var user struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		user.Name = "not empty"
+		user.Age = 42
+
+		err := FillStructWith(&user, map[string]interface{}{
+			"name": nil,
+			"age":  nil,
+		})
+
+		assert.Equal(t, nil, err)
+		assert.Equal(t, "", user.Name)
+		assert.Equal(t, 0, user.Age)
+	})
+
+	t.Run("should ignore extra or missing fields", func(t *testing.T) {
+		var user struct {
+			Name    string `ksql:"name"`
+			Age     int    `ksql:"age"`
+			Missing string `ksql:"missing"`
+		}
+		user.Missing = "should be untouched"
+
+		err := FillStructWith(&user, map[string]interface{}{
+			"name":        "fake name",
+			"age":         42,
+			"extra_field": "some value",
+		})
+
+		tt.AssertEqual(t, nil, err)
+		tt.AssertEqual(t, "fake name", user.Name)
+		tt.AssertEqual(t, 42, user.Age)
+		tt.AssertEqual(t, "should be untouched", user.Missing)
+	})
+
+	t.Run("should report error if input is not a pointer", func(t *testing.T) {
+		type User struct {
+			Name    string `ksql:"name"`
+			Age     int    `ksql:"age"`
+			Missing string `ksql:"missing"`
+		}
+		var user User
+
+		err := FillStructWith(user, map[string]interface{}{
+			"name":        "fake name",
+			"age":         42,
+			"extra_field": "some value",
+		})
+
+		tt.AssertErrContains(t, err, "FillStructWith", "expected input to be a pointer", "User")
+	})
+
+	t.Run("should report error if input is not a pointer to struct", func(t *testing.T) {
+		type User struct {
+			Name    string `ksql:"name"`
+			Age     int    `ksql:"age"`
+			Missing string `ksql:"missing"`
+		}
+		var users []User
+
+		err := FillStructWith(&users, map[string]interface{}{
+			"name":        "fake name",
+			"age":         42,
+			"extra_field": "some value",
+		})
+
+		tt.AssertErrContains(t, err, "FillStructWith", "expected input to be a pointer to a struct", "User")
+	})
+
+	t.Run("should report error if input and target types are incompatible", func(t *testing.T) {
+		type User struct {
+			Age int `ksql:"age"`
+		}
+		var user User
+
+		err := FillStructWith(&user, map[string]interface{}{
+			"age": "not compatible with integer type",
+		})
+
+		tt.AssertErrContains(t, err, "FillStructWith", "age", "string", "int")
+	})
+}
+
+func TestFillSliceWith(t *testing.T) {
+	t.Run("should fill a list correctly", func(t *testing.T) {
+		var users []struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		err := FillSliceWith(&users, []map[string]interface{}{
+			{
+				"name": "Jorge",
+			},
+			{
+				"name": "Luciana",
+			},
+			{
+				"name": "Breno",
+			},
+		})
+
+		tt.AssertEqual(t, err, nil)
+		tt.AssertEqual(t, len(users), 3)
+		tt.AssertEqual(t, users[0].Name, "Jorge")
+		tt.AssertEqual(t, users[1].Name, "Luciana")
+		tt.AssertEqual(t, users[2].Name, "Breno")
+	})
+
+	t.Run("should report error if input is not a pointer", func(t *testing.T) {
+		var users []struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		err := FillSliceWith(users, []map[string]interface{}{{
+			"name": "Jorge",
+		}})
+
+		tt.AssertErrContains(t, err, "FillSliceWith", "expected input to be a pointer")
+	})
+
+	t.Run("should report error if input is not a pointer to a slice", func(t *testing.T) {
+		var user struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+		err := FillSliceWith(&user, []map[string]interface{}{{
+			"name": "Jorge",
+		}})
+
+		tt.AssertErrContains(t, err, "FillSliceWith")
+	})
+}
+
+func TestCallFunctionWithRows(t *testing.T) {
+	t.Run("should call the function correctly", func(t *testing.T) {
+		type User struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+
+		var inputUsers []User
+		fn := func(users []User) error {
+			inputUsers = users
+			return nil
+		}
+
+		err := CallFunctionWithRows(fn, []map[string]interface{}{
+			{
+				"name": "fake-name1",
+				"age":  42,
+			},
+			{
+				"name": "fake-name2",
+				"age":  43,
+			},
+		})
+		tt.AssertNoErr(t, err)
+		tt.AssertEqual(t, inputUsers, []User{
+			{
+				Name: "fake-name1",
+				Age:  42,
+			},
+			{
+				Name: "fake-name2",
+				Age:  43,
+			},
+		})
+	})
+
+	t.Run("should forward errors correctly", func(t *testing.T) {
+		type User struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+
+		fn := func(users []User) error {
+			return fmt.Errorf("fake-error-msg")
+		}
+
+		err := CallFunctionWithRows(fn, []map[string]interface{}{{
+			"name": "fake-name1",
+			"age":  42,
+		}})
+		tt.AssertErrContains(t, err, "fake-error-msg")
+	})
+
+	t.Run("should report error if the input function is invalid", func(t *testing.T) {
+		type User struct {
+			Name string `ksql:"name"`
+			Age  int    `ksql:"age"`
+		}
+
+		err := CallFunctionWithRows(func() {}, []map[string]interface{}{{
+			"name": "fake-name1",
+			"age":  42,
+		}})
+		tt.AssertErrContains(t, err)
+	})
+}
diff --git a/kstructs/testhelpers.go b/kstructs/testhelpers.go
index da5c66d..792e20f 100644
--- a/kstructs/testhelpers.go
+++ b/kstructs/testhelpers.go
@@ -1,3 +1,4 @@
+// Package kstructs is deprecated: use ksqltest instead.
 package kstructs
 
 import (