Deprecate kstructs in favor of ksqltest

pull/16/head
Vinícius Garcia 2022-02-22 22:41:15 -03:00
parent 9b18a8fbcf
commit 06b871a418
5 changed files with 552 additions and 12 deletions

View File

@ -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",

10
ksql.go
View File

@ -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

144
ksqltest/testhelpers.go Normal file
View File

@ -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
}

View File

@ -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)
})
}

View File

@ -1,3 +1,4 @@
// Package kstructs is deprecated: use ksqltest instead.
package kstructs
import (