From f76b7e6a55d98a7a7d79fd93d526339a8b9b951c Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sun, 21 Sep 2014 18:20:44 +0100 Subject: [PATCH 01/18] Improved encoding performance by changing psuedo-type detection logic --- encoding/encoder.go | 6 ------ encoding/encoder_test.go | 6 ------ utils.go | 19 +++++++++++++------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/encoding/encoder.go b/encoding/encoder.go index b37feb26..9b0ec9d3 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -7,7 +7,6 @@ import ( "reflect" "runtime" "sort" - "time" ) // Encode returns the encoded value of v. @@ -44,11 +43,6 @@ func encode(v reflect.Value) (reflect.Value, error) { return reflect.Value{}, nil } - // Special cases - // Time should not be encoded as it is handled by the Expr method - if v.Type() == reflect.TypeOf(time.Time{}) { - return v, nil - } for _, hook := range encodeHooks { success, ret, err := hook(v) if err != nil { diff --git a/encoding/encoder_test.go b/encoding/encoder_test.go index 44840dbf..4b365d80 100644 --- a/encoding/encoder_test.go +++ b/encoding/encoder_test.go @@ -4,7 +4,6 @@ import ( "image" "reflect" "testing" - "time" ) var encodeExpected = map[string]interface{}{ @@ -108,9 +107,6 @@ type Optionals struct { Mr map[string]interface{} `gorethink:"mr"` Mo map[string]interface{} `gorethink:",omitempty"` - - Tr time.Time `gorethink:"tr"` - To time.Time `gorethink:",omitempty"` } var optionalsExpected = map[string]interface{}{ @@ -118,7 +114,6 @@ var optionalsExpected = map[string]interface{}{ "omitempty": 0, "slr": []interface{}(nil), "mr": map[string]interface{}{}, - "tr": time.Time{}, } func TestOmitEmpty(t *testing.T) { @@ -126,7 +121,6 @@ func TestOmitEmpty(t *testing.T) { o.Sw = "something" o.Mr = map[string]interface{}{} o.Mo = map[string]interface{}{} - o.Tr = time.Time{} got, err := Encode(&o) if err != nil { diff --git a/utils.go b/utils.go index 4915ca78..ac1d58ae 100644 --- a/utils.go +++ b/utils.go @@ -231,15 +231,22 @@ func protobufToString(p proto.Message, indentLevel int) string { return prefixLines(proto.MarshalTextString(p), strings.Repeat(" ", indentLevel)) } +var timeType = reflect.TypeOf(time.Time{}) +var termType = reflect.TypeOf(Term{}) + func encode(v interface{}) (interface{}, error) { + if _, ok := v.(Term); ok { + return v, nil + } + encoding.RegisterEncodeHook(func(v reflect.Value) (success bool, ret reflect.Value, err error) { - if v.Type() == reflect.TypeOf(time.Time{}) { - return true, v, nil - } else if v.Type() == reflect.TypeOf(Term{}) { - return true, v, nil - } else { - return false, v, nil + if v.Kind() == reflect.Struct { + if v.Type().ConvertibleTo(timeType) || v.Type().ConvertibleTo(termType) { + return true, v, nil + } } + + return false, v, nil }) return encoding.Encode(v) From 241dcadd58d7e0bea0d14f6eda8b426736f7533d Mon Sep 17 00:00:00 2001 From: Juuso Haavisto Date: Sat, 27 Sep 2014 13:23:32 +0300 Subject: [PATCH 02/18] golint: remove unnecessary else blocks, omit few types from variable initializations and change how few integers are manipulated --- encoding/decoder.go | 2 +- encoding/encoder.go | 2 +- encoding/encoder_test.go | 2 +- gorethink_test.go | 2 +- pseudotypes.go | 9 +++------ query.go | 3 +-- query_control.go | 13 ++++++------- session.go | 4 ++-- utils.go | 26 ++++++++++++-------------- 9 files changed, 28 insertions(+), 35 deletions(-) diff --git a/encoding/decoder.go b/encoding/decoder.go index ef35b0ae..af554ef0 100644 --- a/encoding/decoder.go +++ b/encoding/decoder.go @@ -331,7 +331,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error for _, key := range sv.MapKeys() { var subdv reflect.Value - var subsv reflect.Value = sv.MapIndex(key) + var subsv = sv.MapIndex(key) skey := key.Interface().(string) diff --git a/encoding/encoder.go b/encoding/encoder.go index b37feb26..cda55de1 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -97,7 +97,7 @@ func encode(v reflect.Value) (reflect.Value, error) { return reflect.Zero(reflect.TypeOf(map[string]interface{}{})), nil } - var sv stringValues = v.MapKeys() + var sv = v.MapKeys() sort.Sort(sv) for _, k := range sv { ev, err := encode(v.MapIndex(k)) diff --git a/encoding/encoder_test.go b/encoding/encoder_test.go index 44840dbf..797cca44 100644 --- a/encoding/encoder_test.go +++ b/encoding/encoder_test.go @@ -29,7 +29,7 @@ var encodeExpected = map[string]interface{}{ func TestEncode(t *testing.T) { // Top is defined in decoder_test.go - var in Top = Top{ + var in = Top{ Level0: 1, Embed0: Embed0{ Level1b: 2, diff --git a/gorethink_test.go b/gorethink_test.go index 65820c9a..85b5634b 100644 --- a/gorethink_test.go +++ b/gorethink_test.go @@ -176,7 +176,7 @@ type PseudoTypes struct { B []byte } -var str T = T{ +var str = T{ A: "A", B: 1, C: 1, diff --git a/pseudotypes.go b/pseudotypes.go index 5a6c7fa0..f1f79f91 100644 --- a/pseudotypes.go +++ b/pseudotypes.go @@ -141,9 +141,8 @@ func reqlGroupedDataToObj(obj map[string]interface{}) (interface{}, error) { }) } return ret, nil - } else { - return nil, fmt.Errorf("pseudo-type GROUPED_DATA object %v does not have the expected field \"data\"", obj) } + return nil, fmt.Errorf("pseudo-type GROUPED_DATA object %v does not have the expected field \"data\"", obj) } func reqlBinaryToNativeBytes(obj map[string]interface{}) (interface{}, error) { @@ -155,10 +154,8 @@ func reqlBinaryToNativeBytes(obj map[string]interface{}) (interface{}, error) { } return b, nil - } else { - return nil, fmt.Errorf("pseudo-type BINARY object %v field \"data\" is not valid", obj) } - } else { - return nil, fmt.Errorf("pseudo-type BINARY object %v does not have the expected field \"data\"", obj) + return nil, fmt.Errorf("pseudo-type BINARY object %v field \"data\" is not valid", obj) } + return nil, fmt.Errorf("pseudo-type BINARY object %v does not have the expected field \"data\"", obj) } diff --git a/query.go b/query.go index d774973c..23acda32 100644 --- a/query.go +++ b/query.go @@ -88,9 +88,8 @@ func (t Term) String() string { if t.rootTerm { return fmt.Sprintf("r.%s(%s)", t.name, strings.Join(allArgsToStringSlice(t.args, t.optArgs), ", ")) - } else { - return fmt.Sprintf("%s.%s(%s)", t.args[0].String(), t.name, strings.Join(allArgsToStringSlice(t.args[1:], t.optArgs), ", ")) } + return fmt.Sprintf("%s.%s(%s)", t.args[0].String(), t.name, strings.Join(allArgsToStringSlice(t.args[1:], t.optArgs), ", ")) } type WriteResponse struct { diff --git a/query_control.go b/query_control.go index 492b257b..d27046d4 100644 --- a/query_control.go +++ b/query_control.go @@ -104,14 +104,13 @@ func expr(value interface{}, depth int) Term { // Check if slice is a byte slice if typ.Elem().Kind() == reflect.Uint8 { return Binary(rval.Bytes()) - } else { - vals := []Term{} - for i := 0; i < rval.Len(); i++ { - vals = append(vals, expr(rval.Index(i).Interface(), depth)) - } - - return makeArray(vals) } + vals := []Term{} + for i := 0; i < rval.Len(); i++ { + vals = append(vals, expr(rval.Index(i).Interface(), depth)) + } + + return makeArray(vals) } if typ.Kind() == reflect.Map { vals := map[string]Term{} diff --git a/session.go b/session.go index b3c99567..10f0b1d2 100644 --- a/session.go +++ b/session.go @@ -245,7 +245,7 @@ func (s *Session) handleBatchResponse(cursor *Cursor, response *Response) { cursor.extend(response) s.Lock() - cursor.outstandingRequests -= 1 + cursor.outstandingRequests-- if response.Type != p.Response_SUCCESS_PARTIAL && response.Type != p.Response_SUCCESS_FEED && @@ -301,7 +301,7 @@ func (s *Session) asyncContinueQuery(cursor *Cursor) error { // stopQuery sends closes a query by sending Query_STOP to the server. func (s *Session) stopQuery(cursor *Cursor) error { cursor.mu.Lock() - cursor.outstandingRequests += 1 + cursor.outstandingRequests++ cursor.mu.Unlock() q := Query{ diff --git a/utils.go b/utils.go index 4915ca78..e52e818f 100644 --- a/utils.go +++ b/utils.go @@ -65,7 +65,7 @@ func makeObject(args termsObj) Term { } } -var nextVarId int64 = 0 +var nextVarId int64 func makeFunc(f interface{}) Term { value := reflect.ValueOf(f) @@ -102,9 +102,8 @@ func funcWrap(value interface{}) Term { return makeFunc(func(x Term) Term { return val }) - } else { - return val } + return val } func funcWrapArgs(args []interface{}) []interface{} { @@ -120,21 +119,20 @@ func funcWrapArgs(args []interface{}) []interface{} { func implVarScan(value Term) bool { if value.termType == p.Term_IMPLICIT_VAR { return true - } else { - for _, v := range value.args { - if implVarScan(v) { - return true - } + } + for _, v := range value.args { + if implVarScan(v) { + return true } + } - for _, v := range value.optArgs { - if implVarScan(v) { - return true - } + for _, v := range value.optArgs { + if implVarScan(v) { + return true } - - return false } + + return false } // Convert an opt args struct to a map. From c2d2510c271259b208809f2b2b6164b259fdb068 Mon Sep 17 00:00:00 2001 From: Juuso Haavisto Date: Sat, 27 Sep 2014 16:48:38 +0300 Subject: [PATCH 03/18] cancel the changes in encoding package because of failed CI build --- encoding/decoder.go | 2 +- encoding/encoder.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/encoding/decoder.go b/encoding/decoder.go index af554ef0..ef35b0ae 100644 --- a/encoding/decoder.go +++ b/encoding/decoder.go @@ -331,7 +331,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error for _, key := range sv.MapKeys() { var subdv reflect.Value - var subsv = sv.MapIndex(key) + var subsv reflect.Value = sv.MapIndex(key) skey := key.Interface().(string) diff --git a/encoding/encoder.go b/encoding/encoder.go index cda55de1..b37feb26 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -97,7 +97,7 @@ func encode(v reflect.Value) (reflect.Value, error) { return reflect.Zero(reflect.TypeOf(map[string]interface{}{})), nil } - var sv = v.MapKeys() + var sv stringValues = v.MapKeys() sort.Sort(sv) for _, k := range sv { ev, err := encode(v.MapIndex(k)) From c25fec4f689b08b6692727d5b4ffde00c75dfe29 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 27 Sep 2014 19:45:47 +0100 Subject: [PATCH 04/18] Added further improvements to encoding performance --- encoding/cache.go | 177 ++++++++++---------- encoding/decoder.go | 125 +++++++------- encoding/decoder_test.go | 6 +- encoding/encoder.go | 189 +++++----------------- encoding/encoder_test.go | 66 +++----- encoding/encoder_types.go | 331 ++++++++++++++++++++++++++++++++++++++ encoding/encoding.go | 18 +-- encoding/errors.go | 54 +++++++ encoding/utils.go | 59 +++++++ query.go | 2 +- query_control.go | 73 +++------ query_write.go | 6 +- types/time.go | 55 +++++++ utils.go | 24 ++- 14 files changed, 741 insertions(+), 444 deletions(-) create mode 100644 encoding/encoder_types.go create mode 100644 encoding/utils.go create mode 100644 types/time.go diff --git a/encoding/cache.go b/encoding/cache.go index 2545fe56..ee983cea 100644 --- a/encoding/cache.go +++ b/encoding/cache.go @@ -8,53 +8,69 @@ import ( "sync" ) -type FieldMapper interface { - FieldMap() map[string]string -} +// A field represents a single field found in a struct. +type field struct { + name string + nameBytes []byte // []byte(name) -// newCache returns a new cache. -func init() { - fieldCache.m = make(map[reflect.Type][]field) + tag bool + index []int + typ reflect.Type + omitEmpty bool + quoted bool } -// fieldCache caches meta-data about a struct. -var fieldCache struct { - l sync.RWMutex - m map[reflect.Type][]field +func fillField(f field) field { + f.nameBytes = []byte(f.name) + + return f } -func cachedTypeFields(v reflect.Value) []field { - t := v.Type() +// byName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from tag", then +// breaking ties with index sequence. +type byName []field - fieldCache.l.RLock() - f := fieldCache.m[t] - fieldCache.l.RUnlock() - if f != nil { - return f - } +func (x byName) Len() int { return len(x) } - // Compute fields without lock. - // Might duplicate effort but won't hold other computations back. - f = typeFields(v) - if f == nil { - f = []field{} +func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byName) Less(i, j int) bool { + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) +} - fieldCache.l.Lock() - if fieldCache.m == nil { - fieldCache.m = map[reflect.Type][]field{} +// byIndex sorts field by index sequence. +type byIndex []field + +func (x byIndex) Len() int { return len(x) } + +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } } - fieldCache.m[t] = f - fieldCache.l.Unlock() - return f + return len(x[i].index) < len(x[j].index) } // typeFields returns a list of fields that should be recognized for the given type. // The algorithm is breadth-first search over the set of structs to include - the top struct // and then any reachable anonymous structs. -func typeFields(v reflect.Value) []field { - t := v.Type() - +func typeFields(t reflect.Type) []field { // Anonymous fields to explore at the current level and the next. current := []field{} next := []field{{typ: t}} @@ -85,15 +101,10 @@ func typeFields(v reflect.Value) []field { if sf.PkgPath != "" { // unexported continue } - tag := getTag(sf) - - // Check if the field should be ignored if tag == "-" { continue } - - // Otherwise continue parsing tag name, opts := parseTag(tag) if !isValidTag(name) { name = "" @@ -114,7 +125,14 @@ func typeFields(v reflect.Value) []field { if name == "" { name = sf.Name } - fields = append(fields, field{name, tagged, index, ft, opts.Contains("omitempty")}) + fields = append(fields, fillField(field{ + name: name, + tag: tagged, + index: index, + typ: ft, + omitEmpty: opts.Contains("omitempty"), + quoted: opts.Contains("string"), + })) if count[f.typ] > 1 { // If there were multiple instances, add a second, // so that the annihilation code will see a duplicate. @@ -128,7 +146,7 @@ func typeFields(v reflect.Value) []field { // Record new anonymous struct to explore in next round. nextCount[ft]++ if nextCount[ft] == 1 { - next = append(next, field{name: ft.Name(), index: index, typ: ft}) + next = append(next, fillField(field{name: ft.Name(), index: index, typ: ft})) } } } @@ -137,7 +155,7 @@ func typeFields(v reflect.Value) []field { sort.Sort(byName(fields)) // Delete all fields that are hidden by the Go rules for embedded fields, - // except that fields with tags are promoted. + // except that fields with valid tags are promoted. // The fields are sorted in primary order of name, secondary order // of field index length. Loop over names; for each name, delete @@ -165,18 +183,6 @@ func typeFields(v reflect.Value) []field { } fields = out - - var fieldMapperType = reflect.TypeOf(new(FieldMapper)).Elem() - if t.Implements(fieldMapperType) { - fm := v.Interface().(FieldMapper).FieldMap() - - for index, field := range fields { - if mappedName, ok := fm[field.name]; ok { - fields[index].name = mappedName - } - } - } - sort.Sort(byIndex(fields)) return fields @@ -185,7 +191,7 @@ func typeFields(v reflect.Value) []field { // dominantField looks through the fields, all of which are known to // have the same name, to find the single field that dominates the // others using Go's embedding rules, modified by the presence of -// tags. If there are multiple top-level fields, the boolean +// valid tags. If there are multiple top-level fields, the boolean // will be false: This condition is an error in Go and we skip all // the fields. func dominantField(fields []field) (field, bool) { @@ -220,53 +226,32 @@ func dominantField(fields []field) (field, bool) { return fields[0], true } -// ---------------------------------------------------------------------------- - -type field struct { - name string - tag bool - index []int - typ reflect.Type - omitEmpty bool +var fieldCache struct { + sync.RWMutex + m map[reflect.Type][]field } -// byName sorts field by name, breaking ties with depth, -// then breaking ties with "name came from gorethink tag", then -// breaking ties with index sequence. -type byName []field - -func (x byName) Len() int { return len(x) } - -func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } - -func (x byName) Less(i, j int) bool { - if x[i].name != x[j].name { - return x[i].name < x[j].name - } - if len(x[i].index) != len(x[j].index) { - return len(x[i].index) < len(x[j].index) - } - if x[i].tag != x[j].tag { - return x[i].tag +// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. +func cachedTypeFields(t reflect.Type) []field { + fieldCache.RLock() + f := fieldCache.m[t] + fieldCache.RUnlock() + if f != nil { + return f } - return byIndex(x).Less(i, j) -} -// byIndex sorts field by index sequence. -type byIndex []field - -func (x byIndex) Len() int { return len(x) } - -func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = typeFields(t) + if f == nil { + f = []field{} + } -func (x byIndex) Less(i, j int) bool { - for k, xik := range x[i].index { - if k >= len(x[j].index) { - return false - } - if xik != x[j].index[k] { - return xik < x[j].index[k] - } + fieldCache.Lock() + if fieldCache.m == nil { + fieldCache.m = map[reflect.Type][]field{} } - return len(x[i].index) < len(x[j].index) + fieldCache.m[t] = f + fieldCache.Unlock() + return f } diff --git a/encoding/decoder.go b/encoding/decoder.go index ef35b0ae..68f8cbbc 100644 --- a/encoding/decoder.go +++ b/encoding/decoder.go @@ -1,11 +1,13 @@ -// This code is based on encoding/json and gorilla/schema +// // This code is based on encoding/json and gorilla/schema package encoding import ( // "errors" + "errors" "reflect" + "runtime" // "runtime" "strconv" "strings" @@ -14,44 +16,31 @@ import ( // Decode decodes map[string]interface{} into a struct. The first parameter // must be a pointer. func Decode(dst interface{}, src interface{}) (err error) { - // defer func() { - // if r := recover(); r != nil { - // if _, ok := r.(runtime.Error); ok { - // panic(r) - // } - // if v, ok := r.(string); ok { - // err = errors.New(v) - // } else { - // err = r.(error) - // } - // } - // }() + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + if v, ok := r.(string); ok { + err = errors.New(v) + } else { + err = r.(error) + } + } + }() dv := reflect.ValueOf(dst) sv := reflect.ValueOf(src) - if dv.Kind() != reflect.Ptr || dv.IsNil() { return &InvalidDecodeError{reflect.TypeOf(dst)} } - s := &decodeState{} - decode(s, dv, sv) - return s.savedError -} - -type decodeState struct { - savedError error -} - -// saveError saves the first err it is called with. -func (d *decodeState) saveError(err error) { - if d.savedError == nil { - d.savedError = err - } + decode(dv, sv) + return nil } // decodeInterface decodes the source value into the destination value -func decode(s *decodeState, dv, sv reflect.Value) { +func decode(dv, sv reflect.Value) { if dv.IsValid() && sv.IsValid() { // Ensure that the source value has the correct type of parsing if sv.Kind() == reflect.Interface { @@ -60,11 +49,11 @@ func decode(s *decodeState, dv, sv reflect.Value) { switch sv.Kind() { default: - decodeLiteral(s, dv, sv) + decodeLiteral(dv, sv) case reflect.Slice, reflect.Array: - decodeArray(s, dv, sv) + decodeArray(dv, sv) case reflect.Map: - decodeObject(s, dv, sv) + decodeObject(dv, sv) case reflect.Struct: dv = indirect(dv, false) dv.Set(sv) @@ -74,7 +63,7 @@ func decode(s *decodeState, dv, sv reflect.Value) { // decodeLiteral decodes the source value into the destination value. This function // is used to decode literal values. -func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { +func decodeLiteral(dv reflect.Value, sv reflect.Value) { dv = indirect(dv, true) // Special case for if sv is nil: @@ -94,7 +83,7 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { case bool: switch dv.Kind() { default: - s.saveError(&DecodeTypeError{"bool", dv.Type()}) + panic(&DecodeTypeError{"bool", dv.Type()}) return case reflect.Bool: dv.SetBool(value) @@ -104,7 +93,7 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { if dv.NumMethod() == 0 { dv.Set(reflect.ValueOf(value)) } else { - s.saveError(&DecodeTypeError{"bool", dv.Type()}) + panic(&DecodeTypeError{"bool", dv.Type()}) return } } @@ -112,35 +101,35 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { case string: switch dv.Kind() { default: - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return case reflect.String: dv.SetString(value) case reflect.Bool: b, err := strconv.ParseBool(value) if err != nil { - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return } dv.SetBool(b) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(value, 10, 64) if err != nil || dv.OverflowInt(n) { - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return } dv.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: n, err := strconv.ParseUint(value, 10, 64) if err != nil || dv.OverflowUint(n) { - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return } dv.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(value, 64) if err != nil || dv.OverflowFloat(n) { - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return } dv.SetFloat(n) @@ -148,7 +137,7 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { if dv.NumMethod() == 0 { dv.Set(reflect.ValueOf(string(value))) } else { - s.saveError(&DecodeTypeError{"string", dv.Type()}) + panic(&DecodeTypeError{"string", dv.Type()}) return } } @@ -156,11 +145,11 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { case int, int8, int16, int32, int64: switch dv.Kind() { default: - s.saveError(&DecodeTypeError{"int", dv.Type()}) + panic(&DecodeTypeError{"int", dv.Type()}) return case reflect.Interface: if dv.NumMethod() != 0 { - s.saveError(&DecodeTypeError{"int", dv.Type()}) + panic(&DecodeTypeError{"int", dv.Type()}) return } dv.Set(reflect.ValueOf(value)) @@ -177,11 +166,11 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { case uint, uint8, uint16, uint32, uint64: switch dv.Kind() { default: - s.saveError(&DecodeTypeError{"uint", dv.Type()}) + panic(&DecodeTypeError{"uint", dv.Type()}) return case reflect.Interface: if dv.NumMethod() != 0 { - s.saveError(&DecodeTypeError{"uint", dv.Type()}) + panic(&DecodeTypeError{"uint", dv.Type()}) return } dv.Set(reflect.ValueOf(value)) @@ -198,11 +187,11 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { case float32, float64: switch dv.Kind() { default: - s.saveError(&DecodeTypeError{"float", dv.Type()}) + panic(&DecodeTypeError{"float", dv.Type()}) return case reflect.Interface: if dv.NumMethod() != 0 { - s.saveError(&DecodeTypeError{"float", dv.Type()}) + panic(&DecodeTypeError{"float", dv.Type()}) return } dv.Set(reflect.ValueOf(value)) @@ -217,7 +206,7 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { dv.SetString(strconv.FormatFloat(float64(reflect.ValueOf(value).Float()), 'g', -1, 64)) } default: - s.saveError(&DecodeTypeError{sv.Type().String(), dv.Type()}) + panic(&DecodeTypeError{sv.Type().String(), dv.Type()}) return } @@ -226,7 +215,7 @@ func decodeLiteral(s *decodeState, dv reflect.Value, sv reflect.Value) { // decodeArray decodes the source value into the destination value. This function // is used when the source value is a slice or array. -func decodeArray(s *decodeState, dv reflect.Value, sv reflect.Value) { +func decodeArray(dv reflect.Value, sv reflect.Value) { dv = indirect(dv, false) dt := dv.Type() @@ -235,14 +224,14 @@ func decodeArray(s *decodeState, dv reflect.Value, sv reflect.Value) { case reflect.Interface: if dv.NumMethod() == 0 { // Decoding into nil interface? Switch to non-reflect code. - dv.Set(reflect.ValueOf(decodeArrayInterface(s, sv))) + dv.Set(reflect.ValueOf(decodeArrayInterface(sv))) return } // Otherwise it's invalid. fallthrough default: - s.saveError(&DecodeTypeError{"array", dv.Type()}) + panic(&DecodeTypeError{"array", dv.Type()}) return case reflect.Array: case reflect.Slice: @@ -275,10 +264,10 @@ func decodeArray(s *decodeState, dv reflect.Value, sv reflect.Value) { if i < dv.Len() { // Decode into element. - decode(s, dv.Index(i), sv.Index(i)) + decode(dv.Index(i), sv.Index(i)) } else { // Ran out of fixed array: skip. - decode(s, reflect.Value{}, sv.Index(i)) + decode(reflect.Value{}, sv.Index(i)) } i++ @@ -300,13 +289,13 @@ func decodeArray(s *decodeState, dv reflect.Value, sv reflect.Value) { // decodeObject decodes the source value into the destination value. This function // is used when the source value is a map or struct. -func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error) { +func decodeObject(dv reflect.Value, sv reflect.Value) (err error) { dv = indirect(dv, false) dt := dv.Type() // Decoding into nil interface? Switch to non-reflect code. if dv.Kind() == reflect.Interface && dv.NumMethod() == 0 { - dv.Set(reflect.ValueOf(decodeObjectInterface(s, sv))) + dv.Set(reflect.ValueOf(decodeObjectInterface(sv))) return nil } @@ -315,7 +304,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error case reflect.Map: // map must have string kind if dt.Key().Kind() != reflect.String { - s.saveError(&DecodeTypeError{"object", dv.Type()}) + panic(&DecodeTypeError{"object", dv.Type()}) break } if dv.IsNil() { @@ -323,7 +312,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error } case reflect.Struct: default: - s.saveError(&DecodeTypeError{"object", dv.Type()}) + panic(&DecodeTypeError{"object", dv.Type()}) return } @@ -345,7 +334,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error subdv = mapElem } else { var f *field - fields := cachedTypeFields(dv) + fields := cachedTypeFields(dv.Type()) for i := range fields { ff := &fields[i] if ff.name == skey { @@ -370,7 +359,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error } } - decode(s, subdv, subsv) + decode(subdv, subsv) if dv.Kind() == reflect.Map { kv := reflect.ValueOf(skey) @@ -385,7 +374,7 @@ func decodeObject(s *decodeState, dv reflect.Value, sv reflect.Value) (err error // less reflection // decodeInterface decodes the source value into interface{} -func decodeInterface(s *decodeState, sv reflect.Value) interface{} { +func decodeInterface(sv reflect.Value) interface{} { // Ensure that the source value has the correct type of parsing if sv.Kind() == reflect.Interface { sv = reflect.ValueOf(sv.Interface()) @@ -393,34 +382,34 @@ func decodeInterface(s *decodeState, sv reflect.Value) interface{} { switch sv.Kind() { case reflect.Slice, reflect.Array: - return decodeArrayInterface(s, sv) + return decodeArrayInterface(sv) case reflect.Map: - return decodeObjectInterface(s, sv) + return decodeObjectInterface(sv) default: - return decodeLiteralInterface(s, sv) + return decodeLiteralInterface(sv) } } // decodeArrayInterface decodes the source value into []interface{} -func decodeArrayInterface(s *decodeState, sv reflect.Value) []interface{} { +func decodeArrayInterface(sv reflect.Value) []interface{} { arr := []interface{}{} for i := 0; i < sv.Len(); i++ { - arr = append(arr, decodeInterface(s, sv.Index(i))) + arr = append(arr, decodeInterface(sv.Index(i))) } return arr } // decodeObjectInterface decodes the source value into map[string]interface{} -func decodeObjectInterface(s *decodeState, sv reflect.Value) map[string]interface{} { +func decodeObjectInterface(sv reflect.Value) map[string]interface{} { m := map[string]interface{}{} for _, key := range sv.MapKeys() { - m[key.Interface().(string)] = decodeInterface(s, sv.MapIndex(key)) + m[key.Interface().(string)] = decodeInterface(sv.MapIndex(key)) } return m } // decodeLiteralInterface returns the interface of the source value -func decodeLiteralInterface(s *decodeState, sv reflect.Value) interface{} { +func decodeLiteralInterface(sv reflect.Value) interface{} { if !sv.IsValid() { return nil } diff --git a/encoding/decoder_test.go b/encoding/decoder_test.go index 6c2d4538..3a75bb42 100644 --- a/encoding/decoder_test.go +++ b/encoding/decoder_test.go @@ -145,13 +145,13 @@ var decodeTests = []decodeTest{ {in: float64(2.0), ptr: new(interface{}), out: float64(2.0)}, {in: string("2"), ptr: new(interface{}), out: string("2")}, {in: "a\u1234", ptr: new(string), out: "a\u1234"}, - {in: map[string]interface{}{"X": []interface{}{1, 2, 3}, "Y": 4}, ptr: new(T), out: T{Y: 4}, err: &DecodeTypeError{"array", reflect.TypeOf("")}}, + {in: map[string]interface{}{"X": []interface{}{1, 2, 3}, "Y": 4}, ptr: new(T), out: T{}, err: &DecodeTypeError{"array", reflect.TypeOf("")}}, {in: map[string]interface{}{"x": 1}, ptr: new(tx), out: tx{}}, {in: map[string]interface{}{"F1": float64(1), "F2": 2, "F3": 3}, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: string("3")}}, {in: map[string]interface{}{"F1": string("1"), "F2": 2, "F3": 3}, ptr: new(V), out: V{F1: string("1"), F2: int32(2), F3: string("3")}}, { - in: map[string]interface{}{"k1": 1, "k2": "s", "k3": []interface{}{1, 2.0, 3e-3}, "k4": map[string]interface{}{"kk1": "s", "kk2": 2}}, - out: map[string]interface{}{"k1": 1, "k2": "s", "k3": []interface{}{1, 2.0, 3e-3}, "k4": map[string]interface{}{"kk1": "s", "kk2": 2}}, + in: map[string]interface{}{"k1": int64(1), "k2": "s", "k3": []interface{}{int64(1), 2.0, 3e-3}, "k4": map[string]interface{}{"kk1": "s", "kk2": int64(2)}}, + out: map[string]interface{}{"k1": int64(1), "k2": "s", "k3": []interface{}{int64(1), 2.0, 3e-3}, "k4": map[string]interface{}{"kk1": "s", "kk2": int64(2)}}, ptr: new(interface{}), }, diff --git a/encoding/encoder.go b/encoding/encoder.go index 9b0ec9d3..b6de2c29 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -6,9 +6,11 @@ import ( "errors" "reflect" "runtime" - "sort" + "sync" ) +type encoderFunc func(v reflect.Value) interface{} + // Encode returns the encoded value of v. // // Encode traverses the value v recursively and looks for structs. If a struct @@ -28,160 +30,55 @@ func Encode(v interface{}) (ev interface{}, err error) { } }() - val, err := encode(reflect.ValueOf(v)) - if err != nil { - return nil, err - } + return encode(reflect.ValueOf(v)), nil +} - ev = val.Interface() +func encode(v reflect.Value) interface{} { + return valueEncoder(v)(v) +} - return +var encoderCache struct { + sync.RWMutex + m map[reflect.Type]encoderFunc } -func encode(v reflect.Value) (reflect.Value, error) { +func valueEncoder(v reflect.Value) encoderFunc { if !v.IsValid() { - return reflect.Value{}, nil - } - - for _, hook := range encodeHooks { - success, ret, err := hook(v) - if err != nil { - return ret, err - } - if success { - return ret, nil - } - } - - switch v.Kind() { - case - reflect.Bool, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Float32, reflect.Float64, - reflect.String: - return v, nil - case reflect.Struct: - // If the value is a struct then get the name used by each field and - // insert the encoded values into a map - m := reflect.MakeMap(reflect.TypeOf(map[string]interface{}{})) - - for _, f := range cachedTypeFields(v) { - fv := fieldByIndex(v, f.index) - if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) { - continue - } - - ev, err := encode(fv) - if err != nil { - return reflect.Value{}, err - } - m.SetMapIndex(reflect.ValueOf(f.name), ev) - } - - return m, nil - case reflect.Map: - // If the value is a map then encode each element - m := reflect.MakeMap(reflect.TypeOf(map[string]interface{}{})) - - if v.Type().Key().Kind() != reflect.String { - return reflect.Value{}, &UnsupportedTypeError{v.Type()} - } - if v.IsNil() { - return reflect.Zero(reflect.TypeOf(map[string]interface{}{})), nil - } - - var sv stringValues = v.MapKeys() - sort.Sort(sv) - for _, k := range sv { - ev, err := encode(v.MapIndex(k)) - if err != nil { - return reflect.Value{}, err - } - - m.SetMapIndex(k, ev) - } - - return m, nil - case reflect.Slice: - // If the value is a slice then encode each element - s := reflect.MakeSlice(reflect.TypeOf([]interface{}{}), v.Len(), v.Len()) - - if v.IsNil() { - return reflect.Zero(reflect.TypeOf([]interface{}{})), nil - } - - for i := 0; i < v.Len(); i++ { - ev, err := encode(v.Index(i)) - if err != nil { - return reflect.Value{}, err - } - - s.Index(i).Set(ev) - } - - return s, nil - case reflect.Array: - // If the value is a array then encode each element - s := reflect.MakeSlice(reflect.TypeOf([]interface{}{}), v.Len(), v.Len()) - for i := 0; i < v.Len(); i++ { - ev, err := encode(v.Index(i)) - if err != nil { - return reflect.Value{}, err - } - - s.Index(i).Set(ev) - } - return s, nil - case reflect.Interface, reflect.Ptr: - // If the value is an interface or pointer then encode its element - if v.IsNil() { - return reflect.Value{}, nil - } - - return encode(v.Elem()) - default: - return reflect.Value{}, &UnsupportedTypeError{v.Type()} + return invalidValueEncoder } + return typeEncoder(v.Type()) } -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() +func typeEncoder(t reflect.Type) encoderFunc { + encoderCache.RLock() + f := encoderCache.m[t] + encoderCache.RUnlock() + if f != nil { + return f } - return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) -} - -func fieldByIndex(v reflect.Value, index []int) reflect.Value { - for _, i := range index { - if v.Kind() == reflect.Ptr { - if v.IsNil() { - return reflect.Value{} - } - v = v.Elem() - } - v = v.Field(i) + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + encoderCache.Lock() + if encoderCache.m == nil { + encoderCache.m = make(map[reflect.Type]encoderFunc) + } + var wg sync.WaitGroup + wg.Add(1) + encoderCache.m[t] = func(v reflect.Value) interface{} { + wg.Wait() + return f(v) } - return v + encoderCache.Unlock() + + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = newTypeEncoder(t, true) + wg.Done() + encoderCache.Lock() + encoderCache.m[t] = f + encoderCache.Unlock() + return f } - -// stringValues is a slice of reflect.Value holding *reflect.StringValue. -// It implements the methods to sort by string. -type stringValues []reflect.Value - -func (sv stringValues) Len() int { return len(sv) } -func (sv stringValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] } -func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) } -func (sv stringValues) get(i int) string { return sv[i].String() } diff --git a/encoding/encoder_test.go b/encoding/encoder_test.go index 4b365d80..b2535516 100644 --- a/encoding/encoder_test.go +++ b/encoding/encoder_test.go @@ -7,23 +7,23 @@ import ( ) var encodeExpected = map[string]interface{}{ - "Level0": 1, - "Level1b": 2, - "Level1c": 3, - "Level1a": 5, - "LEVEL1B": 6, + "Level0": int64(1), + "Level1b": int64(2), + "Level1c": int64(3), + "Level1a": int64(5), + "LEVEL1B": int64(6), "e": map[string]interface{}{ - "Level1a": 8, - "Level1b": 9, - "Level1c": 10, - "Level1d": 11, - "x": 12, + "Level1a": int64(8), + "Level1b": int64(9), + "Level1c": int64(10), + "Level1d": int64(11), + "x": int64(12), }, - "Loop1": 13, - "Loop2": 14, - "X": 15, - "Y": 16, - "Z": 17, + "Loop1": int64(13), + "Loop2": int64(14), + "X": int64(15), + "Y": int64(16), + "Z": int64(17), } func TestEncode(t *testing.T) { @@ -66,34 +66,6 @@ func TestEncode(t *testing.T) { } } -type FieldMappable struct { - Str string - Int int -} - -func (f FieldMappable) FieldMap() map[string]string { - return map[string]string{ - "Str": "str", - "Int": "int", - } -} - -func TestFieldMapper(t *testing.T) { - var in = FieldMappable{"string", 123} - var out = map[string]interface{}{ - "str": "string", - "int": 123, - } - - got, err := Encode(&in) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, out) { - t.Errorf(" got: %v\nwant: %v\n", got, out) - } -} - type Optionals struct { Sr string `gorethink:"sr"` So string `gorethink:"so,omitempty"` @@ -111,8 +83,8 @@ type Optionals struct { var optionalsExpected = map[string]interface{}{ "sr": "", - "omitempty": 0, - "slr": []interface{}(nil), + "omitempty": int64(0), + "slr": []interface{}{}, "mr": map[string]interface{}{}, } @@ -140,7 +112,7 @@ type MyStruct struct { func TestAnonymousNonstruct(t *testing.T) { var i IntType = 11 a := MyStruct{i} - var want = map[string]interface{}{"IntType": IntType(11)} + var want = map[string]interface{}{"IntType": int64(11)} got, err := Encode(a) if err != nil { @@ -193,7 +165,7 @@ func TestEmbeddedBug(t *testing.T) { if err != nil { t.Fatal("Encode:", err) } - want = map[string]interface{}{"A": 23} + want = map[string]interface{}{"A": int64(23)} if !reflect.DeepEqual(got, want) { t.Fatalf("Encode: got %v want %v", got, want) } diff --git a/encoding/encoder_types.go b/encoding/encoder_types.go new file mode 100644 index 00000000..32e1f53e --- /dev/null +++ b/encoding/encoder_types.go @@ -0,0 +1,331 @@ +package encoding + +import ( + "encoding" + "encoding/base64" + "math" + "reflect" + "strconv" + "time" +) + +var ( + marshalerType = reflect.TypeOf(new(Marshaler)).Elem() + textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() + + timeType = reflect.TypeOf(new(time.Time)).Elem() +) + +// newTypeEncoder constructs an encoderFunc for a type. +// The returned encoder only checks CanAddr when allowAddr is true. +func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { + if t.Implements(marshalerType) { + return marshalerEncoder + } + if t.Kind() != reflect.Ptr && allowAddr { + if reflect.PtrTo(t).Implements(marshalerType) { + return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) + } + } + // Check for psuedo-types first + switch t { + case timeType: + return timePseudoTypeEncoder + } + + switch t.Kind() { + case reflect.Bool: + return boolEncoder + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return intEncoder + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return uintEncoder + case reflect.Float32: + return float32Encoder + case reflect.Float64: + return float64Encoder + case reflect.String: + return stringEncoder + case reflect.Interface: + return interfaceEncoder + case reflect.Struct: + return newStructEncoder(t) + case reflect.Map: + return newMapEncoder(t) + case reflect.Slice: + return newSliceEncoder(t) + case reflect.Array: + return newArrayEncoder(t) + case reflect.Ptr: + return newPtrEncoder(t) + default: + return unsupportedTypeEncoder + } +} + +func invalidValueEncoder(v reflect.Value) interface{} { + return nil +} + +func marshalerEncoder(v reflect.Value) interface{} { + if v.Kind() == reflect.Ptr && v.IsNil() { + return nil + } + m := v.Interface().(Marshaler) + ev, err := m.MarshalRQL() + if err != nil { + panic(&MarshalerError{v.Type(), err}) + } + + return ev +} + +func addrMarshalerEncoder(v reflect.Value) interface{} { + va := v.Addr() + if va.IsNil() { + return nil + } + m := va.Interface().(Marshaler) + ev, err := m.MarshalRQL() + if err != nil { + panic(&MarshalerError{v.Type(), err}) + } + + return ev +} + +func textMarshalerEncoder(v reflect.Value) interface{} { + if v.Kind() == reflect.Ptr && v.IsNil() { + return "" + } + m := v.Interface().(encoding.TextMarshaler) + b, err := m.MarshalText() + if err != nil { + panic(&MarshalerError{v.Type(), err}) + } + + return b +} + +func addrTextMarshalerEncoder(v reflect.Value) interface{} { + va := v.Addr() + if va.IsNil() { + return "" + } + m := va.Interface().(encoding.TextMarshaler) + b, err := m.MarshalText() + if err != nil { + panic(&MarshalerError{v.Type(), err}) + } + + return b +} + +func boolEncoder(v reflect.Value) interface{} { + if v.Bool() { + return true + } else { + return false + } +} + +func intEncoder(v reflect.Value) interface{} { + return v.Int() +} + +func uintEncoder(v reflect.Value) interface{} { + return v.Uint() +} + +type floatEncoder int // number of bits + +func (bits floatEncoder) encode(v reflect.Value) interface{} { + f := v.Float() + if math.IsInf(f, 0) || math.IsNaN(f) { + panic(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, int(bits))}) + } + return f +} + +var ( + float32Encoder = (floatEncoder(32)).encode + float64Encoder = (floatEncoder(64)).encode +) + +func stringEncoder(v reflect.Value) interface{} { + return v.String() +} + +func interfaceEncoder(v reflect.Value) interface{} { + if v.IsNil() { + return nil + } + return encode(v.Elem()) +} + +func unsupportedTypeEncoder(v reflect.Value) interface{} { + panic(&UnsupportedTypeError{v.Type()}) +} + +type structEncoder struct { + fields []field + fieldEncs []encoderFunc +} + +func (se *structEncoder) encode(v reflect.Value) interface{} { + m := make(map[string]interface{}) + + for i, f := range se.fields { + fv := fieldByIndex(v, f.index) + if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) { + continue + } + + m[f.name] = se.fieldEncs[i](fv) + } + + return m +} + +func newStructEncoder(t reflect.Type) encoderFunc { + fields := cachedTypeFields(t) + se := &structEncoder{ + fields: fields, + fieldEncs: make([]encoderFunc, len(fields)), + } + for i, f := range fields { + se.fieldEncs[i] = typeEncoder(typeByIndex(t, f.index)) + } + return se.encode +} + +type mapEncoder struct { + elemEnc encoderFunc +} + +func (me *mapEncoder) encode(v reflect.Value) interface{} { + if v.IsNil() { + return nil + } + + m := make(map[string]interface{}) + + for _, k := range v.MapKeys() { + m[k.String()] = me.elemEnc(v.MapIndex(k)) + } + + return m +} + +func newMapEncoder(t reflect.Type) encoderFunc { + if t.Key().Kind() != reflect.String { + return unsupportedTypeEncoder + } + me := &mapEncoder{typeEncoder(t.Elem())} + return me.encode +} + +// sliceEncoder just wraps an arrayEncoder, checking to make sure the value isn't nil. +type sliceEncoder struct { + arrayEnc encoderFunc +} + +func (se *sliceEncoder) encode(v reflect.Value) interface{} { + if v.IsNil() { + return []interface{}{} + } + return se.arrayEnc(v) +} + +func newSliceEncoder(t reflect.Type) encoderFunc { + // Byte slices get special treatment; arrays don't. + if t.Elem().Kind() == reflect.Uint8 { + return encodeByteSlice + } + enc := &sliceEncoder{newArrayEncoder(t)} + return enc.encode +} + +type arrayEncoder struct { + elemEnc encoderFunc +} + +func (ae *arrayEncoder) encode(v reflect.Value) interface{} { + n := v.Len() + + a := make([]interface{}, n) + for i := 0; i < n; i++ { + a[i] = ae.elemEnc(v.Index(i)) + } + + return a +} + +func newArrayEncoder(t reflect.Type) encoderFunc { + enc := &arrayEncoder{typeEncoder(t.Elem())} + return enc.encode +} + +type ptrEncoder struct { + elemEnc encoderFunc +} + +func (pe *ptrEncoder) encode(v reflect.Value) interface{} { + if v.IsNil() { + return nil + } + return pe.elemEnc(v.Elem()) +} + +func newPtrEncoder(t reflect.Type) encoderFunc { + enc := &ptrEncoder{typeEncoder(t.Elem())} + return enc.encode +} + +type condAddrEncoder struct { + canAddrEnc, elseEnc encoderFunc +} + +func (ce *condAddrEncoder) encode(v reflect.Value) interface{} { + if v.CanAddr() { + return ce.canAddrEnc(v) + } else { + return ce.elseEnc(v) + } +} + +// newCondAddrEncoder returns an encoder that checks whether its value +// CanAddr and delegates to canAddrEnc if so, else to elseEnc. +func newCondAddrEncoder(canAddrEnc, elseEnc encoderFunc) encoderFunc { + enc := &condAddrEncoder{canAddrEnc: canAddrEnc, elseEnc: elseEnc} + return enc.encode +} + +// Pseudo-type encoders + +// Encode a time.Time value to the TIME RQL type +func timePseudoTypeEncoder(v reflect.Value) interface{} { + t := v.Interface().(time.Time) + + return map[string]interface{}{ + "$REQL_TYPE": "TIME", + "timestamp": t.Unix(), + "timezone": "+00:00", + } +} + +// Encode a byte slice to the BINARY RQL type +func encodeByteSlice(v reflect.Value) interface{} { + var b []byte + if !v.IsNil() { + b = v.Bytes() + } + + dst := make([]byte, base64.StdEncoding.EncodedLen(len(b))) + base64.StdEncoding.Encode(dst, b) + + return map[string]interface{}{ + "$REQL_TYPE": "BINARY", + "data": dst, + } +} diff --git a/encoding/encoding.go b/encoding/encoding.go index a2bae1da..57d0755e 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -1,15 +1,13 @@ package encoding -import "reflect" - -type EncodeHook func(src reflect.Value) (success bool, ret reflect.Value, err error) - -var encodeHooks []EncodeHook - -func init() { - encodeHooks = []EncodeHook{} +// Marshaler is the interface implemented by objects that +// can marshal themselves into a valid RQL psuedo-type. +type Marshaler interface { + MarshalRQL() (interface{}, error) } -func RegisterEncodeHook(hook EncodeHook) { - encodeHooks = append(encodeHooks, hook) +// Unmarshaler is the interface implemented by objects +// that can unmarshal a psuedo-type object of themselves. +type Unmarshaler interface { + UnmarshalRQL(interface{}) error } diff --git a/encoding/errors.go b/encoding/errors.go index ca1f6ec8..7fd78f4f 100644 --- a/encoding/errors.go +++ b/encoding/errors.go @@ -1,10 +1,38 @@ package encoding import ( + "fmt" "reflect" "strconv" + "strings" ) +// An InvalidEncodeError describes an invalid argument passed to Encode. +// (The argument to Encode must be a non-nil pointer.) +type InvalidEncodeError struct { + Type reflect.Type +} + +func (e *InvalidEncodeError) Error() string { + if e.Type == nil { + return "gorethink: Encode(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "gorethink: Encode(non-pointer " + e.Type.String() + ")" + } + return "gorethink: Encode(nil " + e.Type.String() + ")" +} + +type MarshalerError struct { + Type reflect.Type + Err error +} + +func (e *MarshalerError) Error() string { + return "gorethink: error calling MarshalRQL for type " + e.Type.String() + ": " + e.Err.Error() +} + // An UnsupportedTypeError is returned by Marshal when attempting // to encode an unsupported value type. type UnsupportedTypeError struct { @@ -64,3 +92,29 @@ func (e *InvalidDecodeError) Error() string { } return "gorethink: Decode(nil " + e.Type.String() + ")" } + +// Error implements the error interface and can represents multiple +// errors that occur in the course of a single decode. +type Error struct { + Errors []string +} + +func (e *Error) Error() string { + points := make([]string, len(e.Errors)) + for i, err := range e.Errors { + points[i] = fmt.Sprintf("* %s", err) + } + + return fmt.Sprintf( + "%d error(s) decoding:\n\n%s", + len(e.Errors), strings.Join(points, "\n")) +} + +func appendErrors(errors []string, err error) []string { + switch e := err.(type) { + case *Error: + return append(errors, e.Errors...) + default: + return append(errors, e.Error()) + } +} diff --git a/encoding/utils.go b/encoding/utils.go new file mode 100644 index 00000000..43c736f8 --- /dev/null +++ b/encoding/utils.go @@ -0,0 +1,59 @@ +package encoding + +import "reflect" + +func getKind(val reflect.Value) reflect.Kind { + kind := val.Kind() + + switch { + case kind >= reflect.Int && kind <= reflect.Int64: + return reflect.Int + case kind >= reflect.Uint && kind <= reflect.Uint64: + return reflect.Uint + case kind >= reflect.Float32 && kind <= reflect.Float64: + return reflect.Float32 + default: + return kind + } +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +func fieldByIndex(v reflect.Value, index []int) reflect.Value { + for _, i := range index { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + } + v = v.Field(i) + } + return v +} + +func typeByIndex(t reflect.Type, index []int) reflect.Type { + for _, i := range index { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + t = t.Field(i).Type + } + return t +} diff --git a/query.go b/query.go index d774973c..e743a7bc 100644 --- a/query.go +++ b/query.go @@ -121,9 +121,9 @@ type RunOpts struct { TimeFormat interface{} `gorethink:"time_format,omitempty"` GroupFormat interface{} `gorethink:"group_format,omitempty"` BinaryFormat interface{} `gorethink:"binary_format,omitempty"` + EncodeExpr interface{} `gorethink:"encode_expr,omitempty"` // Unsupported options - BatchConf interface{} `gorethink:"batch_conf,omitempty"` } diff --git a/query_control.go b/query_control.go index 492b257b..8ccf9f2a 100644 --- a/query_control.go +++ b/query_control.go @@ -1,13 +1,9 @@ package gorethink import ( - "bytes" "encoding/base64" - "fmt" - "io" - "io/ioutil" + "reflect" - "time" p "github.com/dancannon/gorethink/ql2" ) @@ -21,35 +17,25 @@ var byteSliceType = reflect.TypeOf([]byte(nil)) // // If you want to call expression methods on an object that is not yet an // expression, this is the function you want. -func Expr(value interface{}) Term { - return expr(value, 20) +func Expr(val interface{}) Term { + return expr(val, 20) } -func expr(value interface{}, depth int) Term { +func expr(val interface{}, depth int) Term { if depth <= 0 { panic("Maximum nesting depth limit exceeded") } - if value == nil { + if val == nil { return Term{ termType: p.Term_DATUM, data: nil, } } - switch val := value.(type) { + switch val := val.(type) { case Term: return val - case time.Time: - return EpochTime(val.Unix()) - case io.Reader: - b, err := ioutil.ReadAll(val) - if err != nil { - panic(fmt.Sprintf("Error reading bytes from reader: %s", err.Error())) - } - return Binary(b) - case bytes.Buffer: - return Binary(val) case []byte: return Binary(val) case []interface{}: @@ -68,27 +54,13 @@ func expr(value interface{}, depth int) Term { return makeObject(vals) default: // Use reflection to check for other types - typ := reflect.TypeOf(val) - rval := reflect.ValueOf(val) - - if typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Interface { - v := reflect.ValueOf(val) + valType := reflect.TypeOf(val) + valValue := reflect.ValueOf(val) - if v.IsNil() { - return Term{ - termType: p.Term_DATUM, - data: nil, - } - } - - val = v.Elem().Interface() - typ = reflect.TypeOf(val) - } - - if typ.Kind() == reflect.Func { + if valType.Kind() == reflect.Func { return makeFunc(val) } - if typ.Kind() == reflect.Struct { + if valType.Kind() == reflect.Struct { data, err := encode(val) if err != nil || data == nil { @@ -100,23 +72,23 @@ func expr(value interface{}, depth int) Term { return expr(data, depth-1) } - if typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array { + if valType.Kind() == reflect.Slice || valType.Kind() == reflect.Array { // Check if slice is a byte slice - if typ.Elem().Kind() == reflect.Uint8 { - return Binary(rval.Bytes()) + if valType.Elem().Kind() == reflect.Uint8 { + return Binary(valValue.Bytes()) } else { vals := []Term{} - for i := 0; i < rval.Len(); i++ { - vals = append(vals, expr(rval.Index(i).Interface(), depth)) + for i := 0; i < valValue.Len(); i++ { + vals = append(vals, expr(valValue.Index(i).Interface(), depth)) } return makeArray(vals) } } - if typ.Kind() == reflect.Map { + if valType.Kind() == reflect.Map { vals := map[string]Term{} - for _, k := range rval.MapKeys() { - vals[k.String()] = expr(rval.MapIndex(k).Interface(), depth) + for _, k := range valValue.MapKeys() { + vals[k.String()] = expr(valValue.MapIndex(k).Interface(), depth) } return makeObject(vals) @@ -193,15 +165,6 @@ func Binary(data interface{}) Term { switch data := data.(type) { case Term: return constructRootTerm("Binary", p.Term_BINARY, []interface{}{data}, map[string]interface{}{}) - case io.Reader: - var err error - b, err = ioutil.ReadAll(data) - if err != nil { - panic(fmt.Sprintf("Error reading bytes from reader: %s", err.Error())) - } - return Binary(b) - case bytes.Buffer: - b = data.Bytes() case []byte: b = data default: diff --git a/query_write.go b/query_write.go index b6ee29ac..18168a84 100644 --- a/query_write.go +++ b/query_write.go @@ -1,8 +1,6 @@ package gorethink -import ( - p "github.com/dancannon/gorethink/ql2" -) +import p "github.com/dancannon/gorethink/ql2" type InsertOpts struct { Durability interface{} `gorethink:"durability,omitempty"` @@ -28,7 +26,7 @@ func (t Term) Insert(arg interface{}, optArgs ...InsertOpts) Term { if len(optArgs) >= 1 { opts = optArgs[0].toMap() } - return constructMethodTerm(t, "Insert", p.Term_INSERT, []interface{}{funcWrap(arg)}, opts) + return constructMethodTerm(t, "Insert", p.Term_INSERT, []interface{}{Expr(arg)}, opts) } type UpdateOpts struct { diff --git a/types/time.go b/types/time.go new file mode 100644 index 00000000..65a283bd --- /dev/null +++ b/types/time.go @@ -0,0 +1,55 @@ +package types + +import ( + "fmt" + "math" + "strconv" + + "time" +) + +type Time struct { + time.Time +} + +func (t Time) MarshalRQL() (interface{}, error) { + return map[string]interface{}{ + "$REQL_TYPE": "TIME", + "timestamp": t.Unix(), + "timezone": "+00:00", + }, nil +} + +func (t *Time) UnmarshalRQL(data interface{}) error { + obj, ok := data.(map[string]interface{}) + if !ok { + return fmt.Errorf("Could not unmarshal time, expected a map but received %t", data) + } + + timestamp := obj["epoch_time"].(float64) + timezone := obj["timezone"].(string) + + sec, ms := math.Modf(timestamp) + + t.Time = time.Unix(int64(sec), int64(ms*1000*1000*1000)) + + // Caclulate the timezone + if timezone != "" { + hours, err := strconv.Atoi(timezone[1:3]) + if err != nil { + return err + } + minutes, err := strconv.Atoi(timezone[4:6]) + if err != nil { + return err + } + tzOffset := ((hours * 60) + minutes) * 60 + if timezone[:1] == "-" { + tzOffset = 0 - tzOffset + } + + t.Time = t.In(time.FixedZone(timezone, tzOffset)) + } + + return nil +} diff --git a/utils.go b/utils.go index ac1d58ae..99e938a4 100644 --- a/utils.go +++ b/utils.go @@ -6,8 +6,9 @@ import ( "sync/atomic" "time" - "code.google.com/p/goprotobuf/proto" "github.com/dancannon/gorethink/encoding" + + "code.google.com/p/goprotobuf/proto" p "github.com/dancannon/gorethink/ql2" ) @@ -234,20 +235,15 @@ func protobufToString(p proto.Message, indentLevel int) string { var timeType = reflect.TypeOf(time.Time{}) var termType = reflect.TypeOf(Term{}) -func encode(v interface{}) (interface{}, error) { - if _, ok := v.(Term); ok { - return v, nil +func encode(data interface{}) (interface{}, error) { + if _, ok := data.(Term); ok { + return data, nil } - encoding.RegisterEncodeHook(func(v reflect.Value) (success bool, ret reflect.Value, err error) { - if v.Kind() == reflect.Struct { - if v.Type().ConvertibleTo(timeType) || v.Type().ConvertibleTo(termType) { - return true, v, nil - } - } - - return false, v, nil - }) + v, err := encoding.Encode(data) + if err != nil { + return nil, err + } - return encoding.Encode(v) + return v, nil } From 3cf4c5212fbc98661640653c0e07a050ed994f3f Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 27 Sep 2014 20:59:28 +0100 Subject: [PATCH 05/18] Updated the query build function --- query.go | 6 ++++++ session.go | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/query.go b/query.go index e743a7bc..229195bf 100644 --- a/query.go +++ b/query.go @@ -28,6 +28,12 @@ func (t Term) build() interface{} { switch t.termType { case p.Term_DATUM: return t.data + case p.Term_MAKE_OBJ: + res := map[string]interface{}{} + for k, v := range t.optArgs { + res[k] = v.build() + } + return res case p.Term_BINARY: if len(t.args) == 0 { return map[string]interface{}{ diff --git a/session.go b/session.go index b3c99567..a8e81412 100644 --- a/session.go +++ b/session.go @@ -21,10 +21,10 @@ func (q *Query) build() []interface{} { res := []interface{}{q.Type} if q.Term != nil { res = append(res, q.Term.build()) + } - if len(q.GlobalOpts) > 0 { - res = append(res, q.GlobalOpts) - } + if len(q.GlobalOpts) > 0 { + res = append(res, q.GlobalOpts) } return res From 14acf0679f84ce1c04e6739335897032ef2a7390 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 27 Sep 2014 23:24:17 +0100 Subject: [PATCH 06/18] Fixed pseudo-types --- encoding/encoder_types.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/encoding/encoder_types.go b/encoding/encoder_types.go index 32e1f53e..6994b8b3 100644 --- a/encoding/encoder_types.go +++ b/encoding/encoder_types.go @@ -308,9 +308,9 @@ func timePseudoTypeEncoder(v reflect.Value) interface{} { t := v.Interface().(time.Time) return map[string]interface{}{ - "$REQL_TYPE": "TIME", - "timestamp": t.Unix(), - "timezone": "+00:00", + "$reql_type$": "TIME", + "epoch_time": t.Unix(), + "timezone": "+00:00", } } @@ -325,7 +325,7 @@ func encodeByteSlice(v reflect.Value) interface{} { base64.StdEncoding.Encode(dst, b) return map[string]interface{}{ - "$REQL_TYPE": "BINARY", - "data": dst, + "$reql_type$": "BINARY", + "data": dst, } } From 375232287903cf561b68dfc1529b9de0d5635a62 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sun, 28 Sep 2014 10:23:30 +0100 Subject: [PATCH 07/18] Improved memory allocation for utils function --- utils.go | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/utils.go b/utils.go index 99e938a4..a7fc95ad 100644 --- a/utils.go +++ b/utils.go @@ -54,7 +54,7 @@ func makeArray(args termsList) Term { // makeObject takes a map of terms and produces a single MAKE_OBJECT term func makeObject(args termsObj) Term { // First all evaluate all fields in the map - temp := termsObj{} + temp := make(termsObj) for k, v := range args { temp[k] = Expr(v) } @@ -72,12 +72,12 @@ func makeFunc(f interface{}) Term { value := reflect.ValueOf(f) valueType := value.Type() - var argNums []interface{} - var args []reflect.Value + var argNums = make([]interface{}, valueType.NumIn()) + var args = make([]reflect.Value, valueType.NumIn()) for i := 0; i < valueType.NumIn(); i++ { // Get a slice of the VARs to use as the function arguments - args = append(args, reflect.ValueOf(constructRootTerm("var", p.Term_VAR, []interface{}{nextVarId}, map[string]interface{}{}))) - argNums = append(argNums, nextVarId) + args[i] = reflect.ValueOf(constructRootTerm("var", p.Term_VAR, []interface{}{nextVarId}, map[string]interface{}{})) + argNums[i] = nextVarId atomic.AddInt64(&nextVarId, 1) // make sure all input arguments are of type Term @@ -153,9 +153,9 @@ func optArgsToMap(optArgs OptArgs) map[string]interface{} { // Convert a list into a slice of terms func convertTermList(l []interface{}) termsList { - terms := termsList{} - for _, v := range l { - terms = append(terms, Expr(v)) + terms := make(termsList, len(l)) + for i, v := range l { + terms[i] = Expr(v) } return terms @@ -189,33 +189,38 @@ func mergeArgs(args ...interface{}) []interface{} { // Helper functions for debugging func allArgsToStringSlice(args termsList, optArgs termsObj) []string { - allArgs := []string{} + allArgs := make([]string, len(args)+len(optArgs)) + i := 0 for _, v := range args { - allArgs = append(allArgs, v.String()) + allArgs[i] = v.String() + i++ } for k, v := range optArgs { - allArgs = append(allArgs, k+"="+v.String()) + allArgs[i] = k + "=" + v.String() + i++ } return allArgs } func argsToStringSlice(args termsList) []string { - allArgs := []string{} + allArgs := make([]string, len(args)) - for _, v := range args { - allArgs = append(allArgs, v.String()) + for i, v := range args { + allArgs[i] = v.String() } return allArgs } func optArgsToStringSlice(optArgs termsObj) []string { - allArgs := []string{} + allArgs := make([]string, len(optArgs)) + i := 0 for k, v := range optArgs { - allArgs = append(allArgs, k+"="+v.String()) + allArgs[i] = k + "=" + v.String() + i++ } return allArgs From 8835d49af7d845f203cb599e3d8f2cda486bfd52 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 12:32:15 +0100 Subject: [PATCH 08/18] Tidied up Expr function and fixed tests --- encoding/encoder.go | 13 ++++++--- encoding/encoder_types.go | 4 +++ encoding/encoding.go | 6 +++++ gorethink.go | 12 +++++++++ query_control.go | 40 ++++++++-------------------- query_control_test.go | 25 ------------------ query_transformation.go | 4 +-- types/time.go | 55 --------------------------------------- 8 files changed, 43 insertions(+), 116 deletions(-) create mode 100644 gorethink.go delete mode 100644 types/time.go diff --git a/encoding/encoder.go b/encoding/encoder.go index b6de2c29..2e3b4ad7 100644 --- a/encoding/encoder.go +++ b/encoding/encoder.go @@ -59,12 +59,10 @@ func typeEncoder(t reflect.Type) encoderFunc { // To deal with recursive types, populate the map with an // indirect func before we build it. This type waits on the - // real func (f) to be ready and then calls it. This indirect + // real func (f) to + // be ready and then calls it. This indirect // func is only used for recursive types. encoderCache.Lock() - if encoderCache.m == nil { - encoderCache.m = make(map[reflect.Type]encoderFunc) - } var wg sync.WaitGroup wg.Add(1) encoderCache.m[t] = func(v reflect.Value) interface{} { @@ -82,3 +80,10 @@ func typeEncoder(t reflect.Type) encoderFunc { encoderCache.Unlock() return f } + +// IgnoreType causes the encoder to ignore a type when encoding +func IgnoreType(t reflect.Type) { + encoderCache.RLock() + encoderCache.m[t] = doNothingEncoder + encoderCache.RUnlock() +} diff --git a/encoding/encoder_types.go b/encoding/encoder_types.go index 6994b8b3..c93464c7 100644 --- a/encoding/encoder_types.go +++ b/encoding/encoder_types.go @@ -67,6 +67,10 @@ func invalidValueEncoder(v reflect.Value) interface{} { return nil } +func doNothingEncoder(v reflect.Value) interface{} { + return v.Interface() +} + func marshalerEncoder(v reflect.Value) interface{} { if v.Kind() == reflect.Ptr && v.IsNil() { return nil diff --git a/encoding/encoding.go b/encoding/encoding.go index 57d0755e..c61bf690 100644 --- a/encoding/encoding.go +++ b/encoding/encoding.go @@ -1,5 +1,7 @@ package encoding +import "reflect" + // Marshaler is the interface implemented by objects that // can marshal themselves into a valid RQL psuedo-type. type Marshaler interface { @@ -11,3 +13,7 @@ type Marshaler interface { type Unmarshaler interface { UnmarshalRQL(interface{}) error } + +func init() { + encoderCache.m = make(map[reflect.Type]encoderFunc) +} diff --git a/gorethink.go b/gorethink.go new file mode 100644 index 00000000..c8e66ec6 --- /dev/null +++ b/gorethink.go @@ -0,0 +1,12 @@ +package gorethink + +import ( + "reflect" + + "github.com/dancannon/gorethink/encoding" +) + +func init() { + // Set encoding package + encoding.IgnoreType(reflect.TypeOf(Term{})) +} diff --git a/query_control.go b/query_control.go index 8ccf9f2a..568bbc54 100644 --- a/query_control.go +++ b/query_control.go @@ -36,31 +36,15 @@ func expr(val interface{}, depth int) Term { switch val := val.(type) { case Term: return val - case []byte: - return Binary(val) - case []interface{}: - vals := []Term{} - for _, v := range val { - vals = append(vals, expr(v, depth)) - } - - return makeArray(vals) - case map[string]interface{}: - vals := map[string]Term{} - for k, v := range val { - vals[k] = expr(v, depth) - } - - return makeObject(vals) default: // Use reflection to check for other types valType := reflect.TypeOf(val) valValue := reflect.ValueOf(val) - if valType.Kind() == reflect.Func { + switch valType.Kind() { + case reflect.Func: return makeFunc(val) - } - if valType.Kind() == reflect.Struct { + case reflect.Struct, reflect.Ptr: data, err := encode(val) if err != nil || data == nil { @@ -71,8 +55,8 @@ func expr(val interface{}, depth int) Term { } return expr(data, depth-1) - } - if valType.Kind() == reflect.Slice || valType.Kind() == reflect.Array { + + case reflect.Slice, reflect.Array: // Check if slice is a byte slice if valType.Elem().Kind() == reflect.Uint8 { return Binary(valValue.Bytes()) @@ -84,20 +68,18 @@ func expr(val interface{}, depth int) Term { return makeArray(vals) } - } - if valType.Kind() == reflect.Map { + case reflect.Map: vals := map[string]Term{} for _, k := range valValue.MapKeys() { vals[k.String()] = expr(valValue.MapIndex(k).Interface(), depth) } return makeObject(vals) - } - - // If no other match was found then return a datum value - return Term{ - termType: p.Term_DATUM, - data: val, + default: + return Term{ + termType: p.Term_DATUM, + data: val, + } } } } diff --git a/query_control_test.go b/query_control_test.go index ab3ed1d3..9822a6f7 100644 --- a/query_control_test.go +++ b/query_control_test.go @@ -2,7 +2,6 @@ package gorethink import ( "bytes" - "strings" "testing" "time" @@ -243,18 +242,6 @@ func (s *RethinkSuite) TestControlBinaryByteArrayAlias(c *test.C) { c.Assert(bytes.Equal(response, []byte("Hello World")), test.Equals, true) } -func (s *RethinkSuite) TestControlBinaryReader(c *test.C) { - var response []byte - - query := Binary(strings.NewReader("Hello World")) - res, err := query.Run(sess) - c.Assert(err, test.IsNil) - - err = res.One(&response) - c.Assert(err, test.IsNil) - c.Assert(bytes.Equal(response, []byte("Hello World")), test.Equals, true) -} - func (s *RethinkSuite) TestControlBinaryExpr(c *test.C) { var response []byte @@ -267,18 +254,6 @@ func (s *RethinkSuite) TestControlBinaryExpr(c *test.C) { c.Assert(bytes.Equal(response, []byte("Hello World")), test.Equals, true) } -func (s *RethinkSuite) TestControlBinaryExprReader(c *test.C) { - var response []byte - - query := Expr(strings.NewReader("Hello World")) - res, err := query.Run(sess) - c.Assert(err, test.IsNil) - - err = res.One(&response) - c.Assert(err, test.IsNil) - c.Assert(bytes.Equal(response, []byte("Hello World")), test.Equals, true) -} - func (s *RethinkSuite) TestControlBinaryExprAlias(c *test.C) { var response []byte diff --git a/query_transformation.go b/query_transformation.go index c3e17fcf..3c49a0ce 100644 --- a/query_transformation.go +++ b/query_transformation.go @@ -1,8 +1,6 @@ package gorethink -import ( - p "github.com/dancannon/gorethink/ql2" -) +import p "github.com/dancannon/gorethink/ql2" // Transform each element of the sequence by applying the given mapping function. func (t Term) Map(args ...interface{}) Term { diff --git a/types/time.go b/types/time.go deleted file mode 100644 index 65a283bd..00000000 --- a/types/time.go +++ /dev/null @@ -1,55 +0,0 @@ -package types - -import ( - "fmt" - "math" - "strconv" - - "time" -) - -type Time struct { - time.Time -} - -func (t Time) MarshalRQL() (interface{}, error) { - return map[string]interface{}{ - "$REQL_TYPE": "TIME", - "timestamp": t.Unix(), - "timezone": "+00:00", - }, nil -} - -func (t *Time) UnmarshalRQL(data interface{}) error { - obj, ok := data.(map[string]interface{}) - if !ok { - return fmt.Errorf("Could not unmarshal time, expected a map but received %t", data) - } - - timestamp := obj["epoch_time"].(float64) - timezone := obj["timezone"].(string) - - sec, ms := math.Modf(timestamp) - - t.Time = time.Unix(int64(sec), int64(ms*1000*1000*1000)) - - // Caclulate the timezone - if timezone != "" { - hours, err := strconv.Atoi(timezone[1:3]) - if err != nil { - return err - } - minutes, err := strconv.Atoi(timezone[4:6]) - if err != nil { - return err - } - tzOffset := ((hours * 60) + minutes) * 60 - if timezone[:1] == "-" { - tzOffset = 0 - tzOffset - } - - t.Time = t.In(time.FixedZone(timezone, tzOffset)) - } - - return nil -} From c6e5505142f2029d89822b87d0df5d05a10f46fb Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 15:00:49 +0100 Subject: [PATCH 09/18] Added support for decoding GEOMETRY pseudo-type --- pseudotypes.go | 125 +++++++++++++++++++++++++++++++++++++++ query_geospatial.go | 1 + query_geospatial_test.go | 36 +++++++++++ 3 files changed, 162 insertions(+) create mode 100644 query_geospatial.go create mode 100644 query_geospatial_test.go diff --git a/pseudotypes.go b/pseudotypes.go index 5a6c7fa0..5004ebb2 100644 --- a/pseudotypes.go +++ b/pseudotypes.go @@ -9,6 +9,21 @@ import ( "fmt" ) +// Pseudo-Type structs +type Geometry struct { + Type string + Point Point + Line Line + Lines Lines +} + +type Point LatLon +type Line []Point +type Lines []Line +type LatLon struct { + Lat, Lon float64 +} + func convertPseudotype(obj map[string]interface{}, opts map[string]interface{}) (interface{}, error) { if reqlType, ok := obj["$reql_type$"]; ok { if reqlType == "TIME" { @@ -64,6 +79,23 @@ func convertPseudotype(obj map[string]interface{}, opts map[string]interface{}) } else { return nil, fmt.Errorf("Unknown binary_format run option \"%s\".", reqlType) } + } else if reqlType == "GEOMETRY" { + geometryFormat := "native" + if opt, ok := opts["geometry_format"]; ok { + if sopt, ok := opt.(string); ok { + geometryFormat = sopt + } else { + return nil, fmt.Errorf("Invalid geometry_format run option \"%s\".", opt) + } + } + + if geometryFormat == "native" { + return reqlGeometryToNativeGeometry(obj) + } else if geometryFormat == "raw" { + return obj, nil + } else { + return nil, fmt.Errorf("Unknown geometry_format run option \"%s\".", reqlType) + } } else { return obj, nil } @@ -162,3 +194,96 @@ func reqlBinaryToNativeBytes(obj map[string]interface{}) (interface{}, error) { return nil, fmt.Errorf("pseudo-type BINARY object %v does not have the expected field \"data\"", obj) } } + +func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, error) { + if typ, ok := obj["type"]; !ok { + return nil, fmt.Errorf("pseudo-type GEOMETRY object %v does not have the expected field \"type\"", obj) + } else if typ, ok := typ.(string); !ok { + return nil, fmt.Errorf("pseudo-type GEOMETRY object %v field \"type\" is not valid", obj) + } else if coords, ok := obj["coordinates"]; !ok { + return nil, fmt.Errorf("pseudo-type GEOMETRY object %v does not have the expected field \"coordinates\"", obj) + } else if typ == "Point" { + if point, err := unmarshalPoint(coords); err != nil { + return nil, err + } else { + return Geometry{ + Type: "Point", + Point: point, + }, nil + } + } else if typ == "LineString" { + if line, err := unmarshalLineString(coords); err != nil { + return nil, err + } else { + return Geometry{ + Type: "LineString", + Line: line, + }, nil + } + } else if typ == "Polygon" { + if lines, err := unmarshalPolygon(coords); err != nil { + return nil, err + } else { + return Geometry{ + Type: "Polygon", + Lines: lines, + }, nil + } + } else { + return nil, fmt.Errorf("pseudo-type GEOMETRY object %v field has unknown type %s", typ) + } +} + +func unmarshalPoint(v interface{}) (Point, error) { + coords, ok := v.([]interface{}) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + if len(coords) != 2 { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + lat, ok := coords[0].(float64) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + lon, ok := coords[1].(float64) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + return Point{lat, lon}, nil +} + +func unmarshalLineString(v interface{}) (Line, error) { + points, ok := v.([]interface{}) + if !ok { + return Line{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + var err error + line := make(Line, len(points)) + for i, coords := range points { + line[i], err = unmarshalPoint(coords) + if err != nil { + return Line{}, err + } + } + return line, nil +} + +func unmarshalPolygon(v interface{}) (Lines, error) { + lines, ok := v.([]interface{}) + if !ok { + return Lines{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + var err error + polygon := make(Lines, len(lines)) + for i, line := range lines { + polygon[i], err = unmarshalLineString(line) + if err != nil { + return Lines{}, err + } + } + return polygon, nil +} diff --git a/query_geospatial.go b/query_geospatial.go new file mode 100644 index 00000000..561e0c35 --- /dev/null +++ b/query_geospatial.go @@ -0,0 +1 @@ +package gorethink diff --git a/query_geospatial_test.go b/query_geospatial_test.go new file mode 100644 index 00000000..9c0d29bd --- /dev/null +++ b/query_geospatial_test.go @@ -0,0 +1,36 @@ +package gorethink + +import test "gopkg.in/check.v1" + +func (s *RethinkSuite) TestGeospatialGeometryPseudoType(c *test.C) { + var response Geometry + res, err := Expr(map[string]interface{}{ + "$reql_type$": "GEOMETRY", + "type": "Polygon", + "coordinates": []interface{}{ + []interface{}{ + []interface{}{-122.423246, 37.779388}, + []interface{}{-122.423246, 37.329898}, + []interface{}{-121.88642, 37.329898}, + []interface{}{-121.88642, 37.779388}, + []interface{}{-122.423246, 37.779388}, + }, + }, + }).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, Geometry{ + Type: "Polygon", + Lines: Lines{ + Line{ + Point{-122.423246, 37.779388}, + Point{-122.423246, 37.329898}, + Point{-121.88642, 37.329898}, + Point{-121.88642, 37.779388}, + Point{-122.423246, 37.779388}, + }, + }, + }) +} From be7ad56003fac8f51c2be78fbf9fe531cf872a28 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 15:05:02 +0100 Subject: [PATCH 10/18] Updated proto file --- ql2/ql2.pb.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- ql2/ql2.proto | 19 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/ql2/ql2.pb.go b/ql2/ql2.pb.go index af885e91..2f72bec2 100644 --- a/ql2/ql2.pb.go +++ b/ql2/ql2.pb.go @@ -302,6 +302,8 @@ const ( Term_VAR Term_TermType = 10 // Takes some javascript code and executes it. Term_JAVASCRIPT Term_TermType = 11 + // STRING {timeout: !NUMBER} -> Function(*) + Term_UUID Term_TermType = 169 // Takes an HTTP URL and gets it. If the get succeeds and // returns valid JSON, it is converted into a DATUM Term_HTTP Term_TermType = 153 @@ -409,7 +411,9 @@ const ( // Take the union of multiple sequences (preserves duplicate elements! (use distinct)). Term_UNION Term_TermType = 44 // Get the Nth element of a sequence. - Term_NTH Term_TermType = 45 + Term_NTH Term_TermType = 45 + // do NTH or GET_FIELD depending on target object + Term_BRACKET Term_TermType = 170 Term_INNER_JOIN Term_TermType = 48 Term_OUTER_JOIN Term_TermType = 49 // An inner-join that does an equality comparison on two attributes. @@ -642,7 +646,20 @@ const ( Term_CHANGES Term_TermType = 152 Term_ARGS Term_TermType = 154 // BINARY is client-only at the moment, it is not supported on the server - Term_BINARY Term_TermType = 155 + Term_BINARY Term_TermType = 155 + Term_GEOJSON Term_TermType = 157 + Term_TO_GEOJSON Term_TermType = 158 + Term_POINT Term_TermType = 159 + Term_LINE Term_TermType = 160 + Term_POLYGON Term_TermType = 161 + Term_DISTANCE Term_TermType = 162 + Term_INTERSECTS Term_TermType = 163 + Term_INCLUDES Term_TermType = 164 + Term_CIRCLE Term_TermType = 165 + Term_GET_INTERSECTING Term_TermType = 166 + Term_FILL Term_TermType = 167 + Term_GET_NEAREST Term_TermType = 168 + Term_POLYGON_SUB Term_TermType = 171 ) var Term_TermType_name = map[int32]string{ @@ -651,6 +668,7 @@ var Term_TermType_name = map[int32]string{ 3: "MAKE_OBJ", 10: "VAR", 11: "JAVASCRIPT", + 169: "UUID", 153: "HTTP", 12: "ERROR", 13: "IMPLICIT_VAR", @@ -701,6 +719,7 @@ var Term_TermType_name = map[int32]string{ 86: "IS_EMPTY", 44: "UNION", 45: "NTH", + 170: "BRACKET", 48: "INNER_JOIN", 49: "OUTER_JOIN", 50: "EQ_JOIN", @@ -793,6 +812,19 @@ var Term_TermType_name = map[int32]string{ 152: "CHANGES", 154: "ARGS", 155: "BINARY", + 157: "GEOJSON", + 158: "TO_GEOJSON", + 159: "POINT", + 160: "LINE", + 161: "POLYGON", + 162: "DISTANCE", + 163: "INTERSECTS", + 164: "INCLUDES", + 165: "CIRCLE", + 166: "GET_INTERSECTING", + 167: "FILL", + 168: "GET_NEAREST", + 171: "POLYGON_SUB", } var Term_TermType_value = map[string]int32{ "DATUM": 1, @@ -800,6 +832,7 @@ var Term_TermType_value = map[string]int32{ "MAKE_OBJ": 3, "VAR": 10, "JAVASCRIPT": 11, + "UUID": 169, "HTTP": 153, "ERROR": 12, "IMPLICIT_VAR": 13, @@ -850,6 +883,7 @@ var Term_TermType_value = map[string]int32{ "IS_EMPTY": 86, "UNION": 44, "NTH": 45, + "BRACKET": 170, "INNER_JOIN": 48, "OUTER_JOIN": 49, "EQ_JOIN": 50, @@ -942,6 +976,19 @@ var Term_TermType_value = map[string]int32{ "CHANGES": 152, "ARGS": 154, "BINARY": 155, + "GEOJSON": 157, + "TO_GEOJSON": 158, + "POINT": 159, + "LINE": 160, + "POLYGON": 161, + "DISTANCE": 162, + "INTERSECTS": 163, + "INCLUDES": 164, + "CIRCLE": 165, + "GET_INTERSECTING": 166, + "FILL": 167, + "GET_NEAREST": 168, + "POLYGON_SUB": 171, } func (x Term_TermType) Enum() *Term_TermType { diff --git a/ql2/ql2.proto b/ql2/ql2.proto index 6ba41bc9..94eff39a 100644 --- a/ql2/ql2.proto +++ b/ql2/ql2.proto @@ -258,6 +258,7 @@ message Term { // Takes some javascript code and executes it. JAVASCRIPT = 11; // STRING {timeout: !NUMBER} -> DATUM | // STRING {timeout: !NUMBER} -> Function(*) + UUID = 169; // () -> DATUM // Takes an HTTP URL and gets it. If the get succeeds and // returns valid JSON, it is converted into a DATUM @@ -388,6 +389,10 @@ message Term { UNION = 44; // Sequence... -> Sequence // Get the Nth element of a sequence. NTH = 45; // Sequence, NUMBER -> DATUM + // do NTH or GET_FIELD depending on target object + BRACKET = 170; // Sequence | OBJECT, NUMBER | STRING -> DATUM + // OBSOLETE_GROUPED_MAPREDUCE = 46; + // OBSOLETE_GROUPBY = 47; INNER_JOIN = 48; // Sequence, Sequence, Function(2) -> Sequence OUTER_JOIN = 49; // Sequence, Sequence, Function(2) -> Sequence @@ -657,6 +662,20 @@ message Term { // BINARY is client-only at the moment, it is not supported on the server BINARY = 155; // STRING -> PSEUDOTYPE(BINARY) + + GEOJSON = 157; // OBJECT -> PSEUDOTYPE(GEOMETRY) + TO_GEOJSON = 158; // PSEUDOTYPE(GEOMETRY) -> OBJECT + POINT = 159; // NUMBER, NUMBER -> PSEUDOTYPE(GEOMETRY) + LINE = 160; // (ARRAY | PSEUDOTYPE(GEOMETRY))... -> PSEUDOTYPE(GEOMETRY) + POLYGON = 161; // (ARRAY | PSEUDOTYPE(GEOMETRY))... -> PSEUDOTYPE(GEOMETRY) + DISTANCE = 162; // PSEUDOTYPE(GEOMETRY), PSEUDOTYPE(GEOMETRY) {geo_system:STRING, unit:STRING} -> NUMBER + INTERSECTS = 163; // PSEUDOTYPE(GEOMETRY), PSEUDOTYPE(GEOMETRY) -> BOOL + INCLUDES = 164; // PSEUDOTYPE(GEOMETRY), PSEUDOTYPE(GEOMETRY) -> BOOL + CIRCLE = 165; // PSEUDOTYPE(GEOMETRY), NUMBER {num_vertices:NUMBER, geo_system:STRING, unit:STRING, fill:BOOL} -> PSEUDOTYPE(GEOMETRY) + GET_INTERSECTING = 166; // TABLE, PSEUDOTYPE(GEOMETRY) {index:!STRING} -> StreamSelection + FILL = 167; // PSEUDOTYPE(GEOMETRY) -> PSEUDOTYPE(GEOMETRY) + GET_NEAREST = 168; // TABLE, PSEUDOTYPE(GEOMETRY) {index:!STRING, max_results:NUM, max_dist:NUM, geo_system:STRING, unit:STRING} -> ARRAY + POLYGON_SUB = 171; // PSEUDOTYPE(GEOMETRY), PSEUDOTYPE(GEOMETRY) -> PSEUDOTYPE(GEOMETRY) } optional TermType type = 1; From 6a96771f6e27ed255678be1b7d5776ae00a9ac29 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 18:56:21 +0100 Subject: [PATCH 11/18] Added geospatial terms --- pseudotypes.go | 51 ++-- query_geospatial.go | 169 ++++++++++++++ query_geospatial_test.go | 485 ++++++++++++++++++++++++++++++++++++++- query_table.go | 1 + types/geometry.go | 14 ++ 5 files changed, 678 insertions(+), 42 deletions(-) create mode 100644 types/geometry.go diff --git a/pseudotypes.go b/pseudotypes.go index 5004ebb2..b273c51e 100644 --- a/pseudotypes.go +++ b/pseudotypes.go @@ -6,24 +6,11 @@ import ( "strconv" "time" + "github.com/dancannon/gorethink/types" + "fmt" ) -// Pseudo-Type structs -type Geometry struct { - Type string - Point Point - Line Line - Lines Lines -} - -type Point LatLon -type Line []Point -type Lines []Line -type LatLon struct { - Lat, Lon float64 -} - func convertPseudotype(obj map[string]interface{}, opts map[string]interface{}) (interface{}, error) { if reqlType, ok := obj["$reql_type$"]; ok { if reqlType == "TIME" { @@ -206,7 +193,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro if point, err := unmarshalPoint(coords); err != nil { return nil, err } else { - return Geometry{ + return types.Geometry{ Type: "Point", Point: point, }, nil @@ -215,7 +202,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro if line, err := unmarshalLineString(coords); err != nil { return nil, err } else { - return Geometry{ + return types.Geometry{ Type: "LineString", Line: line, }, nil @@ -224,7 +211,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro if lines, err := unmarshalPolygon(coords); err != nil { return nil, err } else { - return Geometry{ + return types.Geometry{ Type: "Polygon", Lines: lines, }, nil @@ -234,55 +221,55 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro } } -func unmarshalPoint(v interface{}) (Point, error) { +func unmarshalPoint(v interface{}) (types.Point, error) { coords, ok := v.([]interface{}) if !ok { - return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } if len(coords) != 2 { - return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } lat, ok := coords[0].(float64) if !ok { - return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } lon, ok := coords[1].(float64) if !ok { - return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } - return Point{lat, lon}, nil + return types.Point{lat, lon}, nil } -func unmarshalLineString(v interface{}) (Line, error) { +func unmarshalLineString(v interface{}) (types.Line, error) { points, ok := v.([]interface{}) if !ok { - return Line{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Line{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } var err error - line := make(Line, len(points)) + line := make(types.Line, len(points)) for i, coords := range points { line[i], err = unmarshalPoint(coords) if err != nil { - return Line{}, err + return types.Line{}, err } } return line, nil } -func unmarshalPolygon(v interface{}) (Lines, error) { +func unmarshalPolygon(v interface{}) (types.Lines, error) { lines, ok := v.([]interface{}) if !ok { - return Lines{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + return types.Lines{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) } var err error - polygon := make(Lines, len(lines)) + polygon := make(types.Lines, len(lines)) for i, line := range lines { polygon[i], err = unmarshalLineString(line) if err != nil { - return Lines{}, err + return types.Lines{}, err } } return polygon, nil diff --git a/query_geospatial.go b/query_geospatial.go index 561e0c35..81883c18 100644 --- a/query_geospatial.go +++ b/query_geospatial.go @@ -1 +1,170 @@ package gorethink + +import ( + p "github.com/dancannon/gorethink/ql2" +) + +// CircleOpts describes the optional arguments for a Circle operation +type CircleOpts struct { + NumVertices interface{} `gorethink:"num_vertices,omitempty"` + GeoSystem interface{} `gorethink:"geo_system,omitempty"` + Unit interface{} `gorethink:"unit,omitempty"` + Fill interface{} `gorethink:"fill,omitempty"` +} + +func (o *CircleOpts) toMap() map[string]interface{} { + return optArgsToMap(o) +} + +// Circle constructs a circular line or polygon. A circle in RethinkDB is +// a polygon or line approximating a circle of a given radius around a given +// center, consisting of a specified number of vertices (default 32). +func Circle(point, radius interface{}, optArgs ...CircleOpts) Term { + opts := map[string]interface{}{} + if len(optArgs) >= 1 { + opts = optArgs[0].toMap() + } + + return constructRootTerm("Circle", p.Term_CIRCLE, []interface{}{point, radius}, opts) +} + +// DistanceOpts describes the optional arguments for a Distance operation +type DistanceOpts struct { + GeoSystem interface{} `gorethink:"geo_system,omitempty"` + Unit interface{} `gorethink:"unit,omitempty"` +} + +func (o *DistanceOpts) toMap() map[string]interface{} { + return optArgsToMap(o) +} + +// Distance calculates the Haversine distance between two points. At least one +// of the geometry objects specified must be a point. +func (t Term) Distance(point interface{}, optArgs ...DistanceOpts) Term { + opts := map[string]interface{}{} + if len(optArgs) >= 1 { + opts = optArgs[0].toMap() + } + + return constructMethodTerm(t, "Distance", p.Term_DISTANCE, []interface{}{point}, opts) +} + +// Distance calculates the Haversine distance between two points. At least one +// of the geometry objects specified must be a point. +func Distance(point1, point2 interface{}, optArgs ...DistanceOpts) Term { + opts := map[string]interface{}{} + if len(optArgs) >= 1 { + opts = optArgs[0].toMap() + } + + return constructRootTerm("Distance", p.Term_DISTANCE, []interface{}{point1, point2}, opts) +} + +// Fill converts a Line object into a Polygon object. If the last point does not +// specify the same coordinates as the first point, polygon will close the +// polygon by connecting them +func (t Term) Fill() Term { + return constructMethodTerm(t, "Fill", p.Term_FILL, []interface{}{}, map[string]interface{}{}) +} + +// Geojson converts a GeoJSON object to a ReQL geometry object. +func Geojson(args ...interface{}) Term { + return constructRootTerm("Geojson", p.Term_GEOJSON, args, map[string]interface{}{}) +} + +// ToGeojson converts a ReQL geometry object to a GeoJSON object. +func (t Term) ToGeojson(args ...interface{}) Term { + return constructMethodTerm(t, "ToGeojson", p.Term_TO_GEOJSON, args, map[string]interface{}{}) +} + +// GetIntersectingOpts describes the optional arguments for a GetIntersecting operation +type GetIntersectingOpts struct { + Index interface{} `gorethink:"index,omitempty"` +} + +func (o *GetIntersectingOpts) toMap() map[string]interface{} { + return optArgsToMap(o) +} + +// GetIntersecting gets all documents where the given geometry object intersects +// the geometry object of the requested geospatial index. +func (t Term) GetIntersecting(args interface{}, optArgs ...GetIntersectingOpts) Term { + opts := map[string]interface{}{} + if len(optArgs) >= 1 { + opts = optArgs[0].toMap() + } + + return constructMethodTerm(t, "GetIntersecting", p.Term_GET_INTERSECTING, []interface{}{args}, opts) +} + +// GetIntersectingOpts describes the optional arguments for a GetIntersecting operation +type GetNearestOpts struct { + Index interface{} `gorethink:"index,omitempty"` + MaxResults interface{} `gorethink:"max_results,omitempty"` + MaxDist interface{} `gorethink:"max_dist,omitempty"` + Unit interface{} `gorethink:"unit,omitempty"` + GeoSystem interface{} `gorethink:"geo_system,omitempty"` +} + +func (o *GetNearestOpts) toMap() map[string]interface{} { + return optArgsToMap(o) +} + +// GetNearest gets all documents where the specified geospatial index is within a +// certain distance of the specified point (default 100 kilometers). +func (t Term) GetNearest(point interface{}, optArgs ...GetNearestOpts) Term { + opts := map[string]interface{}{} + if len(optArgs) >= 1 { + opts = optArgs[0].toMap() + } + + return constructMethodTerm(t, "GetNearest", p.Term_GET_NEAREST, []interface{}{point}, opts) +} + +// Includes tests whether a geometry object is completely contained within another. +// When applied to a sequence of geometry objects, includes acts as a filter, +// returning a sequence of objects from the sequence that include the argument. +func (t Term) Includes(args ...interface{}) Term { + return constructMethodTerm(t, "Includes", p.Term_INCLUDES, args, map[string]interface{}{}) +} + +// Intersects tests whether two geometry objects intersect with one another. +// When applied to a sequence of geometry objects, intersects acts as a filter, +// returning a sequence of objects from the sequence that intersect with the +// argument. +func (t Term) Intersects(args ...interface{}) Term { + return constructMethodTerm(t, "Intersects", p.Term_INTERSECTS, args, map[string]interface{}{}) +} + +// Line constructs a geometry object of type Line. The line can be specified in +// one of two ways: +// - Two or more two-item arrays, specifying longitude and latitude numbers of +// the line's vertices; +// - Two or more Point objects specifying the line's vertices. +func Line(args ...interface{}) Term { + return constructRootTerm("Line", p.Term_LINE, args, map[string]interface{}{}) +} + +// Point constructs a geometry object of type Point. The point is specified by +// two floating point numbers, the longitude (−180 to 180) and latitude +// (−90 to 90) of the point on a perfect sphere. +func Point(lat, lon interface{}) Term { + return constructRootTerm("Point", p.Term_POINT, []interface{}{lat, lon}, map[string]interface{}{}) +} + +// Polygon constructs a geometry object of type Polygon. The Polygon can be +// specified in one of two ways: +// - Three or more two-item arrays, specifying longitude and latitude numbers of the polygon's vertices; +// - Three or more Point objects specifying the polygon's vertices. +func Polygon(args ...interface{}) Term { + return constructRootTerm("Polygon", p.Term_POLYGON, args, map[string]interface{}{}) +} + +// PolygonSub "punches a hole" out of the parent polygon using the polygon passed +// to the function. +// polygon1.PolygonSub(polygon2) -> polygon +// In the example above polygon2 must be completely contained within polygon1 +// and must have no holes itself (it must not be the output of polygon_sub itself). +func (t Term) PolygonSub(args ...interface{}) Term { + return constructMethodTerm(t, "PolygonSub", p.Term_POLYGON_SUB, args, map[string]interface{}{}) +} diff --git a/query_geospatial_test.go b/query_geospatial_test.go index 9c0d29bd..bb4c3a01 100644 --- a/query_geospatial_test.go +++ b/query_geospatial_test.go @@ -1,9 +1,13 @@ package gorethink -import test "gopkg.in/check.v1" +import ( + "github.com/dancannon/gorethink/types" + + test "gopkg.in/check.v1" +) func (s *RethinkSuite) TestGeospatialGeometryPseudoType(c *test.C) { - var response Geometry + var response types.Geometry res, err := Expr(map[string]interface{}{ "$reql_type$": "GEOMETRY", "type": "Polygon", @@ -21,15 +25,476 @@ func (s *RethinkSuite) TestGeospatialGeometryPseudoType(c *test.C) { err = res.One(&response) c.Assert(err, test.IsNil) - c.Assert(response, test.DeepEquals, Geometry{ + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{-122.423246, 37.779388}, + types.Point{-122.423246, 37.329898}, + types.Point{-121.88642, 37.329898}, + types.Point{-121.88642, 37.779388}, + types.Point{-122.423246, 37.779388}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialCircle(c *test.C) { + var response types.Geometry + res, err := Circle([]float64{-122.423246, 37.779388}, 10).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{-122.423246, 37.77929790366427}, + types.Point{-122.42326814543915, 37.77929963483801}, + types.Point{-122.4232894398445, 37.779304761831504}, + types.Point{-122.42330906488651, 37.77931308761787}, + types.Point{-122.42332626638755, 37.77932429224285}, + types.Point{-122.42334038330416, 37.77933794512014}, + types.Point{-122.42335087313059, 37.77935352157849}, + types.Point{-122.42335733274696, 37.77937042302436}, + types.Point{-122.4233595139113, 37.77938799994533}, + types.Point{-122.42335733279968, 37.7794055768704}, + types.Point{-122.42335087322802, 37.779422478327966}, + types.Point{-122.42334038343147, 37.77943805480385}, + types.Point{-122.42332626652532, 37.779451707701796}, + types.Point{-122.42330906501378, 37.77946291234741}, + types.Point{-122.42328943994191, 37.77947123815131}, + types.Point{-122.42326814549187, 37.77947636515649}, + types.Point{-122.423246, 37.779478096334365}, + types.Point{-122.42322385450814, 37.77947636515649}, + types.Point{-122.4232025600581, 37.77947123815131}, + types.Point{-122.42318293498623, 37.77946291234741}, + types.Point{-122.42316573347469, 37.779451707701796}, + types.Point{-122.42315161656855, 37.77943805480385}, + types.Point{-122.423141126772, 37.779422478327966}, + types.Point{-122.42313466720033, 37.7794055768704}, + types.Point{-122.42313248608872, 37.77938799994533}, + types.Point{-122.42313466725305, 37.77937042302436}, + types.Point{-122.42314112686942, 37.77935352157849}, + types.Point{-122.42315161669585, 37.77933794512014}, + types.Point{-122.42316573361246, 37.77932429224285}, + types.Point{-122.4231829351135, 37.77931308761787}, + types.Point{-122.42320256015552, 37.779304761831504}, + types.Point{-122.42322385456086, 37.77929963483801}, + types.Point{-122.423246, 37.77929790366427}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialCirclePoint(c *test.C) { + var response types.Geometry + res, err := Circle(Point(-122.423246, 37.779388), 10).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ Type: "Polygon", - Lines: Lines{ - Line{ - Point{-122.423246, 37.779388}, - Point{-122.423246, 37.329898}, - Point{-121.88642, 37.329898}, - Point{-121.88642, 37.779388}, - Point{-122.423246, 37.779388}, + Lines: types.Lines{ + types.Line{ + types.Point{-122.423246, 37.77929790366427}, + types.Point{-122.42326814543915, 37.77929963483801}, + types.Point{-122.4232894398445, 37.779304761831504}, + types.Point{-122.42330906488651, 37.77931308761787}, + types.Point{-122.42332626638755, 37.77932429224285}, + types.Point{-122.42334038330416, 37.77933794512014}, + types.Point{-122.42335087313059, 37.77935352157849}, + types.Point{-122.42335733274696, 37.77937042302436}, + types.Point{-122.4233595139113, 37.77938799994533}, + types.Point{-122.42335733279968, 37.7794055768704}, + types.Point{-122.42335087322802, 37.779422478327966}, + types.Point{-122.42334038343147, 37.77943805480385}, + types.Point{-122.42332626652532, 37.779451707701796}, + types.Point{-122.42330906501378, 37.77946291234741}, + types.Point{-122.42328943994191, 37.77947123815131}, + types.Point{-122.42326814549187, 37.77947636515649}, + types.Point{-122.423246, 37.779478096334365}, + types.Point{-122.42322385450814, 37.77947636515649}, + types.Point{-122.4232025600581, 37.77947123815131}, + types.Point{-122.42318293498623, 37.77946291234741}, + types.Point{-122.42316573347469, 37.779451707701796}, + types.Point{-122.42315161656855, 37.77943805480385}, + types.Point{-122.423141126772, 37.779422478327966}, + types.Point{-122.42313466720033, 37.7794055768704}, + types.Point{-122.42313248608872, 37.77938799994533}, + types.Point{-122.42313466725305, 37.77937042302436}, + types.Point{-122.42314112686942, 37.77935352157849}, + types.Point{-122.42315161669585, 37.77933794512014}, + types.Point{-122.42316573361246, 37.77932429224285}, + types.Point{-122.4231829351135, 37.77931308761787}, + types.Point{-122.42320256015552, 37.779304761831504}, + types.Point{-122.42322385456086, 37.77929963483801}, + types.Point{-122.423246, 37.77929790366427}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialCirclePointFill(c *test.C) { + var response types.Geometry + res, err := Circle(Point(-122.423246, 37.779388), 10, CircleOpts{Fill: true}).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{-122.423246, 37.77929790366427}, + types.Point{-122.42326814543915, 37.77929963483801}, + types.Point{-122.4232894398445, 37.779304761831504}, + types.Point{-122.42330906488651, 37.77931308761787}, + types.Point{-122.42332626638755, 37.77932429224285}, + types.Point{-122.42334038330416, 37.77933794512014}, + types.Point{-122.42335087313059, 37.77935352157849}, + types.Point{-122.42335733274696, 37.77937042302436}, + types.Point{-122.4233595139113, 37.77938799994533}, + types.Point{-122.42335733279968, 37.7794055768704}, + types.Point{-122.42335087322802, 37.779422478327966}, + types.Point{-122.42334038343147, 37.77943805480385}, + types.Point{-122.42332626652532, 37.779451707701796}, + types.Point{-122.42330906501378, 37.77946291234741}, + types.Point{-122.42328943994191, 37.77947123815131}, + types.Point{-122.42326814549187, 37.77947636515649}, + types.Point{-122.423246, 37.779478096334365}, + types.Point{-122.42322385450814, 37.77947636515649}, + types.Point{-122.4232025600581, 37.77947123815131}, + types.Point{-122.42318293498623, 37.77946291234741}, + types.Point{-122.42316573347469, 37.779451707701796}, + types.Point{-122.42315161656855, 37.77943805480385}, + types.Point{-122.423141126772, 37.779422478327966}, + types.Point{-122.42313466720033, 37.7794055768704}, + types.Point{-122.42313248608872, 37.77938799994533}, + types.Point{-122.42313466725305, 37.77937042302436}, + types.Point{-122.42314112686942, 37.77935352157849}, + types.Point{-122.42315161669585, 37.77933794512014}, + types.Point{-122.42316573361246, 37.77932429224285}, + types.Point{-122.4231829351135, 37.77931308761787}, + types.Point{-122.42320256015552, 37.779304761831504}, + types.Point{-122.42322385456086, 37.77929963483801}, + types.Point{-122.423246, 37.77929790366427}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialPointDistanceMethod(c *test.C) { + var response float64 + res, err := Point(-122.423246, 37.779388).Distance(Point(-117.220406, 32.719464)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, 734125.249602186) +} + +func (s *RethinkSuite) TestGeospatialPointDistanceRoot(c *test.C) { + var response float64 + res, err := Distance(Point(-122.423246, 37.779388), Point(-117.220406, 32.719464)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, 734125.249602186) +} + +func (s *RethinkSuite) TestGeospatialPointDistanceRootKm(c *test.C) { + var response float64 + res, err := Distance(Point(-122.423246, 37.779388), Point(-117.220406, 32.719464), DistanceOpts{Unit: "km"}).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, 734.125249602186) +} + +func (s *RethinkSuite) TestGeospatialFill(c *test.C) { + var response types.Geometry + res, err := Line( + []float64{-122.423246, 37.779388}, + []float64{-122.423246, 37.329898}, + []float64{-121.886420, 37.329898}, + []float64{-121.886420, 37.779388}, + ).Fill().Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{-122.423246, 37.779388}, + types.Point{-122.423246, 37.329898}, + types.Point{-121.88642, 37.329898}, + types.Point{-121.88642, 37.779388}, + types.Point{-122.423246, 37.779388}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialGeojson(c *test.C) { + var response types.Geometry + res, err := Geojson(map[string]interface{}{ + "type": "Point", + "coordinates": []interface{}{-122.423246, 37.779388}, + }).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Point", + Point: types.Point{-122.423246, 37.779388}, + }) +} + +func (s *RethinkSuite) TestGeospatialToGeojson(c *test.C) { + var response map[string]interface{} + res, err := Point(-122.423246, 37.779388).ToGeojson().Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, map[string]interface{}{ + "type": "Point", + "coordinates": []interface{}{-122.423246, 37.779388}, + }) +} + +func (s *RethinkSuite) TestGeospatialGetIntersecting(c *test.C) { + // Setup table + Db("test").TableDrop("geospatial").Run(sess) + Db("test").TableCreate("geospatial").Run(sess) + Db("test").Table("geospatial").IndexCreate("area", IndexCreateOpts{ + Geo: true, + }).Run(sess) + Db("test").Table("geospatial").Insert([]interface{}{ + map[string]interface{}{"area": Circle(Point(-117.220406, 32.719464), 100000)}, + map[string]interface{}{"area": Circle(Point(-100.220406, 20.719464), 100000)}, + map[string]interface{}{"area": Circle(Point(-117.200406, 32.723464), 100000)}, + }).Run(sess) + + var response []interface{} + res, err := Db("test").Table("geospatial").GetIntersecting( + Circle(Point(-117.220406, 32.719464), 100000), + GetIntersectingOpts{ + Index: "area", + }, + ).Run(sess) + c.Assert(err, test.IsNil) + + err = res.All(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.HasLen, 2) +} + +func (s *RethinkSuite) TestGeospatialGetNearest(c *test.C) { + // Setup table + Db("test").TableDrop("geospatial").Run(sess) + Db("test").TableCreate("geospatial").Run(sess) + Db("test").Table("geospatial").IndexCreate("area", IndexCreateOpts{ + Geo: true, + }).Run(sess) + Db("test").Table("geospatial").Insert([]interface{}{ + map[string]interface{}{"area": Circle(Point(-117.220406, 32.719464), 100000)}, + map[string]interface{}{"area": Circle(Point(-100.220406, 20.719464), 100000)}, + map[string]interface{}{"area": Circle(Point(-115.210306, 32.733364), 100000)}, + }).Run(sess) + + var response []interface{} + res, err := Db("test").Table("geospatial").GetNearest( + Point(-117.220406, 32.719464), + GetNearestOpts{ + Index: "area", + MaxDist: 1, + }, + ).Run(sess) + c.Assert(err, test.IsNil) + + err = res.All(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, test.HasLen, 1) +} + +func (s *RethinkSuite) TestGeospatialIncludesTrue(c *test.C) { + var response bool + res, err := Polygon( + Point(-122.4, 37.7), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.7), + ).Includes(Point(-122.3, 37.4)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, true) +} + +func (s *RethinkSuite) TestGeospatialIncludesFalse(c *test.C) { + var response bool + res, err := Polygon( + Point(-122.4, 37.7), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.7), + ).Includes(Point(100.3, 37.4)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, false) +} + +func (s *RethinkSuite) TestGeospatialIntersectsTrue(c *test.C) { + var response bool + res, err := Polygon( + Point(-122.4, 37.7), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.7), + ).Intersects(Polygon( + Point(-122.3, 37.4), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.4), + )).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, true) +} + +func (s *RethinkSuite) TestGeospatialIntersectsFalse(c *test.C) { + var response bool + res, err := Polygon( + Point(-122.4, 37.7), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.7), + ).Intersects(Polygon( + Point(-102.4, 37.7), + Point(-102.4, 37.3), + Point(-101.8, 37.3), + Point(-101.8, 37.7), + )).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.Equals, false) +} + +func (s *RethinkSuite) TestGeospatialLineLatLon(c *test.C) { + var response types.Geometry + res, err := Line([]float64{-122.423246, 37.779388}, []float64{-121.886420, 37.329898}).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "LineString", + Line: types.Line{ + types.Point{-122.423246, 37.779388}, + types.Point{-121.886420, 37.329898}, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialLinePoint(c *test.C) { + var response types.Geometry + res, err := Line(Point(-122.423246, 37.779388), Point(-121.886420, 37.329898)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "LineString", + Line: types.Line{ + types.Point{-122.423246, 37.779388}, + types.Point{-121.886420, 37.329898}, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialPoint(c *test.C) { + var response types.Geometry + res, err := Point(-122.423246, 37.779388).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Point", + Point: types.Point{-122.423246, 37.779388}, + }) +} + +func (s *RethinkSuite) TestGeospatialPolygon(c *test.C) { + var response types.Geometry + res, err := Polygon(Point(-122.423246, 37.779388), Point(-122.423246, 37.329898), Point(-121.886420, 37.329898)).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{Lat: -122.423246, Lon: 37.779388}, + types.Point{Lat: -122.423246, Lon: 37.329898}, + types.Point{Lat: -121.88642, Lon: 37.329898}, + types.Point{Lat: -122.423246, Lon: 37.779388}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialPolygonSub(c *test.C) { + var response types.Geometry + res, err := Polygon( + Point(-122.4, 37.7), + Point(-122.4, 37.3), + Point(-121.8, 37.3), + Point(-121.8, 37.7), + ).PolygonSub(Polygon( + Point(-122.3, 37.4), + Point(-122.3, 37.6), + Point(-122.0, 37.6), + Point(-122.0, 37.4), + )).Run(sess) + c.Assert(err, test.IsNil) + + err = res.One(&response) + c.Assert(err, test.IsNil) + c.Assert(response, test.DeepEquals, types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{-122.4, 37.7}, + types.Point{-122.4, 37.3}, + types.Point{-121.8, 37.3}, + types.Point{-121.8, 37.7}, + types.Point{-122.4, 37.7}, + }, + types.Line{ + types.Point{-122.3, 37.4}, + types.Point{-122.3, 37.6}, + types.Point{-122, 37.6}, + types.Point{-122, 37.4}, + types.Point{-122.3, 37.4}, }, }, }) diff --git a/query_table.go b/query_table.go index fec297c8..615b2abb 100644 --- a/query_table.go +++ b/query_table.go @@ -47,6 +47,7 @@ func (t Term) TableList(args ...interface{}) Term { type IndexCreateOpts struct { Multi interface{} `gorethink:"multi,omitempty"` + Geo interface{} `gorethink:"geo,omitempty"` } func (o *IndexCreateOpts) toMap() map[string]interface{} { diff --git a/types/geometry.go b/types/geometry.go new file mode 100644 index 00000000..14247f3c --- /dev/null +++ b/types/geometry.go @@ -0,0 +1,14 @@ +package types + +type Geometry struct { + Type string + Point Point + Line Line + Lines Lines +} + +type Point struct { + Lat, Lon float64 +} +type Line []Point +type Lines []Line From a08fa463fb6ecb0b2dbd24a95bfdcb59c1e8e56d Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 19:28:50 +0100 Subject: [PATCH 12/18] Added geometry encoding + fixed tests --- encoding/encoder_types.go | 28 +++- pseudotypes.go | 60 +------- query_geospatial_test.go | 289 +++++++++++++++++++++----------------- types/geometry.go | 82 ++++++++++- 4 files changed, 270 insertions(+), 189 deletions(-) diff --git a/encoding/encoder_types.go b/encoding/encoder_types.go index c93464c7..7423dde8 100644 --- a/encoding/encoder_types.go +++ b/encoding/encoder_types.go @@ -7,13 +7,16 @@ import ( "reflect" "strconv" "time" + + "github.com/dancannon/gorethink/types" ) var ( marshalerType = reflect.TypeOf(new(Marshaler)).Elem() textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() - timeType = reflect.TypeOf(new(time.Time)).Elem() + timeType = reflect.TypeOf(new(time.Time)).Elem() + geometryType = reflect.TypeOf(new(types.Geometry)).Elem() ) // newTypeEncoder constructs an encoderFunc for a type. @@ -31,6 +34,8 @@ func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { switch t { case timeType: return timePseudoTypeEncoder + case geometryType: + return geometryPseudoTypeEncoder } switch t.Kind() { @@ -318,6 +323,27 @@ func timePseudoTypeEncoder(v reflect.Value) interface{} { } } +// Encode a time.Time value to the TIME RQL type +func geometryPseudoTypeEncoder(v reflect.Value) interface{} { + g := v.Interface().(types.Geometry) + + var coords interface{} + switch g.Type { + case "Point": + coords = g.Point.Marshal() + case "LineString": + coords = g.Line.Marshal() + case "Polygon": + coords = g.Lines.Marshal() + } + + return map[string]interface{}{ + "$reql_type$": "GEOMETRY", + "type": g.Type, + "coordinates": coords, + } +} + // Encode a byte slice to the BINARY RQL type func encodeByteSlice(v reflect.Value) interface{} { var b []byte diff --git a/pseudotypes.go b/pseudotypes.go index b273c51e..6949fa83 100644 --- a/pseudotypes.go +++ b/pseudotypes.go @@ -190,7 +190,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro } else if coords, ok := obj["coordinates"]; !ok { return nil, fmt.Errorf("pseudo-type GEOMETRY object %v does not have the expected field \"coordinates\"", obj) } else if typ == "Point" { - if point, err := unmarshalPoint(coords); err != nil { + if point, err := types.UnmarshalPoint(coords); err != nil { return nil, err } else { return types.Geometry{ @@ -199,7 +199,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro }, nil } } else if typ == "LineString" { - if line, err := unmarshalLineString(coords); err != nil { + if line, err := types.UnmarshalLineString(coords); err != nil { return nil, err } else { return types.Geometry{ @@ -208,7 +208,7 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro }, nil } } else if typ == "Polygon" { - if lines, err := unmarshalPolygon(coords); err != nil { + if lines, err := types.UnmarshalPolygon(coords); err != nil { return nil, err } else { return types.Geometry{ @@ -220,57 +220,3 @@ func reqlGeometryToNativeGeometry(obj map[string]interface{}) (interface{}, erro return nil, fmt.Errorf("pseudo-type GEOMETRY object %v field has unknown type %s", typ) } } - -func unmarshalPoint(v interface{}) (types.Point, error) { - coords, ok := v.([]interface{}) - if !ok { - return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - if len(coords) != 2 { - return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - lat, ok := coords[0].(float64) - if !ok { - return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - lon, ok := coords[1].(float64) - if !ok { - return types.Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - - return types.Point{lat, lon}, nil -} - -func unmarshalLineString(v interface{}) (types.Line, error) { - points, ok := v.([]interface{}) - if !ok { - return types.Line{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - - var err error - line := make(types.Line, len(points)) - for i, coords := range points { - line[i], err = unmarshalPoint(coords) - if err != nil { - return types.Line{}, err - } - } - return line, nil -} - -func unmarshalPolygon(v interface{}) (types.Lines, error) { - lines, ok := v.([]interface{}) - if !ok { - return types.Lines{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) - } - - var err error - polygon := make(types.Lines, len(lines)) - for i, line := range lines { - polygon[i], err = unmarshalLineString(line) - if err != nil { - return types.Lines{}, err - } - } - return polygon, nil -} diff --git a/query_geospatial_test.go b/query_geospatial_test.go index bb4c3a01..a576bd60 100644 --- a/query_geospatial_test.go +++ b/query_geospatial_test.go @@ -6,7 +6,7 @@ import ( test "gopkg.in/check.v1" ) -func (s *RethinkSuite) TestGeospatialGeometryPseudoType(c *test.C) { +func (s *RethinkSuite) TestGeospatialDecodeGeometryPseudoType(c *test.C) { var response types.Geometry res, err := Expr(map[string]interface{}{ "$reql_type$": "GEOMETRY", @@ -29,11 +29,40 @@ func (s *RethinkSuite) TestGeospatialGeometryPseudoType(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.423246, 37.779388}, - types.Point{-122.423246, 37.329898}, - types.Point{-121.88642, 37.329898}, - types.Point{-121.88642, 37.779388}, - types.Point{-122.423246, 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + }, + }, + }) +} + +func (s *RethinkSuite) TestGeospatialEncodeGeometryPseudoType(c *test.C) { + encoded, err := encode(types.Geometry{ + Type: "Polygon", + Lines: types.Lines{ + types.Line{ + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + }, + }, + }) + c.Assert(err, test.IsNil) + c.Assert(encoded, test.DeepEquals, map[string]interface{}{ + "$reql_type$": "GEOMETRY", + "type": "Polygon", + "coordinates": []interface{}{ + []interface{}{ + []interface{}{-122.423246, 37.779388}, + []interface{}{-122.423246, 37.329898}, + []interface{}{-121.88642, 37.329898}, + []interface{}{-121.88642, 37.779388}, + []interface{}{-122.423246, 37.779388}, }, }, }) @@ -50,39 +79,39 @@ func (s *RethinkSuite) TestGeospatialCircle(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.423246, 37.77929790366427}, - types.Point{-122.42326814543915, 37.77929963483801}, - types.Point{-122.4232894398445, 37.779304761831504}, - types.Point{-122.42330906488651, 37.77931308761787}, - types.Point{-122.42332626638755, 37.77932429224285}, - types.Point{-122.42334038330416, 37.77933794512014}, - types.Point{-122.42335087313059, 37.77935352157849}, - types.Point{-122.42335733274696, 37.77937042302436}, - types.Point{-122.4233595139113, 37.77938799994533}, - types.Point{-122.42335733279968, 37.7794055768704}, - types.Point{-122.42335087322802, 37.779422478327966}, - types.Point{-122.42334038343147, 37.77943805480385}, - types.Point{-122.42332626652532, 37.779451707701796}, - types.Point{-122.42330906501378, 37.77946291234741}, - types.Point{-122.42328943994191, 37.77947123815131}, - types.Point{-122.42326814549187, 37.77947636515649}, - types.Point{-122.423246, 37.779478096334365}, - types.Point{-122.42322385450814, 37.77947636515649}, - types.Point{-122.4232025600581, 37.77947123815131}, - types.Point{-122.42318293498623, 37.77946291234741}, - types.Point{-122.42316573347469, 37.779451707701796}, - types.Point{-122.42315161656855, 37.77943805480385}, - types.Point{-122.423141126772, 37.779422478327966}, - types.Point{-122.42313466720033, 37.7794055768704}, - types.Point{-122.42313248608872, 37.77938799994533}, - types.Point{-122.42313466725305, 37.77937042302436}, - types.Point{-122.42314112686942, 37.77935352157849}, - types.Point{-122.42315161669585, 37.77933794512014}, - types.Point{-122.42316573361246, 37.77932429224285}, - types.Point{-122.4231829351135, 37.77931308761787}, - types.Point{-122.42320256015552, 37.779304761831504}, - types.Point{-122.42322385456086, 37.77929963483801}, - types.Point{-122.423246, 37.77929790366427}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, + types.Point{Lon: -122.42326814543915, Lat: 37.77929963483801}, + types.Point{Lon: -122.4232894398445, Lat: 37.779304761831504}, + types.Point{Lon: -122.42330906488651, Lat: 37.77931308761787}, + types.Point{Lon: -122.42332626638755, Lat: 37.77932429224285}, + types.Point{Lon: -122.42334038330416, Lat: 37.77933794512014}, + types.Point{Lon: -122.42335087313059, Lat: 37.77935352157849}, + types.Point{Lon: -122.42335733274696, Lat: 37.77937042302436}, + types.Point{Lon: -122.4233595139113, Lat: 37.77938799994533}, + types.Point{Lon: -122.42335733279968, Lat: 37.7794055768704}, + types.Point{Lon: -122.42335087322802, Lat: 37.779422478327966}, + types.Point{Lon: -122.42334038343147, Lat: 37.77943805480385}, + types.Point{Lon: -122.42332626652532, Lat: 37.779451707701796}, + types.Point{Lon: -122.42330906501378, Lat: 37.77946291234741}, + types.Point{Lon: -122.42328943994191, Lat: 37.77947123815131}, + types.Point{Lon: -122.42326814549187, Lat: 37.77947636515649}, + types.Point{Lon: -122.423246, Lat: 37.779478096334365}, + types.Point{Lon: -122.42322385450814, Lat: 37.77947636515649}, + types.Point{Lon: -122.4232025600581, Lat: 37.77947123815131}, + types.Point{Lon: -122.42318293498623, Lat: 37.77946291234741}, + types.Point{Lon: -122.42316573347469, Lat: 37.779451707701796}, + types.Point{Lon: -122.42315161656855, Lat: 37.77943805480385}, + types.Point{Lon: -122.423141126772, Lat: 37.779422478327966}, + types.Point{Lon: -122.42313466720033, Lat: 37.7794055768704}, + types.Point{Lon: -122.42313248608872, Lat: 37.77938799994533}, + types.Point{Lon: -122.42313466725305, Lat: 37.77937042302436}, + types.Point{Lon: -122.42314112686942, Lat: 37.77935352157849}, + types.Point{Lon: -122.42315161669585, Lat: 37.77933794512014}, + types.Point{Lon: -122.42316573361246, Lat: 37.77932429224285}, + types.Point{Lon: -122.4231829351135, Lat: 37.77931308761787}, + types.Point{Lon: -122.42320256015552, Lat: 37.779304761831504}, + types.Point{Lon: -122.42322385456086, Lat: 37.77929963483801}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, }, }, }) @@ -99,39 +128,39 @@ func (s *RethinkSuite) TestGeospatialCirclePoint(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.423246, 37.77929790366427}, - types.Point{-122.42326814543915, 37.77929963483801}, - types.Point{-122.4232894398445, 37.779304761831504}, - types.Point{-122.42330906488651, 37.77931308761787}, - types.Point{-122.42332626638755, 37.77932429224285}, - types.Point{-122.42334038330416, 37.77933794512014}, - types.Point{-122.42335087313059, 37.77935352157849}, - types.Point{-122.42335733274696, 37.77937042302436}, - types.Point{-122.4233595139113, 37.77938799994533}, - types.Point{-122.42335733279968, 37.7794055768704}, - types.Point{-122.42335087322802, 37.779422478327966}, - types.Point{-122.42334038343147, 37.77943805480385}, - types.Point{-122.42332626652532, 37.779451707701796}, - types.Point{-122.42330906501378, 37.77946291234741}, - types.Point{-122.42328943994191, 37.77947123815131}, - types.Point{-122.42326814549187, 37.77947636515649}, - types.Point{-122.423246, 37.779478096334365}, - types.Point{-122.42322385450814, 37.77947636515649}, - types.Point{-122.4232025600581, 37.77947123815131}, - types.Point{-122.42318293498623, 37.77946291234741}, - types.Point{-122.42316573347469, 37.779451707701796}, - types.Point{-122.42315161656855, 37.77943805480385}, - types.Point{-122.423141126772, 37.779422478327966}, - types.Point{-122.42313466720033, 37.7794055768704}, - types.Point{-122.42313248608872, 37.77938799994533}, - types.Point{-122.42313466725305, 37.77937042302436}, - types.Point{-122.42314112686942, 37.77935352157849}, - types.Point{-122.42315161669585, 37.77933794512014}, - types.Point{-122.42316573361246, 37.77932429224285}, - types.Point{-122.4231829351135, 37.77931308761787}, - types.Point{-122.42320256015552, 37.779304761831504}, - types.Point{-122.42322385456086, 37.77929963483801}, - types.Point{-122.423246, 37.77929790366427}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, + types.Point{Lon: -122.42326814543915, Lat: 37.77929963483801}, + types.Point{Lon: -122.4232894398445, Lat: 37.779304761831504}, + types.Point{Lon: -122.42330906488651, Lat: 37.77931308761787}, + types.Point{Lon: -122.42332626638755, Lat: 37.77932429224285}, + types.Point{Lon: -122.42334038330416, Lat: 37.77933794512014}, + types.Point{Lon: -122.42335087313059, Lat: 37.77935352157849}, + types.Point{Lon: -122.42335733274696, Lat: 37.77937042302436}, + types.Point{Lon: -122.4233595139113, Lat: 37.77938799994533}, + types.Point{Lon: -122.42335733279968, Lat: 37.7794055768704}, + types.Point{Lon: -122.42335087322802, Lat: 37.779422478327966}, + types.Point{Lon: -122.42334038343147, Lat: 37.77943805480385}, + types.Point{Lon: -122.42332626652532, Lat: 37.779451707701796}, + types.Point{Lon: -122.42330906501378, Lat: 37.77946291234741}, + types.Point{Lon: -122.42328943994191, Lat: 37.77947123815131}, + types.Point{Lon: -122.42326814549187, Lat: 37.77947636515649}, + types.Point{Lon: -122.423246, Lat: 37.779478096334365}, + types.Point{Lon: -122.42322385450814, Lat: 37.77947636515649}, + types.Point{Lon: -122.4232025600581, Lat: 37.77947123815131}, + types.Point{Lon: -122.42318293498623, Lat: 37.77946291234741}, + types.Point{Lon: -122.42316573347469, Lat: 37.779451707701796}, + types.Point{Lon: -122.42315161656855, Lat: 37.77943805480385}, + types.Point{Lon: -122.423141126772, Lat: 37.779422478327966}, + types.Point{Lon: -122.42313466720033, Lat: 37.7794055768704}, + types.Point{Lon: -122.42313248608872, Lat: 37.77938799994533}, + types.Point{Lon: -122.42313466725305, Lat: 37.77937042302436}, + types.Point{Lon: -122.42314112686942, Lat: 37.77935352157849}, + types.Point{Lon: -122.42315161669585, Lat: 37.77933794512014}, + types.Point{Lon: -122.42316573361246, Lat: 37.77932429224285}, + types.Point{Lon: -122.4231829351135, Lat: 37.77931308761787}, + types.Point{Lon: -122.42320256015552, Lat: 37.779304761831504}, + types.Point{Lon: -122.42322385456086, Lat: 37.77929963483801}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, }, }, }) @@ -148,39 +177,39 @@ func (s *RethinkSuite) TestGeospatialCirclePointFill(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.423246, 37.77929790366427}, - types.Point{-122.42326814543915, 37.77929963483801}, - types.Point{-122.4232894398445, 37.779304761831504}, - types.Point{-122.42330906488651, 37.77931308761787}, - types.Point{-122.42332626638755, 37.77932429224285}, - types.Point{-122.42334038330416, 37.77933794512014}, - types.Point{-122.42335087313059, 37.77935352157849}, - types.Point{-122.42335733274696, 37.77937042302436}, - types.Point{-122.4233595139113, 37.77938799994533}, - types.Point{-122.42335733279968, 37.7794055768704}, - types.Point{-122.42335087322802, 37.779422478327966}, - types.Point{-122.42334038343147, 37.77943805480385}, - types.Point{-122.42332626652532, 37.779451707701796}, - types.Point{-122.42330906501378, 37.77946291234741}, - types.Point{-122.42328943994191, 37.77947123815131}, - types.Point{-122.42326814549187, 37.77947636515649}, - types.Point{-122.423246, 37.779478096334365}, - types.Point{-122.42322385450814, 37.77947636515649}, - types.Point{-122.4232025600581, 37.77947123815131}, - types.Point{-122.42318293498623, 37.77946291234741}, - types.Point{-122.42316573347469, 37.779451707701796}, - types.Point{-122.42315161656855, 37.77943805480385}, - types.Point{-122.423141126772, 37.779422478327966}, - types.Point{-122.42313466720033, 37.7794055768704}, - types.Point{-122.42313248608872, 37.77938799994533}, - types.Point{-122.42313466725305, 37.77937042302436}, - types.Point{-122.42314112686942, 37.77935352157849}, - types.Point{-122.42315161669585, 37.77933794512014}, - types.Point{-122.42316573361246, 37.77932429224285}, - types.Point{-122.4231829351135, 37.77931308761787}, - types.Point{-122.42320256015552, 37.779304761831504}, - types.Point{-122.42322385456086, 37.77929963483801}, - types.Point{-122.423246, 37.77929790366427}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, + types.Point{Lon: -122.42326814543915, Lat: 37.77929963483801}, + types.Point{Lon: -122.4232894398445, Lat: 37.779304761831504}, + types.Point{Lon: -122.42330906488651, Lat: 37.77931308761787}, + types.Point{Lon: -122.42332626638755, Lat: 37.77932429224285}, + types.Point{Lon: -122.42334038330416, Lat: 37.77933794512014}, + types.Point{Lon: -122.42335087313059, Lat: 37.77935352157849}, + types.Point{Lon: -122.42335733274696, Lat: 37.77937042302436}, + types.Point{Lon: -122.4233595139113, Lat: 37.77938799994533}, + types.Point{Lon: -122.42335733279968, Lat: 37.7794055768704}, + types.Point{Lon: -122.42335087322802, Lat: 37.779422478327966}, + types.Point{Lon: -122.42334038343147, Lat: 37.77943805480385}, + types.Point{Lon: -122.42332626652532, Lat: 37.779451707701796}, + types.Point{Lon: -122.42330906501378, Lat: 37.77946291234741}, + types.Point{Lon: -122.42328943994191, Lat: 37.77947123815131}, + types.Point{Lon: -122.42326814549187, Lat: 37.77947636515649}, + types.Point{Lon: -122.423246, Lat: 37.779478096334365}, + types.Point{Lon: -122.42322385450814, Lat: 37.77947636515649}, + types.Point{Lon: -122.4232025600581, Lat: 37.77947123815131}, + types.Point{Lon: -122.42318293498623, Lat: 37.77946291234741}, + types.Point{Lon: -122.42316573347469, Lat: 37.779451707701796}, + types.Point{Lon: -122.42315161656855, Lat: 37.77943805480385}, + types.Point{Lon: -122.423141126772, Lat: 37.779422478327966}, + types.Point{Lon: -122.42313466720033, Lat: 37.7794055768704}, + types.Point{Lon: -122.42313248608872, Lat: 37.77938799994533}, + types.Point{Lon: -122.42313466725305, Lat: 37.77937042302436}, + types.Point{Lon: -122.42314112686942, Lat: 37.77935352157849}, + types.Point{Lon: -122.42315161669585, Lat: 37.77933794512014}, + types.Point{Lon: -122.42316573361246, Lat: 37.77932429224285}, + types.Point{Lon: -122.4231829351135, Lat: 37.77931308761787}, + types.Point{Lon: -122.42320256015552, Lat: 37.779304761831504}, + types.Point{Lon: -122.42322385456086, Lat: 37.77929963483801}, + types.Point{Lon: -122.423246, Lat: 37.77929790366427}, }, }, }) @@ -232,11 +261,11 @@ func (s *RethinkSuite) TestGeospatialFill(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.423246, 37.779388}, - types.Point{-122.423246, 37.329898}, - types.Point{-121.88642, 37.329898}, - types.Point{-121.88642, 37.779388}, - types.Point{-122.423246, 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, }, }, }) @@ -254,7 +283,7 @@ func (s *RethinkSuite) TestGeospatialGeojson(c *test.C) { c.Assert(err, test.IsNil) c.Assert(response, test.DeepEquals, types.Geometry{ Type: "Point", - Point: types.Point{-122.423246, 37.779388}, + Point: types.Point{Lon: -122.423246, Lat: 37.779388}, }) } @@ -407,8 +436,8 @@ func (s *RethinkSuite) TestGeospatialLineLatLon(c *test.C) { c.Assert(response, test.DeepEquals, types.Geometry{ Type: "LineString", Line: types.Line{ - types.Point{-122.423246, 37.779388}, - types.Point{-121.886420, 37.329898}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -121.886420, Lat: 37.329898}, }, }) } @@ -423,8 +452,8 @@ func (s *RethinkSuite) TestGeospatialLinePoint(c *test.C) { c.Assert(response, test.DeepEquals, types.Geometry{ Type: "LineString", Line: types.Line{ - types.Point{-122.423246, 37.779388}, - types.Point{-121.886420, 37.329898}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -121.886420, Lat: 37.329898}, }, }) } @@ -438,7 +467,7 @@ func (s *RethinkSuite) TestGeospatialPoint(c *test.C) { c.Assert(err, test.IsNil) c.Assert(response, test.DeepEquals, types.Geometry{ Type: "Point", - Point: types.Point{-122.423246, 37.779388}, + Point: types.Point{Lon: -122.423246, Lat: 37.779388}, }) } @@ -453,10 +482,10 @@ func (s *RethinkSuite) TestGeospatialPolygon(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{Lat: -122.423246, Lon: 37.779388}, - types.Point{Lat: -122.423246, Lon: 37.329898}, - types.Point{Lat: -121.88642, Lon: 37.329898}, - types.Point{Lat: -122.423246, Lon: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.779388}, + types.Point{Lon: -122.423246, Lat: 37.329898}, + types.Point{Lon: -121.88642, Lat: 37.329898}, + types.Point{Lon: -122.423246, Lat: 37.779388}, }, }, }) @@ -483,18 +512,18 @@ func (s *RethinkSuite) TestGeospatialPolygonSub(c *test.C) { Type: "Polygon", Lines: types.Lines{ types.Line{ - types.Point{-122.4, 37.7}, - types.Point{-122.4, 37.3}, - types.Point{-121.8, 37.3}, - types.Point{-121.8, 37.7}, - types.Point{-122.4, 37.7}, + types.Point{Lon: -122.4, Lat: 37.7}, + types.Point{Lon: -122.4, Lat: 37.3}, + types.Point{Lon: -121.8, Lat: 37.3}, + types.Point{Lon: -121.8, Lat: 37.7}, + types.Point{Lon: -122.4, Lat: 37.7}, }, types.Line{ - types.Point{-122.3, 37.4}, - types.Point{-122.3, 37.6}, - types.Point{-122, 37.6}, - types.Point{-122, 37.4}, - types.Point{-122.3, 37.4}, + types.Point{Lon: -122.3, Lat: 37.4}, + types.Point{Lon: -122.3, Lat: 37.6}, + types.Point{Lon: -122, Lat: 37.6}, + types.Point{Lon: -122, Lat: 37.4}, + types.Point{Lon: -122.3, Lat: 37.4}, }, }, }) diff --git a/types/geometry.go b/types/geometry.go index 14247f3c..a63c7f1e 100644 --- a/types/geometry.go +++ b/types/geometry.go @@ -1,5 +1,7 @@ package types +import "fmt" + type Geometry struct { Type string Point Point @@ -8,7 +10,85 @@ type Geometry struct { } type Point struct { - Lat, Lon float64 + Lon float64 + Lat float64 } type Line []Point type Lines []Line + +func (p Point) Marshal() interface{} { + return []interface{}{p.Lon, p.Lat} +} + +func (l Line) Marshal() interface{} { + coords := make([]interface{}, len(l)) + for i, point := range l { + coords[i] = point.Marshal() + } + return coords +} + +func (l Lines) Marshal() interface{} { + coords := make([]interface{}, len(l)) + for i, line := range l { + coords[i] = line.Marshal() + } + return coords +} + +func UnmarshalPoint(v interface{}) (Point, error) { + coords, ok := v.([]interface{}) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + if len(coords) != 2 { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + lon, ok := coords[0].(float64) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + lat, ok := coords[1].(float64) + if !ok { + return Point{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + return Point{ + Lon: lon, + Lat: lat, + }, nil +} + +func UnmarshalLineString(v interface{}) (Line, error) { + points, ok := v.([]interface{}) + if !ok { + return Line{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + var err error + line := make(Line, len(points)) + for i, coords := range points { + line[i], err = UnmarshalPoint(coords) + if err != nil { + return Line{}, err + } + } + return line, nil +} + +func UnmarshalPolygon(v interface{}) (Lines, error) { + lines, ok := v.([]interface{}) + if !ok { + return Lines{}, fmt.Errorf("pseudo-type GEOMETRY object %v field \"coordinates\" is not valid", v) + } + + var err error + polygon := make(Lines, len(lines)) + for i, line := range lines { + polygon[i], err = UnmarshalLineString(line) + if err != nil { + return Lines{}, err + } + } + return polygon, nil +} From 6441e2680dac38f5b360677b4823cf34261a166c Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 19:47:17 +0100 Subject: [PATCH 13/18] Added UUID term --- query_control.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/query_control.go b/query_control.go index 568bbc54..1bed552f 100644 --- a/query_control.go +++ b/query_control.go @@ -228,3 +228,8 @@ func (t Term) TypeOf(args ...interface{}) Term { func (t Term) Info(args ...interface{}) Term { return constructMethodTerm(t, "Info", p.Term_INFO, args, map[string]interface{}{}) } + +// UUID returns a UUID (universally unique identifier), a string that can be used as a unique ID. +func UUID(args ...interface{}) Term { + return constructRootTerm("UUID", p.Term_UUID, []interface{}{}, map[string]interface{}{}) +} From af5a37009da42c2a1c69e11b048146e6a8fcdf84 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 19:54:38 +0100 Subject: [PATCH 14/18] Updated RunOpts --- query.go | 30 ++++++++++++++++++------------ query_geospatial.go | 4 ++-- query_select_test.go | 8 ++++++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/query.go b/query.go index 229195bf..00ae4011 100644 --- a/query.go +++ b/query.go @@ -119,18 +119,24 @@ type WriteChanges struct { } type RunOpts struct { - Db interface{} `gorethink:"db,omitempty"` - Profile interface{} `gorethink:"profile,omitempty"` - UseOutdated interface{} `gorethink:"use_outdated,omitempty"` - NoReply interface{} `gorethink:"noreply,omitempty"` - ArrayLimit interface{} `gorethink:"array_limit,omitempty"` - TimeFormat interface{} `gorethink:"time_format,omitempty"` - GroupFormat interface{} `gorethink:"group_format,omitempty"` - BinaryFormat interface{} `gorethink:"binary_format,omitempty"` - EncodeExpr interface{} `gorethink:"encode_expr,omitempty"` - - // Unsupported options - BatchConf interface{} `gorethink:"batch_conf,omitempty"` + Db interface{} `gorethink:"db,omitempty"` + Profile interface{} `gorethink:"profile,omitempty"` + UseOutdated interface{} `gorethink:"use_outdated,omitempty"` + NoReply interface{} `gorethink:"noreply,omitempty"` + ArrayLimit interface{} `gorethink:"array_limit,omitempty"` + TimeFormat interface{} `gorethink:"time_format,omitempty"` + GroupFormat interface{} `gorethink:"group_format,omitempty"` + BinaryFormat interface{} `gorethink:"binary_format,omitempty"` + GeometryFormat interface{} `gorethink:"geometry_format,omitempty"` + BatchConf BatchOpts `gorethink:"batch_conf,omitempty"` +} + +type BatchOpts struct { + MinBatchRows interface{} `gorethink:"min_batch_rows,omitempty"` + MaxBatchRows interface{} `gorethink:"max_batch_rows,omitempty"` + MaxBatchBytes interface{} `gorethink:"max_batch_bytes,omitempty"` + MaxBatchSeconds interface{} `gorethink:"max_batch_seconds,omitempty"` + FirstBatchScaledownFactor interface{} `gorethink:"first_batch_scaledown_factor,omitempty"` } func (o *RunOpts) toMap() map[string]interface{} { diff --git a/query_geospatial.go b/query_geospatial.go index 81883c18..2cf73d70 100644 --- a/query_geospatial.go +++ b/query_geospatial.go @@ -148,8 +148,8 @@ func Line(args ...interface{}) Term { // Point constructs a geometry object of type Point. The point is specified by // two floating point numbers, the longitude (−180 to 180) and latitude // (−90 to 90) of the point on a perfect sphere. -func Point(lat, lon interface{}) Term { - return constructRootTerm("Point", p.Term_POINT, []interface{}{lat, lon}, map[string]interface{}{}) +func Point(lon, lat interface{}) Term { + return constructRootTerm("Point", p.Term_POINT, []interface{}{lon, lat}, map[string]interface{}{}) } // Polygon constructs a geometry object of type Polygon. The Polygon can be diff --git a/query_select_test.go b/query_select_test.go index bd3cc1fc..a64064cb 100644 --- a/query_select_test.go +++ b/query_select_test.go @@ -291,7 +291,9 @@ func (s *RethinkSuite) TestSelectMany(c *test.C) { // Test query res, err := Db("test").Table("TestMany").Run(sess, RunOpts{ - BatchConf: map[string]interface{}{"max_els": 5, "max_size": 20}, + BatchConf: BatchOpts{ + MaxBatchRows: 1, + }, }) c.Assert(err, test.IsNil) @@ -332,7 +334,9 @@ func (s *RethinkSuite) TestConcurrentSelectMany(c *test.C) { for i := 0; i < attempts; i++ { go func(i int, c chan error) { res, err := Db("test").Table("TestMany").Run(sess, RunOpts{ - BatchConf: map[string]interface{}{"max_els": 5, "max_size": 20}, + BatchConf: BatchOpts{ + MaxBatchRows: 1, + }, }) if err != nil { c <- err From 2e9b7c2105f1cd64bbb4990a0b446359694482d4 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 21:27:19 +0100 Subject: [PATCH 15/18] Added AtIndex term --- query_transformation.go | 8 ++++++-- query_transformation_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/query_transformation.go b/query_transformation.go index 3c49a0ce..d244b9ab 100644 --- a/query_transformation.go +++ b/query_transformation.go @@ -87,7 +87,6 @@ func (o *SliceOpts) toMap() map[string]interface{} { return optArgsToMap(o) } -// TODO: Add optional arguments // Trim the sequence to within the bounds provided. func (t Term) Slice(args ...interface{}) Term { var opts = map[string]interface{}{} @@ -103,7 +102,12 @@ func (t Term) Slice(args ...interface{}) Term { return constructMethodTerm(t, "Slice", p.Term_SLICE, args, opts) } -// Get the nth element of a sequence. +// AtIndex gets a single field from an object or the nth element from a sequence. +func (t Term) AtIndex(args ...interface{}) Term { + return constructMethodTerm(t, "AtIndex", p.Term_BRACKET, args, map[string]interface{}{}) +} + +// Nth gets the nth element from a sequence. func (t Term) Nth(args ...interface{}) Term { return constructMethodTerm(t, "Nth", p.Term_NTH, args, map[string]interface{}{}) } diff --git a/query_transformation_test.go b/query_transformation_test.go index b0e6112b..8ae24bcc 100644 --- a/query_transformation_test.go +++ b/query_transformation_test.go @@ -311,6 +311,39 @@ func (s *RethinkSuite) TestTransformationNth(c *test.C) { c.Assert(response, JsonEquals, 3) } +func (s *RethinkSuite) TestTransformationAtIndexNth(c *test.C) { + query := Expr([]interface{}{1}).AtIndex(Expr(0)) + + var response interface{} + r, err := query.Run(sess) + c.Assert(err, test.IsNil) + + err = r.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, JsonEquals, 1) +} + +func (s *RethinkSuite) TestTransformationAtIndexField(c *test.C) { + query := Expr(map[string]interface{}{"foo": 1}).AtIndex(Expr("foo")) + + var response interface{} + r, err := query.Run(sess) + c.Assert(err, test.IsNil) + + err = r.One(&response) + + c.Assert(err, test.IsNil) + c.Assert(response, JsonEquals, 1) +} + +func (s *RethinkSuite) TestTransformationAtIndexArrayField(c *test.C) { + query := Expr([]interface{}{1}).AtIndex(Expr("foo")) + + _, err := query.Run(sess) + c.Assert(err, test.NotNil) +} + func (s *RethinkSuite) TestTransformationIndexesOf(c *test.C) { query := Expr(arr).IndexesOf(2) From 5a442544ce63cfc70b161d3faa817db2e5784843 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 21:31:36 +0100 Subject: [PATCH 16/18] Renamed result.go to cursor.go --- results.go => cursor.go | 0 results_test.go => cursor_test.go | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) rename results.go => cursor.go (100%) rename results_test.go => cursor_test.go (89%) diff --git a/results.go b/cursor.go similarity index 100% rename from results.go rename to cursor.go diff --git a/results_test.go b/cursor_test.go similarity index 89% rename from results_test.go rename to cursor_test.go index 835268d3..39ac9073 100644 --- a/results_test.go +++ b/cursor_test.go @@ -17,7 +17,7 @@ type attr struct { Value interface{} } -func (s *RethinkSuite) TestRowsLiteral(c *test.C) { +func (s *RethinkSuite) TestCursorLiteral(c *test.C) { res, err := Expr(5).Run(sess) c.Assert(err, test.IsNil) @@ -27,7 +27,7 @@ func (s *RethinkSuite) TestRowsLiteral(c *test.C) { c.Assert(response, JsonEquals, 5) } -func (s *RethinkSuite) TestRowsSlice(c *test.C) { +func (s *RethinkSuite) TestCursorSlice(c *test.C) { res, err := Expr([]interface{}{1, 2, 3, 4, 5}).Run(sess) c.Assert(err, test.IsNil) @@ -37,7 +37,7 @@ func (s *RethinkSuite) TestRowsSlice(c *test.C) { c.Assert(response, JsonEquals, []interface{}{1, 2, 3, 4, 5}) } -func (s *RethinkSuite) TestRowsPartiallyNilSlice(c *test.C) { +func (s *RethinkSuite) TestCursorPartiallyNilSlice(c *test.C) { res, err := Expr(map[string]interface{}{ "item": []interface{}{ map[string]interface{}{"num": 1}, @@ -57,7 +57,7 @@ func (s *RethinkSuite) TestRowsPartiallyNilSlice(c *test.C) { }) } -func (s *RethinkSuite) TestRowsMap(c *test.C) { +func (s *RethinkSuite) TestCursorMap(c *test.C) { res, err := Expr(map[string]interface{}{ "id": 2, "name": "Object 1", @@ -73,7 +73,7 @@ func (s *RethinkSuite) TestRowsMap(c *test.C) { }) } -func (s *RethinkSuite) TestRowsMapIntoInterface(c *test.C) { +func (s *RethinkSuite) TestCursorMapIntoInterface(c *test.C) { res, err := Expr(map[string]interface{}{ "id": 2, "name": "Object 1", @@ -89,7 +89,7 @@ func (s *RethinkSuite) TestRowsMapIntoInterface(c *test.C) { }) } -func (s *RethinkSuite) TestRowsMapNested(c *test.C) { +func (s *RethinkSuite) TestCursorMapNested(c *test.C) { res, err := Expr(map[string]interface{}{ "id": 2, "name": "Object 1", @@ -113,7 +113,7 @@ func (s *RethinkSuite) TestRowsMapNested(c *test.C) { }) } -func (s *RethinkSuite) TestRowsStruct(c *test.C) { +func (s *RethinkSuite) TestCursorStruct(c *test.C) { res, err := Expr(map[string]interface{}{ "id": 2, "name": "Object 1", @@ -137,7 +137,7 @@ func (s *RethinkSuite) TestRowsStruct(c *test.C) { }) } -func (s *RethinkSuite) TestRowsStructPseudoTypes(c *test.C) { +func (s *RethinkSuite) TestCursorStructPseudoTypes(c *test.C) { t := time.Now() res, err := Expr(map[string]interface{}{ @@ -154,7 +154,7 @@ func (s *RethinkSuite) TestRowsStructPseudoTypes(c *test.C) { c.Assert(response.B, JsonEquals, []byte("hello")) } -func (s *RethinkSuite) TestRowsAtomString(c *test.C) { +func (s *RethinkSuite) TestCursorAtomString(c *test.C) { res, err := Expr("a").Run(sess) c.Assert(err, test.IsNil) @@ -164,7 +164,7 @@ func (s *RethinkSuite) TestRowsAtomString(c *test.C) { c.Assert(response, test.Equals, "a") } -func (s *RethinkSuite) TestRowsAtomArray(c *test.C) { +func (s *RethinkSuite) TestCursorAtomArray(c *test.C) { res, err := Expr([]interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}).Run(sess) c.Assert(err, test.IsNil) @@ -203,7 +203,7 @@ func (s *RethinkSuite) TestEmptyResults(c *test.C) { c.Assert(res.IsNil(), test.Equals, true) } -func (s *RethinkSuite) TestRowsAll(c *test.C) { +func (s *RethinkSuite) TestCursorAll(c *test.C) { // Ensure table + database exist DbCreate("test").Exec(sess) Db("test").TableDrop("Table3").Exec(sess) From 52a3eaacb43a057aa09791c07102fe9ca5f7f1bb Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Sat, 4 Oct 2014 22:23:58 +0100 Subject: [PATCH 17/18] Zero values when decoding results --- cursor_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ encoding/decoder.go | 6 ++++ encoding/decoder_test.go | 18 ------------ gorethink_test.go | 5 ++++ 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/cursor_test.go b/cursor_test.go index 39ac9073..c7374e14 100644 --- a/cursor_test.go +++ b/cursor_test.go @@ -258,3 +258,64 @@ func (s *RethinkSuite) TestCursorAll(c *test.C) { }, }) } + +func (s *RethinkSuite) TestCursorReuseResult(c *test.C) { + // Test query + query := Expr([]interface{}{ + map[string]interface{}{ + "A": "a", + }, + map[string]interface{}{ + "B": 1, + }, + map[string]interface{}{ + "A": "a", + }, + map[string]interface{}{ + "B": 1, + }, + map[string]interface{}{ + "A": "a", + "B": 1, + }, + }) + res, err := query.Run(sess) + c.Assert(err, test.IsNil) + + var i int + var result SimpleT + for res.Next(&result) { + switch i { + case 0: + c.Assert(result, test.DeepEquals, SimpleT{ + A: "a", + B: 0, + }) + case 1: + c.Assert(result, test.DeepEquals, SimpleT{ + A: "", + B: 1, + }) + case 2: + c.Assert(result, test.DeepEquals, SimpleT{ + A: "a", + B: 0, + }) + case 3: + c.Assert(result, test.DeepEquals, SimpleT{ + A: "", + B: 1, + }) + case 4: + c.Assert(result, test.DeepEquals, SimpleT{ + A: "a", + B: 1, + }) + default: + c.Fatalf("Unexpected number of results") + } + + i++ + } + c.Assert(res.Err(), test.IsNil) +} diff --git a/encoding/decoder.go b/encoding/decoder.go index ef35b0ae..f28a982c 100644 --- a/encoding/decoder.go +++ b/encoding/decoder.go @@ -33,6 +33,7 @@ func Decode(dst interface{}, src interface{}) (err error) { if dv.Kind() != reflect.Ptr || dv.IsNil() { return &InvalidDecodeError{reflect.TypeOf(dst)} } + s := &decodeState{} decode(s, dv, sv) @@ -52,6 +53,11 @@ func (d *decodeState) saveError(err error) { // decodeInterface decodes the source value into the destination value func decode(s *decodeState, dv, sv reflect.Value) { + if dv.IsValid() { + val := indirect(dv, false) + val.Set(reflect.Zero(val.Type())) + } + if dv.IsValid() && sv.IsValid() { // Ensure that the source value has the correct type of parsing if sv.Kind() == reflect.Interface { diff --git a/encoding/decoder_test.go b/encoding/decoder_test.go index 6c2d4538..bd25d005 100644 --- a/encoding/decoder_test.go +++ b/encoding/decoder_test.go @@ -384,21 +384,3 @@ type Foo struct { type Bar struct { Baz int `gorethink:"baz"` } - -func TestDecodeInterfaceValues(t *testing.T) { - input := map[string]interface{}{ - "foobar": map[string]interface{}{ - "baz": 123, - }, - } - want := &Foo{FooBar: &Bar{Baz: 123}} - - out := &Foo{FooBar: &Bar{}} - err := Decode(out, input) - if err != nil { - t.Errorf("got error %v, expected nil", err) - } - if !reflect.DeepEqual(out, want) { - t.Errorf("got %q, want %q", out, want) - } -} diff --git a/gorethink_test.go b/gorethink_test.go index 65820c9a..e1f51a89 100644 --- a/gorethink_test.go +++ b/gorethink_test.go @@ -155,6 +155,11 @@ type T struct { F X } +type SimpleT struct { + A string + B int +} + type X struct { XA int XB string From 6ed3bbf72c4dcef255896c64f3b607b6041a3133 Mon Sep 17 00:00:00 2001 From: Daniel Cannon Date: Mon, 6 Oct 2014 20:52:09 +0100 Subject: [PATCH 18/18] Bumped version number and updated changelog --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- doc.go | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fe893d..287ddfc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v0.5.0 - 6 Oct 2014 + +- Added geospatial terms (`Circle`, `Distance`, `Fill`, `Geojson`, `ToGeojson`, `GetIntersecting`, `GetNearest`, `Includes`, `Intersects`, `Line`, `Point`, `Polygon`, `PolygonSub`) +- Added `UUID` term for generating unique IDs +- Added `AtIndex` term, combines `Nth` and `GetField` +- Added the `Geometry` type, see the types package +- Updated the `BatchConf` field in `RunOpts`, now uses the `BatchOpts` type + +### Internal Changes +- Fixed encoding performance issues, greatly improves writes/second +- Updated `Next` to zero the destination value every time it is called. + ## v0.4.2 - 6 Sept 2014 - Fixed issue causing `Close` to start an infinite loop diff --git a/README.md b/README.md index 37dcda8e..cbafba2e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ GoRethink - RethinkDB Driver for Go [![wercker status](https://app.wercker.com/s [Go](http://golang.org/) driver for [RethinkDB](http://www.rethinkdb.com/) made by [Daniel Cannon](http://github.com/dancannon) and based off of Christopher Hesse's [RethinkGo](https://github.com/christopherhesse/rethinkgo) driver. -Current version: v0.4.1 (RethinkDB v1.14) +Current version: v0.5.0 (RethinkDB v1.15.1) **Version 0.3 introduced some API changes, for more information check the [change log](CHANGELOG.md)** diff --git a/doc.go b/doc.go index dcc6ed0e..5255510a 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ // Go driver for RethinkDB // -// Current version: v0.4.1 (RethinkDB v1.14) +// Current version: v0.5.0 (RethinkDB v1.15.1) // For more in depth information on how to use RethinkDB check out the API docs // at http://rethinkdb.com/api package gorethink