diff --git a/command.go b/command.go index 5dd55332..602ff61b 100644 --- a/command.go +++ b/command.go @@ -8,6 +8,7 @@ import ( "time" "github.com/go-redis/redis/v8/internal" + "github.com/go-redis/redis/v8/internal/hscan" "github.com/go-redis/redis/v8/internal/proto" "github.com/go-redis/redis/v8/internal/util" ) @@ -371,6 +372,13 @@ func (cmd *SliceCmd) String() string { return cmdString(cmd, cmd.val) } +// Scan scans the results from a key-value Redis map result set ([]interface{}) +// like HMGET and HGETALL to a destination struct. +// The Redis keys are matched to the struct's field with the `redis` tag. +func (cmd *SliceCmd) Scan(val interface{}) error { + return hscan.Scan(cmd.val, val) +} + func (cmd *SliceCmd) readReply(rd *proto.Reader) error { v, err := rd.ReadArrayReply(sliceParser) if err != nil { diff --git a/internal/hscan/hscan.go b/internal/hscan/hscan.go index 1c46f2cd..51346b83 100644 --- a/internal/hscan/hscan.go +++ b/internal/hscan/hscan.go @@ -11,7 +11,7 @@ import ( type decoderFunc func(reflect.Value, string) error var ( - // List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1) + // List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1). decoders = []decoderFunc{ reflect.Bool: decodeBool, reflect.Int: decodeInt, diff --git a/internal/hscan/hscan_test.go b/internal/hscan/hscan_test.go new file mode 100644 index 00000000..7a4d0f5b --- /dev/null +++ b/internal/hscan/hscan_test.go @@ -0,0 +1,125 @@ +package hscan + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type data struct { + Omit string `redis:"-"` + Empty string + + String string `redis:"string"` + Bytes []byte `redis:"byte"` + Int int `redis:"int"` + Uint uint `redis:"uint"` + Float float32 `redis:"float"` + Bool bool `redis:"bool"` +} + +func TestGinkgoSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "hscan") +} + +var _ = Describe("Scan", func() { + It("catches bad args", func() { + var d data + + Expect(Scan([]interface{}{}, &d)).NotTo(HaveOccurred()) + Expect(d).To(Equal(data{})) + + Expect(Scan([]interface{}{"key"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"key", "1", "2"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"key", "1"}, nil)).To(HaveOccurred()) + + var i map[string]interface{} + Expect(Scan([]interface{}{"key", "1"}, &i)).To(HaveOccurred()) + Expect(Scan([]interface{}{"key", "1"}, data{})).To(HaveOccurred()) + Expect(Scan([]interface{}{"key", nil, "string", nil}, data{})).To(HaveOccurred()) + }) + + It("scans good values", func() { + var d data + + // non-tagged fields. + Expect(Scan([]interface{}{"key", "value"}, &d)).NotTo(HaveOccurred()) + Expect(d).To(Equal(data{})) + + res := []interface{}{"string", "str!", + "byte", "bytes!", + "int", "123", + "uint", "456", + "float", "123.456", + "bool", "1"} + Expect(Scan(res, &d)).NotTo(HaveOccurred()) + Expect(d).To(Equal(data{ + String: "str!", + Bytes: []byte("bytes!"), + Int: 123, + Uint: 456, + Float: 123.456, + Bool: true, + })) + + // Scan a different type with the same values to test that + // the struct spec maps don't conflict. + type data2 struct { + String string `redis:"string"` + Bytes []byte `redis:"byte"` + Int int `redis:"int"` + Uint uint `redis:"uint"` + Float float32 `redis:"float"` + Bool bool `redis:"bool"` + } + var d2 data2 + Expect(Scan(res, &d2)).NotTo(HaveOccurred()) + Expect(d2).To(Equal(data2{ + String: "str!", + Bytes: []byte("bytes!"), + Int: 123, + Uint: 456, + Float: 123.456, + Bool: true, + })) + + Expect(Scan([]interface{}{ + "string", "", + "float", "1", + "bool", "t"}, &d)).NotTo(HaveOccurred()) + Expect(d).To(Equal(data{ + String: "", + Bytes: []byte("bytes!"), + Int: 123, + Uint: 456, + Float: 1.0, + Bool: true, + })) + }) + + It("omits untagged fields", func() { + var d data + + Expect(Scan([]interface{}{ + "empty", "value", + "omit", "value", + "string", "str!"}, &d)).NotTo(HaveOccurred()) + Expect(d).To(Equal(data{ + String: "str!", + })) + }) + + It("catches bad values", func() { + var d data + + Expect(Scan([]interface{}{"int", "a"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"uint", "a"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"uint", ""}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"float", "b"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"bool", "-1"}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"bool", ""}, &d)).To(HaveOccurred()) + Expect(Scan([]interface{}{"bool", "123"}, &d)).To(HaveOccurred()) + }) +}) diff --git a/internal/hscan/structmap.go b/internal/hscan/structmap.go index 06d5d2fe..da97f907 100644 --- a/internal/hscan/structmap.go +++ b/internal/hscan/structmap.go @@ -61,7 +61,7 @@ func getStructFields(t reflect.Type, fieldTag string) *structFields { for i := 0; i < num; i++ { f := t.Field(i) - tag := t.Field(i).Tag.Get(fieldTag) + tag := f.Tag.Get(fieldTag) if tag == "" || tag == "-" { continue }