瀏覽代碼

Add experimental scan package.

Gary Burd 13 年之前
父節點
當前提交
95d2e16dc3
共有 5 個文件被更改,包括 391 次插入0 次删除
  1. 18 0
      exp/scan/doc.go
  2. 41 0
      exp/scan/example.go
  3. 119 0
      exp/scan/struct.go
  4. 91 0
      exp/scan/struct_test.go
  5. 122 0
      exp/scan/util.go

+ 18 - 0
exp/scan/doc.go

@@ -0,0 +1,18 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package scan is an experimental package for working with Redis commands and replies.  
+//
+// This package is a work in progress. The API is not stable.
+package scan

+ 41 - 0
exp/scan/example.go

@@ -0,0 +1,41 @@
+// +build ignore 
+
+package main
+
+import (
+	"github.com/garyburd/redigo/exp/scan"
+	"github.com/garyburd/redigo/redis"
+	"log"
+)
+
+type MyStruct struct {
+	A int
+	B string
+}
+
+func main() {
+	c, err := redis.Dial("tcp", ":6379")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	v0 := &MyStruct{1, "hello"}
+
+	_, err = c.Do("HMSET", append([]interface{}{"key"}, scan.FormatStruct(v0)...)...)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	reply, err := c.Do("HGETALL", "key")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	v1 := &MyStruct{}
+
+	err = scan.ScanStruct(reply, v1)
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Printf("v1=%v", v1)
+}

+ 119 - 0
exp/scan/struct.go

@@ -0,0 +1,119 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package scan
+
+import (
+	"errors"
+	"reflect"
+	"strconv"
+)
+
+// ScanStruct scans a reply containing alternating names and values to a
+// struct. The HGETALL and CONFIG GET commands return replies in this format.
+//
+// ScanStruct uses the struct field name to match values in the response. Use
+// 'redis' field tag to override the name:
+//
+//      Field int `redis:"myName"`
+//
+// Fields with the tag redis:"-" are ignored.
+func ScanStruct(reply interface{}, dst interface{}) error {
+	v := reflect.ValueOf(dst)
+	if v.Kind() != reflect.Ptr || v.IsNil() {
+		return errors.New("redigo: ScanStruct value must be non-nil pointer")
+	}
+	v = v.Elem()
+	ss := structSpecForType(v.Type())
+
+	p, ok := reply.([]interface{})
+	if !ok {
+		return errors.New("redigo: ScanStruct expectes multibulk reply")
+	}
+	if len(p)%2 != 0 {
+		return errors.New("redigo: ScanStruct expects even number of values in reply")
+	}
+
+	for i := 0; i < len(p); i += 2 {
+		name, ok := p[i].([]byte)
+		if !ok {
+			return errors.New("redigo: ScanStruct key not a bulk value")
+		}
+		value, ok := p[i+1].([]byte)
+		if !ok {
+			return errors.New("redigo: ScanStruct value not a bulk value")
+		}
+		fs := ss.fieldSpec(name)
+		if fs == nil {
+			continue
+		}
+		fv := v.FieldByIndex(fs.index)
+		switch fv.Type().Kind() {
+		case reflect.String:
+			fv.SetString(string(value))
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			x, err := strconv.ParseInt(string(value), 10, fv.Type().Bits())
+			if err != nil {
+				return err
+			}
+			fv.SetInt(x)
+		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+			x, err := strconv.ParseUint(string(value), 10, fv.Type().Bits())
+			if err != nil {
+				return err
+			}
+			fv.SetUint(x)
+		case reflect.Float32, reflect.Float64:
+			x, err := strconv.ParseFloat(string(value), fv.Type().Bits())
+			if err != nil {
+				return err
+			}
+			fv.SetFloat(x)
+		case reflect.Bool:
+			x := len(value) != 0 && (len(value) != 1 || value[0] != '0')
+			fv.SetBool(x)
+		case reflect.Slice:
+			if fv.Type().Elem().Kind() != reflect.Uint8 {
+				// TODO: check field types in structSpec
+				panic("redigo: unsuported type for field " + string(name))
+			}
+			fv.SetBytes(value)
+		default:
+			// TODO: check field types in structSpec
+			panic("redigo: unsuported type for field " + string(name))
+		}
+	}
+	return nil
+}
+
+func FormatStruct(src interface{}) []interface{} {
+	v := reflect.ValueOf(src)
+	if v.Kind() == reflect.Ptr {
+		if v.IsNil() {
+			panic("redigo: FormatStruct argument must not be nil")
+		}
+		v = v.Elem()
+	}
+	if v.Kind() != reflect.Struct {
+		panic("redigo: FormatStruct argument must be a struct or pointer to a struct")
+	}
+	ss := structSpecForType(v.Type())
+
+	result := make([]interface{}, 0, 2*len(ss.l))
+	for _, fs := range ss.l {
+		fv := v.FieldByIndex(fs.index)
+		result = append(result, fs.name, fv.Interface())
+	}
+	return result
+}

+ 91 - 0
exp/scan/struct_test.go

@@ -0,0 +1,91 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package scan_test
+
+import (
+	"github.com/garyburd/redigo/exp/scan"
+	"reflect"
+	"testing"
+)
+
+var scanStructTests = []struct {
+	title string
+	reply []string
+	value interface{}
+}{
+	{"basic",
+		[]string{"i", "-1234", "u", "5678", "s", "hello", "p", "world", "b", "", "Bt", "1", "Bf", "0"},
+		&struct {
+			I  int    `redis:"i"`
+			U  uint   `redis:"u"`
+			S  string `redis:"s"`
+			P  []byte `redis:"p"`
+			B  bool   `redis:"b"`
+			Bt bool
+			Bf bool
+		}{
+			-1234, 5678, "hello", []byte("world"), false, true, false,
+		},
+	},
+}
+
+func TestScanStruct(t *testing.T) {
+	for _, tt := range scanStructTests {
+
+		var reply []interface{}
+		for _, v := range tt.reply {
+			reply = append(reply, []byte(v))
+		}
+
+		value := reflect.New(reflect.ValueOf(tt.value).Type().Elem())
+
+		if err := scan.ScanStruct(reply, value.Interface()); err != nil {
+			t.Fatalf("ScanStruct(%s) returned error %v", tt.title)
+		}
+
+		if !reflect.DeepEqual(value.Interface(), tt.value) {
+			t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value)
+		}
+	}
+}
+
+var formatStructTests = []struct {
+	title string
+	args  []interface{}
+	value interface{}
+}{
+	{"basic",
+		[]interface{}{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "Bt", true, "Bf", false},
+		&struct {
+			I  int    `redis:"i"`
+			U  uint   `redis:"u"`
+			S  string `redis:"s"`
+			P  []byte `redis:"p"`
+			Bt bool
+			Bf bool
+		}{
+			-1234, 5678, "hello", []byte("world"), true, false,
+		},
+	},
+}
+
+func TestFormatStruct(t *testing.T) {
+	for _, tt := range formatStructTests {
+		args := scan.FormatStruct(tt.value)
+		if !reflect.DeepEqual(args, tt.args) {
+			t.Fatalf("FormatStruct(%s) returned %v, want %v", tt.title, args, tt.args)
+		}
+	}
+}

+ 122 - 0
exp/scan/util.go

@@ -0,0 +1,122 @@
+// Copyright 2012 Gary Burd
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package scan
+
+import (
+	"errors"
+	"reflect"
+	"strings"
+	"sync"
+)
+
+type fieldSpec struct {
+	name      string
+	index     []int
+	omitEmpty bool
+}
+
+type structSpec struct {
+	m map[string]*fieldSpec
+	l []*fieldSpec
+}
+
+func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
+	return ss.m[string(name)]
+}
+
+func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
+	for i := 0; i < t.NumField(); i++ {
+		f := t.Field(i)
+		switch {
+		case f.PkgPath != "":
+			// Ignore unexported fields.
+		case f.Anonymous:
+			// TODO: Handle pointers. Requires change to decoder and 
+			// protection against infinite recursion.
+			if f.Type.Kind() == reflect.Struct {
+				compileStructSpec(f.Type, depth, append(index, i), ss)
+			}
+		default:
+			fs := &fieldSpec{name: f.Name}
+			tag := f.Tag.Get("redis")
+			p := strings.Split(tag, ",")
+			if len(p) > 0 && p[0] != "-" {
+				if len(p[0]) > 0 {
+					fs.name = p[0]
+				}
+				for _, s := range p[1:] {
+					switch s {
+					case "omitempty":
+						fs.omitEmpty = true
+					default:
+						panic(errors.New("redigo: unknown field flag " + s + " for type " + t.Name()))
+					}
+				}
+			}
+			d, found := depth[fs.name]
+			if !found {
+				d = 1 << 30
+			}
+			switch {
+			case len(index) == d:
+				// At same depth, remove from result.
+				delete(ss.m, fs.name)
+				j := 0
+				for i := 0; i < len(ss.l); i++ {
+					if fs.name != ss.l[i].name {
+						ss.l[j] = ss.l[i]
+						j += 1
+					}
+				}
+				ss.l = ss.l[:j]
+			case len(index) < d:
+				fs.index = make([]int, len(index)+1)
+				copy(fs.index, index)
+				fs.index[len(index)] = i
+				depth[fs.name] = len(index)
+				ss.m[fs.name] = fs
+				ss.l = append(ss.l, fs)
+			}
+		}
+	}
+}
+
+var (
+	structSpecMutex  sync.RWMutex
+	structSpecCache  = make(map[reflect.Type]*structSpec)
+	defaultFieldSpec = &fieldSpec{}
+)
+
+func structSpecForType(t reflect.Type) *structSpec {
+
+	structSpecMutex.RLock()
+	ss, found := structSpecCache[t]
+	structSpecMutex.RUnlock()
+	if found {
+		return ss
+	}
+
+	structSpecMutex.Lock()
+	defer structSpecMutex.Unlock()
+	ss, found = structSpecCache[t]
+	if found {
+		return ss
+	}
+
+	ss = &structSpec{m: make(map[string]*fieldSpec)}
+	compileStructSpec(t, make(map[string]int), nil, ss)
+	structSpecCache[t] = ss
+	return ss
+}