Repository: gomodule/redigo
Pull request #489 (2020-06-06 15:54:59)

feat(ScanSlice): Add Scanner support
Add Scanner support to ScanSlice.

Previously ScanStruct manually scanned results into structs which didn't use any special handling required by the struct as defined by its RedisScan method.


Patch:
diff --git a/redis/scan.go b/redis/scan.go
index 4f10066..377e5ff 100644
--- a/redis/scan.go
+++ b/redis/scan.go
@@ -23,6 +23,10 @@ import (
 	"sync"
 )
 
+var (
+	scannerType = reflect.TypeOf((*Scanner)(nil)).Elem()
+)
+
 func ensureLen(d reflect.Value, n int) {
 	if n > d.Cap() {
 		d.Set(reflect.MakeSlice(d.Type(), n, n))
@@ -500,9 +504,13 @@ var (
 	errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct")
 )
 
-// ScanSlice scans src to the slice pointed to by dest. The elements the dest
-// slice must be integer, float, boolean, string, struct or pointer to struct
-// values.
+// ScanSlice scans src to the slice pointed to by dest.
+//
+// If the target is a slice of types which implement Scanner then the custom
+// RedisScan method is used otherwise the following rules apply:
+//
+// The elements the dest slice must be integer, float, boolean, string, struct
+// or pointer to struct values.
 //
 // Struct fields must be integer, float, boolean or string values. All struct
 // fields are used unless a subset is specified using fieldNames.
@@ -518,12 +526,13 @@ func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error
 
 	isPtr := false
 	t := d.Type().Elem()
+	st := t
 	if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
 		isPtr = true
 		t = t.Elem()
 	}
 
-	if t.Kind() != reflect.Struct {
+	if t.Kind() != reflect.Struct || st.Implements(scannerType) {
 		ensureLen(d, len(src))
 		for i, s := range src {
 			if s == nil {


Test patch:
diff --git a/redis/scan_test.go b/redis/scan_test.go
index 3dd94d5..445e071 100644
--- a/redis/scan_test.go
+++ b/redis/scan_test.go
@@ -298,67 +298,90 @@ func TestBadScanStructArgs(t *testing.T) {
 	test(&v2)
 }
 
+type sliceScanner struct {
+	Field string
+}
+
+func (ss *sliceScanner) RedisScan(s interface{}) error {
+	v, ok := s.([]interface{})
+	if !ok {
+		return fmt.Errorf("invalid type %T", s)
+	}
+	return redis.ScanStruct(v, ss)
+}
+
 var scanSliceTests = []struct {
+	name       string
 	src        []interface{}
 	fieldNames []string
 	ok         bool
 	dest       interface{}
 }{
 	{
+		"scanner",
+		[]interface{}{[]interface{}{[]byte("Field"), []byte("1")}},
+		nil,
+		true,
+		[]*sliceScanner{{"1"}},
+	},
+	{
+		"int",
 		[]interface{}{[]byte("1"), nil, []byte("-1")},
 		nil,
 		true,
 		[]int{1, 0, -1},
 	},
 	{
+		"uint",
 		[]interface{}{[]byte("1"), nil, []byte("2")},
 		nil,
 		true,
 		[]uint{1, 0, 2},
 	},
 	{
+		"uint-error",
 		[]interface{}{[]byte("-1")},
 		nil,
 		false,
 		[]uint{1},
 	},
 	{
+		"[]byte",
 		[]interface{}{[]byte("hello"), nil, []byte("world")},
 		nil,
 		true,
 		[][]byte{[]byte("hello"), nil, []byte("world")},
 	},
 	{
+		"string",
 		[]interface{}{[]byte("hello"), nil, []byte("world")},
 		nil,
 		true,
 		[]string{"hello", "", "world"},
 	},
 	{
+		"struct",
 		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
 		nil,
 		true,
 		[]struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}},
 	},
 	{
+		"struct-error",
 		[]interface{}{[]byte("a1"), []byte("b1")},
 		nil,
 		false,
 		[]struct{ A, B, C string }{{"a1", "b1", ""}},
 	},
 	{
-		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
-		nil,
-		true,
-		[]*struct{ A, B string }{{A: "a1", B: "b1"}, {A: "a2", B: "b2"}},
-	},
-	{
+		"struct-field-names",
 		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
 		[]string{"A", "B"},
 		true,
 		[]struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}},
 	},
 	{
+		"struct-no-fields",
 		[]interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")},
 		nil,
 		false,
@@ -368,18 +391,18 @@ var scanSliceTests = []struct {
 
 func TestScanSlice(t *testing.T) {
 	for _, tt := range scanSliceTests {
+		t.Run(tt.name, func(t *testing.T) {
+			typ := reflect.ValueOf(tt.dest).Type()
+			dest := reflect.New(typ)
 
-		typ := reflect.ValueOf(tt.dest).Type()
-		dest := reflect.New(typ)
-
-		err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...)
-		if tt.ok != (err == nil) {
-			t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err)
-			continue
-		}
-		if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) {
-			t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest)
-		}
+			err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...)
+			if tt.ok != (err == nil) {
+				t.Fatalf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err)
+			}
+			if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) {
+				t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest)
+			}
+		})
 	}
 }