Permalink
Browse files

add schema; ability to save struct as redis hash using godis/exp clie…

…nt. todo: add tests
  • Loading branch information...
1 parent c792e43 commit c71e970a67538220cc56d7eab1dd58033fbf2753 Simon Zimmermann committed May 7, 2012
Showing with 382 additions and 0 deletions.
  1. +34 −0 schema/key.go
  2. +307 −0 schema/schema.go
  3. +41 −0 schema/tag.go
View
@@ -0,0 +1,34 @@
+package schema
+
+import (
+ "fmt"
+)
+
+type Key struct {
+ kind string
+ id int64
+}
+
+func (k *Key) String() string {
+ return fmt.Sprintf("%s:%d", k.kind, k.id)
+}
+
+func (k *Key) Count() string {
+ return fmt.Sprintf("%s:count", k.kind)
+}
+
+func (k *Key) Unique(field, value string) string {
+ return fmt.Sprintf("%s:unique:%s:%s", k.kind, field, value)
+}
+
+func (k *Key) Index(field, value string) string {
+ return fmt.Sprintf("%s:index:%s:%s", k.kind, field, value)
+}
+
+func (k *Key) Id() int64 {
+ return k.id
+}
+
+func NewKey(kind string, id int64) *Key {
+ return &Key{kind, id}
+}
View
@@ -0,0 +1,307 @@
+package schema
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+
+ "code.google.com/p/tcgl/monitoring"
+ "github.com/simonz05/godis/exp"
+)
+
+var (
+ nilError = errors.New("Key requested returned a nil reply")
+ typeError = errors.New("Invalid type, expected pointer to struct")
+ idFieldError = errors.New("Expected Id field of type int64 on struct")
+)
+
+/* logic:
+ input: key, *struct
+
+ a) Check if key has Id; yes/no
+ no) INCR k:count
+ set value as Id in struct
+
+ b) Parse struct; returns [field, value, field, value, ...], [hashField, ...].
+ the hashField slice contains any optional properties which should be applied
+ to before or after setting the struct in Redis (uniquity, index).
+
+ c) Check unique fields and set unique fields
+
+ d) Add the actual data from struct to a redis hash
+
+ e) Add optional indexes
+
+ f) if c, d, e did raise an error we need to cleanup the things
+ we changed in the database.
+*/
+func Put(db *redis.Client, k *Key, s interface{}) (*Key, error) {
+ mon := monitoring.BeginMeasuring("database:put")
+ defer mon.EndMeasuring()
+ prep := new(prepare)
+ prep.key = k
+
+ e := setId(db, s, prep)
+
+ if e != nil {
+ return nil, e
+ }
+
+ e = parseStruct(db, s, prep)
+
+ if e != nil {
+ return nil, e
+ }
+
+ for _, o := range prep.unique {
+ uk := k.Unique(o.name, fmt.Sprintf("%v", *o.value))
+ var reply *redis.Reply
+ reply, e = db.Call("SETNX", uk, k.Id())
+
+ if e != nil {
+ goto Error
+ }
+
+ if reply.Elem.Int() != 1 {
+ e = errors.New(fmt.Sprintf("expected unique `%s`:`%v`", o.name, o.value))
+ goto Error
+ }
+
+ prep.dirty = append(prep.dirty, uk)
+ }
+
+ _, e = db.Call(append([]interface{}{"HMSET", k.String()}, prep.args...)...)
+
+ for _, o := range prep.index {
+ ik := fmt.Sprintf("%v", *o.value)
+ _, e = db.Call("SET", k.Index(o.name, ik), k.Id())
+
+ if e != nil {
+ goto Error
+ }
+
+ prep.dirty = append(prep.dirty, ik)
+ }
+
+ return k, e
+Error:
+ cleanup(db, prep)
+ return nil, e
+}
+
+func cleanup(db *redis.Client, prep *prepare) error {
+ p := db.AsyncClient()
+
+ for _, k := range prep.dirty {
+ p.Call("DEL", k)
+ }
+
+ if prep.isNew {
+ p.Call("DEL", prep.key.String())
+ }
+
+ _, e := p.ReadAll()
+ return e
+}
+
+func Get(db *redis.Client, k *Key, s interface{}) error {
+ mon := monitoring.BeginMeasuring("database:get")
+ reply, e := db.Call("HGETALL", k.String())
+
+ if e != nil {
+ return e
+ }
+
+ if reply.Len() == 0 {
+ return nilError
+ }
+
+ e = inflate(db, s, reply.Hash())
+ mon.EndMeasuring()
+ return e
+}
+
+func SetUnique(db *redis.Client, key string) (bool, error) {
+ reply, e := db.Call("SETNX", key, 1)
+
+ if e != nil {
+ return false, e
+ }
+
+ return reply.Elem.Int() == 1, nil
+}
+
+func setId(db *redis.Client, s interface{}, prep *prepare) error {
+ if prep.key.Id() != 0 {
+ return nil
+ }
+
+ prep.isNew = true
+ reply, e := db.Call("INCR", prep.key.Count())
+
+ if e != nil {
+ return e
+ }
+
+ prep.key.id = reply.Elem.Int64()
+
+ if e != nil {
+ return e
+ }
+ v := reflect.ValueOf(s)
+
+ if v.Kind() != reflect.Ptr {
+ return typeError
+ }
+
+ v = v.Elem()
+
+ if v.Kind() != reflect.Struct {
+ return typeError
+ }
+
+ idField := v.FieldByName("Id")
+
+ if !idField.IsValid() || !idField.CanSet() || idField.Kind() != reflect.Int64 {
+ return idFieldError
+ }
+
+ idField.SetInt(prep.key.Id())
+ return nil
+}
+
+type hashField struct {
+ name string
+ value *interface{}
+}
+
+type prepare struct {
+ key *Key
+ unique []*hashField
+ index []*hashField
+ args []interface{}
+ dirty []string
+ isNew bool
+}
+
+// parseStruct takes a pointer to a struct or a struct.
+// We use the struct to fill an array of key:value from the struct.
+func parseStruct(db *redis.Client, s interface{}, prep *prepare) error {
+ v := reflect.ValueOf(s)
+
+ if v.Kind() != reflect.Ptr {
+ return typeError
+ }
+
+ v = v.Elem()
+
+ if v.Kind() != reflect.Struct {
+ return typeError
+ }
+
+ t := v.Type()
+ n := v.NumField()
+ args := make([]interface{}, 0, n*2)
+ argsLen := cap(args)
+ prep.unique = make([]*hashField, 0)
+ prep.index = make([]*hashField, 0)
+
+ for i := 0; i < n; i++ {
+ fieldType := t.Field(i)
+
+ if len(fieldType.PkgPath) > 0 || fieldType.Anonymous {
+ fmt.Printf("unexported field `%s`\n", fieldType.Name)
+ argsLen -= 2
+ continue
+ }
+
+ name, opt := parseTag(fieldType.Tag.Get("redis"))
+
+ if name == "" {
+ name = fieldType.Name
+ }
+
+ fieldValue := v.Field(i).Interface()
+ args = append(args, name, fieldValue)
+
+ unique := opt.Contains("unique")
+ index := opt.Contains("index")
+
+ if index || unique {
+ hf := &hashField{
+ name: name,
+ value: &fieldValue,
+ }
+
+ if index {
+ prep.index = append(prep.index, hf)
+ }
+
+ if unique {
+ prep.unique = append(prep.unique, hf)
+ }
+ }
+ }
+
+ prep.args = args[:argsLen]
+ return nil
+}
+
+// inflate takes a pointer to a struct as dst and a map with
+// values as src. The struct is then filled with the values
+// from the map.
+func inflate(db *redis.Client, dst interface{}, src map[string]redis.Elem) error {
+ v := reflect.ValueOf(dst)
+
+ if v.Kind() != reflect.Ptr {
+ return typeError
+ }
+
+ v = v.Elem()
+
+ if v.Kind() != reflect.Struct {
+ return typeError
+ }
+
+ t := v.Type()
+
+ for i := 0; i < v.NumField(); i++ {
+ fieldValue := v.Field(i)
+ fieldType := t.Field(i)
+
+ if !fieldValue.CanSet() {
+ fmt.Println("Field not setable", fieldType.Name)
+ continue
+ }
+
+ name, _ := parseTag(fieldType.Tag.Get("json"))
+
+ if name == "" {
+ name = fieldType.Name
+ }
+
+ value, ok := src[name]
+
+ if !ok {
+ fmt.Println("Value for field not in map:", name)
+ continue
+ }
+
+ switch fieldValue.Kind() {
+ case reflect.Int:
+ fieldValue.SetInt(int64(value.Int()))
+ case reflect.Int64:
+ fieldValue.SetInt(value.Int64())
+ case reflect.Float64:
+ fieldValue.SetFloat(value.Float64())
+ case reflect.Bool:
+ fieldValue.SetBool(value.Bool())
+ case reflect.String:
+ fieldValue.SetString(value.String())
+ default:
+ panic("type not supported: " + fieldValue.Type().String())
+ }
+ }
+
+ return nil
+}
View
@@ -0,0 +1,41 @@
+package schema
+
+import (
+ "strings"
+)
+
+// copy from the json package
+
+type tagOptions string
+
+// parse a struct field tag and options
+func parseTag(tag string) (string, tagOptions) {
+ if idx := strings.Index(tag, ","); idx != -1 {
+ return tag[:idx], tagOptions(tag[idx+1:])
+ }
+
+ return tag, tagOptions("")
+}
+
+// Contains returns whether checks that a comma-separated list of options
+// contains a particular substr flag. substr must be surrounded by a
+// string boundary or commas.
+func (o tagOptions) Contains(optionName string) bool {
+ if len(o) == 0 {
+ return false
+ }
+ s := string(o)
+ for s != "" {
+ var next string
+ i := strings.Index(s, ",")
+ if i >= 0 {
+ s, next = s[:i], s[i+1:]
+ }
+ if s == optionName {
+ return true
+ }
+ s = next
+ }
+ return false
+}
+

0 comments on commit c71e970

Please sign in to comment.