Skip to content

Commit

Permalink
Add custom scan handling
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stevenh authored and garyburd committed Jun 15, 2017
1 parent 34750db commit 5c48478
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 4 deletions.
2 changes: 1 addition & 1 deletion redis/conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ var dialErrors = []struct {
"localhost",
"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.
{
"redis://weird url",
Expand Down
10 changes: 10 additions & 0 deletions redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,13 @@ type Argument interface {
// in redis commands.
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
}
28 changes: 27 additions & 1 deletion redis/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,25 @@ func convertAssignInt(d reflect.Value, s int64) (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) {
case []byte:
err = convertAssignBulkString(d, s)
Expand All @@ -135,11 +154,15 @@ func convertAssignArray(d reflect.Value, s []interface{}) 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
// fall back to reflection for all other types.
switch s := s.(type) {
case nil:
// ingore
// ignore
case []byte:
switch d := d.(type) {
case *string:
Expand Down Expand Up @@ -219,6 +242,8 @@ func convertAssign(d interface{}, s interface{}) (err error) {

// 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,
// []byte, interface{} or slices of these types. Scan uses the standard strconv
// package to convert bulk strings to numeric and boolean types.
Expand Down Expand Up @@ -359,6 +384,7 @@ var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil po
//
// 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
// standard strconv package to convert bulk string values to numeric and
// boolean types.
Expand Down
58 changes: 56 additions & 2 deletions redis/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,32 @@ import (
"math"
"reflect"
"testing"
"time"

"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 {
src interface{}
dest interface{}
Expand Down Expand Up @@ -59,6 +81,11 @@ var scanConversionTests = []struct {
{[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}},
{[]interface{}{[]byte("1")}, []byte{1}},
{[]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) {
Expand Down Expand Up @@ -86,6 +113,8 @@ var scanConversionErrorTests = []struct {
{int64(-1), byte(0)},
{[]byte("junk"), false},
{redis.Error("blah"), false},
{redis.Error("blah"), durationScan{Duration: time.Minute}},
{"invalid", durationScan{Duration: time.Minute}},
}

func TestScanConversionError(t *testing.T) {
Expand Down Expand Up @@ -158,6 +187,8 @@ type s1 struct {
Bt bool
Bf bool
s0
Sd durationScan `redis:"sd"`
Sdp *durationScan `redis:"sdp"`
}

var scanStructTests = []struct {
Expand All @@ -166,8 +197,31 @@ var scanStructTests = []struct {
value interface{}
}{
{"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},
},
},
}

Expand Down

0 comments on commit 5c48478

Please sign in to comment.