diff --git a/README.md b/README.md index ef941d5..05430d2 100644 --- a/README.md +++ b/README.md @@ -469,9 +469,10 @@ that we actually care about, so it's better not to use composite structs. This library has a few helper functions for helping your tests: -- `ksql.FillStructWith(struct interface{}, dbRow map[string]interface{}) error` -- `ksql.FillSliceWith(structSlice interface{}, dbRows []map[string]interface{}) error` -- `ksql.StructToMap(struct interface{}) (map[string]interface{}, error)` +- `structs.FillStructWith(struct interface{}, dbRow map[string]interface{}) error` +- `structs.FillSliceWith(structSlice interface{}, dbRows []map[string]interface{}) error` +- `structs.StructToMap(struct interface{}) (map[string]interface{}, error)` +- `structs.CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) (map[string]interface{}, error)` If you want to see examples (we have examples for all the public functions) just read the example tests available on our [example service](./examples/example_service) diff --git a/examples/example_service/example_service_test.go b/examples/example_service/example_service_test.go index 1cb465c..983ee88 100644 --- a/examples/example_service/example_service_test.go +++ b/examples/example_service/example_service_test.go @@ -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 := ksql.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{ + err := structs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{ { "id": 1, "name": "fake name", @@ -215,7 +215,7 @@ func TestStreamAllUsers(t *testing.T) { } // Chunk 2: - err = ksql.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{ + err = structs.CallFunctionWithRows(parser.ForEachChunk, []map[string]interface{}{ { "id": 3, "name": "yet another fake name", diff --git a/ksql.go b/ksql.go index 85e6e2a..0665449 100644 --- a/ksql.go +++ b/ksql.go @@ -244,7 +244,7 @@ func (c DB) QueryChunks( parser ChunkParser, ) error { fnValue := reflect.ValueOf(parser.ForEachChunk) - chunkType, err := parseInputFunc(parser.ForEachChunk) + chunkType, err := structs.ParseInputFunc(parser.ForEachChunk) if err != nil { return err } @@ -759,38 +759,6 @@ func (c DB) Transaction(ctx context.Context, fn func(SQLProvider) error) error { } } -var errType = reflect.TypeOf(new(error)).Elem() - -func parseInputFunc(fn interface{}) (reflect.Type, error) { - if fn == nil { - return nil, fmt.Errorf("the ForEachChunk attribute is required and cannot be nil") - } - - t := reflect.TypeOf(fn) - - if t.Kind() != reflect.Func { - return nil, fmt.Errorf("the ForEachChunk callback must be a function") - } - if t.NumIn() != 1 { - return nil, fmt.Errorf("the ForEachChunk callback must have 1 argument") - } - - if t.NumOut() != 1 { - return nil, fmt.Errorf("the ForEachChunk callback must have a single return value") - } - - if t.Out(0) != errType { - return nil, fmt.Errorf("the return value of the ForEachChunk callback must be of type error") - } - - argsType := t.In(0) - if argsType.Kind() != reflect.Slice { - return nil, fmt.Errorf("the argument of the ForEachChunk callback must a slice of structs") - } - - return argsType, nil -} - type nopScanner struct{} var nopScannerValue = reflect.ValueOf(&nopScanner{}).Interface() diff --git a/structs/func_parser.go b/structs/func_parser.go new file mode 100644 index 0000000..d68db69 --- /dev/null +++ b/structs/func_parser.go @@ -0,0 +1,40 @@ +package structs + +import ( + "fmt" + "reflect" +) + +var errType = reflect.TypeOf(new(error)).Elem() + +// ParseInputFunc is used exclusively for parsing +// the ForEachChunk function used on the QueryChunks method. +func ParseInputFunc(fn interface{}) (reflect.Type, error) { + if fn == nil { + return nil, fmt.Errorf("the ForEachChunk attribute is required and cannot be nil") + } + + t := reflect.TypeOf(fn) + + if t.Kind() != reflect.Func { + return nil, fmt.Errorf("the ForEachChunk callback must be a function") + } + if t.NumIn() != 1 { + return nil, fmt.Errorf("the ForEachChunk callback must have 1 argument") + } + + if t.NumOut() != 1 { + return nil, fmt.Errorf("the ForEachChunk callback must have a single return value") + } + + if t.Out(0) != errType { + return nil, fmt.Errorf("the return value of the ForEachChunk callback must be of type error") + } + + argsType := t.In(0) + if argsType.Kind() != reflect.Slice { + return nil, fmt.Errorf("the argument of the ForEachChunk callback must a slice of structs") + } + + return argsType, nil +} diff --git a/structs/structs.go b/structs/structs.go index 7690555..a77b3a0 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -4,8 +4,6 @@ import ( "fmt" "reflect" "strings" - - "github.com/pkg/errors" ) // StructInfo stores metainformation of the struct @@ -120,56 +118,6 @@ func StructToMap(obj interface{}) (map[string]interface{}, error) { return m, nil } -// 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 kind to be a struct but got %T", - record, - ) - } - - info := getCachedTagInfo(tagInfoCache, t) - for colName, rawSrc := range dbRow { - fieldInfo := info.ByName(colName) - if !fieldInfo.Valid { - // Ignore columns not tagged with `ksql:"..."` - continue - } - - src := 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 -} - // PtrConverter was created to make it easier // to handle conversion between ptr and non ptr types, e.g.: // @@ -251,49 +199,6 @@ func (p PtrConverter) Convert(destType reflect.Type) (reflect.Value, error) { return destValue, 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 := 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 -} - // This function collects only the names // that will be used from the input type. // diff --git a/structs/testhelpers.go b/structs/testhelpers.go new file mode 100644 index 0000000..c02c40d --- /dev/null +++ b/structs/testhelpers.go @@ -0,0 +1,125 @@ +package structs + +import ( + "fmt" + "reflect" + + "github.com/pkg/errors" +) + +// 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 kind to be a struct but got %T", + record, + ) + } + + info := GetTagInfo(t) + for colName, rawSrc := range dbRow { + fieldInfo := info.ByName(colName) + if !fieldInfo.Valid { + // Ignore columns not tagged with `ksql:"..."` + continue + } + + src := 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 := 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 := 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/test_helpers.go b/test_helpers.go deleted file mode 100644 index 65fff68..0000000 --- a/test_helpers.go +++ /dev/null @@ -1,31 +0,0 @@ -package ksql - -import ( - "reflect" - - "github.com/vingarcia/ksql/structs" -) - -// CallFunctionWithRows was created for helping test the QueryChunks method -func CallFunctionWithRows(fn interface{}, rows []map[string]interface{}) error { - fnValue := reflect.ValueOf(fn) - chunkType, err := 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 = structs.FillSliceWith(chunkPtr.Interface(), rows) - if err != nil { - return err - } - - err, _ = fnValue.Call([]reflect.Value{chunkPtr.Elem()})[0].Interface().(error) - - return err -}