Skip to content

Commit 6f96beb

Browse files
committed
Add redis.Scan() to scan results from redis maps into structs.
The package uses reflection to decode default types (int, string etc.) from Redis map results (key-value pair sequences) into struct fields where the fields are matched to Redis keys by tags. Similar to how `encoding/json` allows custom decoders using `UnmarshalJSON()`, the package supports decoding of arbitrary types into struct fields by defining a `Decode(string) error` function on types. The field/type spec of every struct that's passed to Scan() is cached in the package so that subsequent scans avoid iteration and reflection of the struct's fields.
1 parent bf010a7 commit 6f96beb

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

internal/hscan/hscan.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package hscan
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
"strconv"
8+
)
9+
10+
// decoderFunc represents decoding functions for default built-in types.
11+
type decoderFunc func(reflect.Value, string) error
12+
13+
var (
14+
// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1)
15+
decoders = []decoderFunc{
16+
reflect.Bool: decodeBool,
17+
reflect.Int: decodeInt,
18+
reflect.Int8: decodeInt,
19+
reflect.Int16: decodeInt,
20+
reflect.Int32: decodeInt,
21+
reflect.Int64: decodeInt,
22+
reflect.Uint: decodeUint,
23+
reflect.Uint8: decodeUint,
24+
reflect.Uint16: decodeUint,
25+
reflect.Uint32: decodeUint,
26+
reflect.Uint64: decodeUint,
27+
reflect.Float32: decodeFloat,
28+
reflect.Float64: decodeFloat,
29+
reflect.Complex64: decodeUnsupported,
30+
reflect.Complex128: decodeUnsupported,
31+
reflect.Array: decodeUnsupported,
32+
reflect.Chan: decodeUnsupported,
33+
reflect.Func: decodeUnsupported,
34+
reflect.Interface: decodeUnsupported,
35+
reflect.Map: decodeUnsupported,
36+
reflect.Ptr: decodeUnsupported,
37+
reflect.Slice: decodeStringSlice,
38+
reflect.String: decodeString,
39+
reflect.Struct: decodeUnsupported,
40+
reflect.UnsafePointer: decodeUnsupported,
41+
}
42+
43+
// Global map of struct field specs that is populated once for every new
44+
// struct type that is scanned. This caches the field types and the corresponding
45+
// decoder functions to avoid iterating through struct fields on subsequent scans.
46+
structSpecs = newStructMap()
47+
)
48+
49+
// Scan scans the results from a key-value Redis map result set ([]interface{})
50+
// to a destination struct. The Redis keys are matched to the struct's field
51+
// with the `redis` tag.
52+
func Scan(vals []interface{}, dest interface{}) error {
53+
if len(vals)%2 != 0 {
54+
return errors.New("args should have an even number of items (key-val)")
55+
}
56+
57+
// The destination to scan into should be a struct pointer.
58+
v := reflect.ValueOf(dest)
59+
if v.Kind() != reflect.Ptr || v.IsNil() {
60+
return fmt.Errorf("redis.Scan(non-pointer %T)", dest)
61+
}
62+
v = v.Elem()
63+
64+
if v.Kind() != reflect.Struct {
65+
return fmt.Errorf("redis.Scan(non-struct %T)", dest)
66+
}
67+
68+
// If the struct field spec is not cached, build and cache it to avoid
69+
// iterating through the fields of a struct type every time values are
70+
// scanned into it.
71+
typ := v.Type()
72+
fMap, ok := structSpecs.get(typ)
73+
74+
if !ok {
75+
fMap = makeStructSpecs(v, "redis")
76+
structSpecs.set(typ, fMap)
77+
}
78+
79+
// Iterate through the (key, value) sequence.
80+
for i := 0; i < len(vals); i += 2 {
81+
key, ok := vals[i].(string)
82+
if !ok {
83+
continue
84+
}
85+
86+
val, ok := vals[i+1].(string)
87+
if !ok {
88+
continue
89+
}
90+
91+
// Check if the field name is in the field spec map.
92+
field, ok := fMap.get(key)
93+
if !ok {
94+
continue
95+
}
96+
97+
if err := field.fn(v.Field(field.index), val); err != nil {
98+
return err
99+
}
100+
}
101+
102+
return nil
103+
}
104+
105+
func decodeBool(f reflect.Value, s string) error {
106+
b, err := strconv.ParseBool(s)
107+
if err != nil {
108+
return err
109+
}
110+
f.SetBool(b)
111+
return nil
112+
}
113+
114+
func decodeInt(f reflect.Value, s string) error {
115+
v, err := strconv.ParseInt(s, 10, 0)
116+
if err != nil {
117+
return err
118+
}
119+
f.SetInt(v)
120+
return nil
121+
}
122+
123+
func decodeUint(f reflect.Value, s string) error {
124+
v, err := strconv.ParseUint(s, 10, 0)
125+
if err != nil {
126+
return err
127+
}
128+
f.SetUint(v)
129+
return nil
130+
}
131+
132+
func decodeFloat(f reflect.Value, s string) error {
133+
v, err := strconv.ParseFloat(s, 0)
134+
if err != nil {
135+
return err
136+
}
137+
f.SetFloat(v)
138+
return nil
139+
}
140+
141+
func decodeString(f reflect.Value, s string) error {
142+
f.SetString(s)
143+
return nil
144+
}
145+
146+
func decodeStringSlice(f reflect.Value, s string) error {
147+
// []byte slice ([]uint8).
148+
if f.Type().Elem().Kind() == reflect.Uint8 {
149+
f.SetBytes([]byte(s))
150+
}
151+
return nil
152+
}
153+
154+
func decodeUnsupported(f reflect.Value, s string) error {
155+
return fmt.Errorf("redis.Scan(unsupported type %v)", f)
156+
}

internal/hscan/structmap.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package hscan
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"sync"
7+
)
8+
9+
// structField represents a single field in a target struct.
10+
type structField struct {
11+
index int
12+
fn decoderFunc
13+
}
14+
15+
// structFields contains the list of all fields in a target struct.
16+
type structFields struct {
17+
m map[string]*structField
18+
}
19+
20+
// structMap contains the map of struct fields for target structs
21+
// indexed by the struct type.
22+
type structMap struct {
23+
m sync.Map
24+
}
25+
26+
func newStructMap() *structMap {
27+
return &structMap{
28+
m: sync.Map{},
29+
}
30+
}
31+
32+
func (s *structMap) get(t reflect.Type) (*structFields, bool) {
33+
m, ok := s.m.Load(t)
34+
if !ok {
35+
return nil, ok
36+
}
37+
38+
return m.(*structFields), true
39+
}
40+
41+
func (s *structMap) set(t reflect.Type, sf *structFields) {
42+
s.m.Store(t, sf)
43+
}
44+
45+
func newStructFields() *structFields {
46+
return &structFields{
47+
m: make(map[string]*structField),
48+
}
49+
}
50+
51+
func (s *structFields) set(tag string, sf *structField) {
52+
s.m[tag] = sf
53+
}
54+
55+
func (s *structFields) get(tag string) (*structField, bool) {
56+
f, ok := s.m[tag]
57+
return f, ok
58+
}
59+
60+
func makeStructSpecs(ob reflect.Value, fieldTag string) *structFields {
61+
var (
62+
num = ob.NumField()
63+
out = newStructFields()
64+
)
65+
66+
for i := 0; i < num; i++ {
67+
f := ob.Field(i)
68+
if !f.IsValid() || !f.CanSet() {
69+
continue
70+
}
71+
72+
tag := ob.Type().Field(i).Tag.Get(fieldTag)
73+
if tag == "" || tag == "-" {
74+
continue
75+
}
76+
77+
tag = strings.Split(tag, ",")[0]
78+
if tag == "" {
79+
continue
80+
}
81+
82+
// Use the built-in decoder.
83+
out.set(tag, &structField{index: i, fn: decoders[f.Kind()]})
84+
}
85+
86+
return out
87+
}

0 commit comments

Comments
 (0)