浏览代码

Add custom scan handling

Add the ability for types to control how they are read / scanned
from redis.

This is useful for types which don't want to use the standard
representation i.e. standard strconv methods, when read from redis.

Examples of this is a time.Time stored in a specific format.

Also:
* Fixed a few comment typos.
Steven Hartland 8 年之前
父节点
当前提交
5c484786fa
共有 4 个文件被更改,包括 94 次插入4 次删除
  1. 1 1
      redis/conn_test.go
  2. 10 0
      redis/redis.go
  3. 27 1
      redis/scan.go
  4. 56 2
      redis/scan_test.go

+ 1 - 1
redis/conn_test.go

@@ -459,7 +459,7 @@ var dialErrors = []struct {
 		"localhost",
 		"localhost",
 		"invalid redis URL scheme",
 		"invalid redis URL scheme",
 	},
 	},
-	// The error message for invalid hosts is diffferent in different
+	// The error message for invalid hosts is different in different
 	// versions of Go, so just check that there is an error message.
 	// versions of Go, so just check that there is an error message.
 	{
 	{
 		"redis://weird url",
 		"redis://weird url",

+ 10 - 0
redis/redis.go

@@ -47,3 +47,13 @@ type Argument interface {
 	// in redis commands.
 	// in redis commands.
 	RedisArg() interface{}
 	RedisArg() interface{}
 }
 }
+
+// Scanner is implemented by types which want to control how their value is
+// interpreted when read from redis.
+type Scanner interface {
+	// RedisScan assigns a value from a redis value.
+	//
+	// An error should be returned if the value cannot be stored without
+	// loss of information.
+	RedisScan(src interface{}) error
+}

+ 27 - 1
redis/scan.go

@@ -110,6 +110,25 @@ func convertAssignInt(d reflect.Value, s int64) (err error) {
 }
 }
 
 
 func convertAssignValue(d reflect.Value, s interface{}) (err error) {
 func convertAssignValue(d reflect.Value, s interface{}) (err error) {
+	if d.Kind() != reflect.Ptr {
+		if d.CanAddr() {
+			d2 := d.Addr()
+			if d2.CanInterface() {
+				if scanner, ok := d2.Interface().(Scanner); ok {
+					return scanner.RedisScan(s)
+				}
+			}
+		}
+	} else if d.CanInterface() {
+		// Already a reflect.Ptr
+		if d.IsNil() {
+			d.Set(reflect.New(d.Type().Elem()))
+		}
+		if scanner, ok := d.Interface().(Scanner); ok {
+			return scanner.RedisScan(s)
+		}
+	}
+
 	switch s := s.(type) {
 	switch s := s.(type) {
 	case []byte:
 	case []byte:
 		err = convertAssignBulkString(d, s)
 		err = convertAssignBulkString(d, s)
@@ -135,11 +154,15 @@ func convertAssignArray(d reflect.Value, s []interface{}) error {
 }
 }
 
 
 func convertAssign(d interface{}, s interface{}) (err error) {
 func convertAssign(d interface{}, s interface{}) (err error) {
+	if scanner, ok := d.(Scanner); ok {
+		return scanner.RedisScan(s)
+	}
+
 	// Handle the most common destination types using type switches and
 	// Handle the most common destination types using type switches and
 	// fall back to reflection for all other types.
 	// fall back to reflection for all other types.
 	switch s := s.(type) {
 	switch s := s.(type) {
 	case nil:
 	case nil:
-		// ingore
+		// ignore
 	case []byte:
 	case []byte:
 		switch d := d.(type) {
 		switch d := d.(type) {
 		case *string:
 		case *string:
@@ -219,6 +242,8 @@ func convertAssign(d interface{}, s interface{}) (err error) {
 
 
 // Scan copies from src to the values pointed at by dest.
 // Scan copies from src to the values pointed at by dest.
 //
 //
+// Scan uses RedisScan if available otherwise:
+//
 // The values pointed at by dest must be an integer, float, boolean, string,
 // The values pointed at by dest must be an integer, float, boolean, string,
 // []byte, interface{} or slices of these types. Scan uses the standard strconv
 // []byte, interface{} or slices of these types. Scan uses the standard strconv
 // package to convert bulk strings to numeric and boolean types.
 // package to convert bulk strings to numeric and boolean types.
@@ -359,6 +384,7 @@ var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil po
 //
 //
 // Fields with the tag redis:"-" are ignored.
 // Fields with the tag redis:"-" are ignored.
 //
 //
+// Each field uses RedisScan if available otherwise:
 // Integer, float, boolean, string and []byte fields are supported. Scan uses the
 // Integer, float, boolean, string and []byte fields are supported. Scan uses the
 // standard strconv package to convert bulk string values to numeric and
 // standard strconv package to convert bulk string values to numeric and
 // boolean types.
 // boolean types.

+ 56 - 2
redis/scan_test.go

@@ -19,10 +19,32 @@ import (
 	"math"
 	"math"
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/garyburd/redigo/redis"
 	"github.com/garyburd/redigo/redis"
 )
 )
 
 
+type durationScan struct {
+	time.Duration `redis:"sd"`
+}
+
+func (t *durationScan) RedisScan(src interface{}) (err error) {
+	if t == nil {
+		return fmt.Errorf("nil pointer")
+	}
+	switch src := src.(type) {
+	case string:
+		t.Duration, err = time.ParseDuration(src)
+	case []byte:
+		t.Duration, err = time.ParseDuration(string(src))
+	case int64:
+		t.Duration = time.Duration(src)
+	default:
+		err = fmt.Errorf("cannot convert from %T to %T", src, t)
+	}
+	return err
+}
+
 var scanConversionTests = []struct {
 var scanConversionTests = []struct {
 	src  interface{}
 	src  interface{}
 	dest interface{}
 	dest interface{}
@@ -59,6 +81,11 @@ var scanConversionTests = []struct {
 	{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
 	{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
 	{[]interface{}{[]byte("1")}, []byte{1}},
 	{[]interface{}{[]byte("1")}, []byte{1}},
 	{[]interface{}{[]byte("1")}, []bool{true}},
 	{[]interface{}{[]byte("1")}, []bool{true}},
+	{"1m", durationScan{Duration: time.Minute}},
+	{[]byte("1m"), durationScan{Duration: time.Minute}},
+	{time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}},
+	{[]interface{}{[]byte("1m")}, []durationScan{durationScan{Duration: time.Minute}}},
+	{[]interface{}{[]byte("1m")}, []*durationScan{&durationScan{Duration: time.Minute}}},
 }
 }
 
 
 func TestScanConversion(t *testing.T) {
 func TestScanConversion(t *testing.T) {
@@ -86,6 +113,8 @@ var scanConversionErrorTests = []struct {
 	{int64(-1), byte(0)},
 	{int64(-1), byte(0)},
 	{[]byte("junk"), false},
 	{[]byte("junk"), false},
 	{redis.Error("blah"), false},
 	{redis.Error("blah"), false},
+	{redis.Error("blah"), durationScan{Duration: time.Minute}},
+	{"invalid", durationScan{Duration: time.Minute}},
 }
 }
 
 
 func TestScanConversionError(t *testing.T) {
 func TestScanConversionError(t *testing.T) {
@@ -158,6 +187,8 @@ type s1 struct {
 	Bt bool
 	Bt bool
 	Bf bool
 	Bf bool
 	s0
 	s0
+	Sd  durationScan  `redis:"sd"`
+	Sdp *durationScan `redis:"sdp"`
 }
 }
 
 
 var scanStructTests = []struct {
 var scanStructTests = []struct {
@@ -166,8 +197,31 @@ var scanStructTests = []struct {
 	value interface{}
 	value interface{}
 }{
 }{
 	{"basic",
 	{"basic",
-		[]string{"i", "-1234", "u", "5678", "s", "hello", "p", "world", "b", "t", "Bt", "1", "Bf", "0", "X", "123", "y", "456"},
-		&s1{I: -1234, U: 5678, S: "hello", P: []byte("world"), B: true, Bt: true, Bf: false, s0: s0{X: 123, Y: 456}},
+		[]string{
+			"i", "-1234",
+			"u", "5678",
+			"s", "hello",
+			"p", "world",
+			"b", "t",
+			"Bt", "1",
+			"Bf", "0",
+			"X", "123",
+			"y", "456",
+			"sd", "1m",
+			"sdp", "1m",
+		},
+		&s1{
+			I:   -1234,
+			U:   5678,
+			S:   "hello",
+			P:   []byte("world"),
+			B:   true,
+			Bt:  true,
+			Bf:  false,
+			s0:  s0{X: 123, Y: 456},
+			Sd:  durationScan{Duration: time.Minute},
+			Sdp: &durationScan{Duration: time.Minute},
+		},
 	},
 	},
 }
 }