Skip to content

Commit

Permalink
basic 'ubjson' struct tag support
Browse files Browse the repository at this point in the history
  • Loading branch information
jmank88 committed Jan 15, 2018
1 parent da2f577 commit f56e91d
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 14 deletions.
16 changes: 13 additions & 3 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,10 +653,10 @@ func objectIntoStruct(structPtr reflect.Value) func(*ObjectDecoder) error {
if err != nil {
return errors.Wrapf(err, "failed to decode key with call #%d", o.count)
}

f := structPtr.Elem().FieldByName(k)
structValue := structPtr.Elem()
f := fieldByName(structValue, k)
if f == zeroValue {
return errors.Errorf("unable to decode entry: no field named %q found", k)
return errors.Errorf("unable to decode entry: no field found named/tagged %q", k)
}
if err := o.Decode(f.Addr().Interface()); err != nil {
return errors.Wrapf(err, "failed to decode value for %q with call #%d", k, o.count)
Expand All @@ -666,6 +666,16 @@ func objectIntoStruct(structPtr reflect.Value) func(*ObjectDecoder) error {
}
}

// fieldByName looks up a field by name. Either the field name, or the overridden
// 'ubjson' struct tag name.
func fieldByName(structValue reflect.Value, k string) reflect.Value {
fs := cachedTypeFields(structValue.Type())
if i, ok := fs.indexByName[k]; ok {
return structValue.Field(i)
}
return reflect.Value{}
}

func objectIntoMap(mapPtr reflect.Value) func(*ObjectDecoder) error {
return func(o *ObjectDecoder) error {
mapValue := mapPtr.Elem()
Expand Down
22 changes: 12 additions & 10 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,16 +549,18 @@ func encodeStruct(structValue reflect.Value) func(*Encoder) error {
if err != nil {
return err
}
for i := 0; i < structValue.NumField(); i++ {
f := structValue.Type().Field(i)
if f.PkgPath == "" {
if err := o.EncodeKey(f.Name); err != nil {
return errors.Wrapf(err, "failed to encode key %q", f.Name)
}
val := structValue.Field(i).Interface()
if err := o.Encode(val); err != nil {
return errors.Wrapf(err, "failed to encode value for key %q", f.Name)
}
fs := cachedTypeFields(structValue.Type())
for _, name := range fs.names {
i, ok := fs.indexByName[name]
if !ok {
panic("invalid cached type info: no index for field " + name)
}
if err := o.EncodeKey(name); err != nil {
return errors.Wrapf(err, "failed to encode key %q", name)
}
val := structValue.Field(i).Interface()
if err := o.Encode(val); err != nil {
return errors.Wrapf(err, "failed to encode value for key %q", name)
}
}

Expand Down
28 changes: 28 additions & 0 deletions example_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ubjson_test

import (
"fmt"

"github.com/jmank88/ubjson"
)

// A CustomValue encodes itself as a fixed length object container.
type TaggedStruct struct {
Field1 string `ubjson:"field1"`
FieldA int `json:"ignored" ubjson:"fieldA"`
}

func Example_taggedStruct() {
v := &TaggedStruct{Field1: "test", FieldA: 42}
if b, err := ubjson.MarshalBlock(v); err != nil {
fmt.Println("error: " + err.Error())
} else {
fmt.Println(string(b))
}

// Output:
// [{]
// [U][6][field1][S][U][4][test]
// [U][6][fieldA][U][42]
// [}]
}
63 changes: 63 additions & 0 deletions fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ubjson

import (
"reflect"
"sync"
"sync/atomic"
)

// Based on 'encoding/json/encode.go'.
var fieldCache struct {
value atomic.Value // map[reflect.Type]fields
mu sync.Mutex // used only by writers
}

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
// Based on 'encoding/json/encode.go'.
func cachedTypeFields(t reflect.Type) fields {
m, _ := fieldCache.value.Load().(map[reflect.Type]fields)
f, ok := m[t]
if ok {
return f
}

// Compute names without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)

fieldCache.mu.Lock()
m, _ = fieldCache.value.Load().(map[reflect.Type]fields)
newM := make(map[reflect.Type]fields, len(m)+1)
for k, v := range m {
newM[k] = v
}
newM[t] = f
fieldCache.value.Store(newM)
fieldCache.mu.Unlock()
return f
}

// Indexes fields by 'ubjson' struct tag if present, otherwise name.
func typeFields(t reflect.Type) fields {
fs := fields{
indexByName: make(map[string]int),
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.PkgPath == "" {
name := f.Name
// Check for 'ubjson' struct tag.
if v, ok := f.Tag.Lookup("ubjson"); ok {
name = v
}
fs.names = append(fs.names, name)
fs.indexByName[name] = i
}
}
return fs
}

type fields struct {
names []string
indexByName map[string]int
}
2 changes: 1 addition & 1 deletion ubjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Most types can be automatically encoded through reflection with the Marshal
// and Unmarshal functions. Encoders and Decoders additionally provide type
// specific methods. Custom encodings can be defined by implementing the Value
// interface.
// interface. 'ubjson' struct tags can be used to override field names.
//
// b, _ := ubjson.MarshalBlock(8)
// // [U][8]
Expand Down
12 changes: 12 additions & 0 deletions ubjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ var cases = map[string]testCase{
'}'},
"[{]\n\t[U][1][A][i][5]\n\t[U][1][B][i][8]\n[}]",
},
"Object-Int8=struct-tagged": {
struct {
A int8 `ubjson:"a"`
a int // Ignored - Exercises field index logic.
B int8 `json:"wrong" ubjson:"b"`
}{5, 0, 8},
[]byte{'{',
'U', 0x01, 'a', 'i', 0x05,
'U', 0x01, 'b', 'i', 0x08,
'}'},
"[{]\n\t[U][1][a][i][5]\n\t[U][1][b][i][8]\n[}]",
},

"Object=complex-struct": {complexStruct, complexStructBinary, complexStructBlock},
"Object=complex-map": {complexMap, complexMapBinary, complexMapBlock},
Expand Down

0 comments on commit f56e91d

Please sign in to comment.