Skip to content

Commit

Permalink
Merge pull request #62 from epiclabs-io/improve-scan
Browse files Browse the repository at this point in the history
Improve struct scanning
  • Loading branch information
asdine authored Mar 3, 2020
2 parents 0d59d07 + d195310 commit 056e27e
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 50 deletions.
30 changes: 19 additions & 11 deletions document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,29 @@ type Document interface {

// NewFromMap creates a document from a map.
// Due to the way maps are designed, iteration order is not guaranteed.
func NewFromMap(m map[string]interface{}) Document {
return mapDocument(m)
func NewFromMap(m interface{}) (Document, error) {
M := reflect.ValueOf(m)
if M.Kind() != reflect.Map || M.Type().Key().Kind() != reflect.String {
return nil, &ErrUnsupportedType{m, "parameter must be a map with a string key"}
}
return mapDocument(M), nil
}

type mapDocument map[string]interface{}
type mapDocument reflect.Value

var _ Document = (*mapDocument)(nil)

func (m mapDocument) Iterate(fn func(f string, v Value) error) error {
for mk, mv := range m {
v, err := NewValue(mv)
M := reflect.Value(m)
it := M.MapRange()

for it.Next() {
v, err := NewValue(it.Value().Interface())
if err != nil {
return err
}

err = fn(mk, v)
err = fn(it.Key().String(), v)
if err != nil {
return err
}
Expand All @@ -66,11 +73,12 @@ func (m mapDocument) Iterate(fn func(f string, v Value) error) error {
}

func (m mapDocument) GetByField(field string) (Value, error) {
v, ok := m[field]
if !ok {
M := reflect.Value(m)
v := M.MapIndex(reflect.ValueOf(field))
if v == (reflect.Value{}) {
return Value{}, ErrFieldNotFound
}
return NewValue(v)
return NewValue(v.Interface())
}

// NewFromStruct creates a document from a struct using reflection.
Expand All @@ -97,7 +105,7 @@ func (s structDocument) Iterate(fn func(f string, v Value) error) error {

for i := 0; i < l; i++ {
sf := tp.Field(i)
if sf.Anonymous || sf.PkgPath != "" {
if sf.PkgPath != "" {
continue
}

Expand Down Expand Up @@ -150,7 +158,7 @@ func (s structDocument) GetByField(field string) (Value, error) {
}
}

if !ok || sf.Anonymous || sf.PkgPath != "" {
if !ok || sf.PkgPath != "" {
return Value{}, ErrFieldNotFound
}

Expand Down
12 changes: 11 additions & 1 deletion document/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ func TestNewFromMap(t *testing.T) {
"nilField": nil,
}

doc := document.NewFromMap(m)
doc, err := document.NewFromMap(m)
require.NoError(t, err)

t.Run("Iterate", func(t *testing.T) {
counter := make(map[string]int)
Expand Down Expand Up @@ -234,6 +235,15 @@ func TestNewFromMap(t *testing.T) {
_, err = doc.GetByField("bar")
require.Equal(t, document.ErrFieldNotFound, err)
})

t.Run("Invalid types", func(t *testing.T) {

// test NewFromMap rejects invalid types
_, err = document.NewFromMap(8)
require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map")
_, err = document.NewFromMap(map[int]float64{2: 4.3})
require.Error(t, err, "Expected document.NewFromMap to return an error if the passed parameter is not a map with a string key type")
})
}

func TestNewFromStruct(t *testing.T) {
Expand Down
54 changes: 30 additions & 24 deletions document/encoding/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ func TestDecodeValueFromDocument(t *testing.T) {
}

func TestEncodeDecode(t *testing.T) {
userMapDoc, err := document.NewFromMap(map[string]interface{}{
"age": 10,
"name": "john",
})
require.NoError(t, err)

addressMapDoc, err := document.NewFromMap(map[string]string{
"city": "Ajaccio",
"country": "France",
})
require.NoError(t, err)

tests := []struct {
name string
d document.Document
Expand All @@ -52,21 +64,15 @@ func TestEncodeDecode(t *testing.T) {
},
{
"Map",
document.NewFromMap(map[string]interface{}{
"age": 10,
"name": "john",
}),
userMapDoc,
`{"age": 10, "name": "john"}`,
},
{
"Nested Document",
document.NewFieldBuffer().
Add("age", document.NewInt64Value(10)).
Add("name", document.NewTextValue("john")).
Add("address", document.NewDocumentValue(document.NewFromMap(map[string]interface{}{
"city": "Ajaccio",
"country": "France",
}))),
Add("address", document.NewDocumentValue(addressMapDoc)),
`{"age": 10, "name": "john", "address": {"city": "Ajaccio", "country": "France"}}`,
},
}
Expand All @@ -84,13 +90,16 @@ func TestEncodeDecode(t *testing.T) {
}

func TestDecodeDocument(t *testing.T) {
mapDoc, err := document.NewFromMap(map[string]string{
"city": "Ajaccio",
"country": "France",
})
require.NoError(t, err)

doc := document.NewFieldBuffer().
Add("age", document.NewInt64Value(10)).
Add("name", document.NewTextValue("john")).
Add("address", document.NewDocumentValue(document.NewFromMap(map[string]interface{}{
"city": "Ajaccio",
"country": "France",
})))
Add("address", document.NewDocumentValue(mapDoc))

data, err := EncodeDocument(doc)
require.NoError(t, err)
Expand All @@ -102,10 +111,7 @@ func TestDecodeDocument(t *testing.T) {
v, err = ec.GetByField("address")
require.NoError(t, err)
var expected, actual bytes.Buffer
err = document.ToJSON(&expected, document.NewFieldBuffer().Add("address", document.NewDocumentValue(document.NewFromMap(map[string]interface{}{
"city": "Ajaccio",
"country": "France",
}))))
err = document.ToJSON(&expected, document.NewFieldBuffer().Add("address", document.NewDocumentValue(mapDoc)))
require.NoError(t, err)
err = document.ToJSON(&actual, document.NewFieldBuffer().Add("address", v))
require.NoError(t, err)
Expand All @@ -118,10 +124,7 @@ func TestDecodeDocument(t *testing.T) {
require.Equal(t, document.NewInt64Value(10), v)
case "address":
var expected, actual bytes.Buffer
err = document.ToJSON(&expected, document.NewFieldBuffer().Add("address", document.NewDocumentValue(document.NewFromMap(map[string]interface{}{
"city": "Ajaccio",
"country": "France",
}))))
err = document.ToJSON(&expected, document.NewFieldBuffer().Add("address", document.NewDocumentValue(mapDoc)))
require.NoError(t, err)
err = document.ToJSON(&actual, document.NewFieldBuffer().Add(f, v))
require.NoError(t, err)
Expand All @@ -137,6 +140,12 @@ func TestDecodeDocument(t *testing.T) {
}

func TestEncodeArray(t *testing.T) {
mapDoc, err := document.NewFromMap(map[string]string{
"city": "Ajaccio",
"country": "France",
})
require.NoError(t, err)

tests := []struct {
name string
a document.Array
Expand All @@ -147,10 +156,7 @@ func TestEncodeArray(t *testing.T) {
document.NewValueBuffer().
Append(document.NewInt64Value(10)).
Append(document.NewTextValue("john")).
Append(document.NewDocumentValue(document.NewFromMap(map[string]interface{}{
"city": "Ajaccio",
"country": "France",
}))).
Append(document.NewDocumentValue(mapDoc)).
Append(document.NewArrayValue(document.NewValueBuffer().Append(document.NewInt64Value(11)))),
`[10, "john", {"city": "Ajaccio", "country": "France"}, [11]]`,
},
Expand Down
19 changes: 7 additions & 12 deletions document/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func Scan(d Document, targets ...interface{}) error {

ref := reflect.ValueOf(target)
if !ref.IsValid() {
return &ErrUnsupportedType{target}
return &ErrUnsupportedType{target, fmt.Sprintf("Parameter %d is not valid", i)}
}

return scanValue(v, ref)
Expand Down Expand Up @@ -85,12 +85,7 @@ func structScan(d Document, ref reflect.Value) error {
return err
}

if f.Type().Kind() == reflect.Ptr {
err = scanValue(v, f)
} else {
err = scanValue(v, f)
}
if err != nil {
if err := scanValue(v, f); err != nil {
return err
}
}
Expand Down Expand Up @@ -182,23 +177,23 @@ func sliceScan(a Array, ref reflect.Value) error {
func MapScan(d Document, t interface{}) error {
ref := reflect.ValueOf(t)
if !ref.IsValid() {
return &ErrUnsupportedType{ref}
return &ErrUnsupportedType{ref, "t must be a valid reference"}
}

if ref.Kind() == reflect.Ptr {
ref = reflect.Indirect(ref)
}

if ref.Kind() != reflect.Map {
return &ErrUnsupportedType{ref}
return &ErrUnsupportedType{ref, "t is not a map"}
}

return mapScan(d, ref)
}

func mapScan(d Document, ref reflect.Value) error {
if ref.Type().Key().Kind() != reflect.String {
return &ErrUnsupportedType{ref}
return &ErrUnsupportedType{ref, "map key must be a string"}
}

if ref.IsNil() {
Expand All @@ -225,7 +220,7 @@ func ScanValue(v Value, t interface{}) error {

func scanValue(v Value, ref reflect.Value) error {
if !ref.IsValid() {
return &ErrUnsupportedType{ref}
return &ErrUnsupportedType{ref, "parameter is not a valid reference"}
}

if ref.Type().Kind() == reflect.Ptr && ref.IsNil() {
Expand Down Expand Up @@ -319,5 +314,5 @@ func scanValue(v Value, ref reflect.Value) error {
return nil
}

return &ErrUnsupportedType{ref}
return &ErrUnsupportedType{ref, "Invalid type"}
}
11 changes: 9 additions & 2 deletions document/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ var (
// this error is used to skip struct or array fields that are not supported.
type ErrUnsupportedType struct {
Value interface{}
Msg string
}

func (e *ErrUnsupportedType) Error() string {
return fmt.Sprintf("unsupported type %T", e.Value)
return fmt.Sprintf("unsupported type %T. %s", e.Value, e.Msg)
}

// ValueType represents a value type supported by the database.
Expand Down Expand Up @@ -164,10 +165,16 @@ func NewValue(x interface{}) (Value, error) {
return NewNullValue(), nil
}
return NewArrayValue(&sliceArray{ref: v}), nil
case reflect.Map:
doc, err := NewFromMap(x)
if err != nil {
return Value{}, err
}
return NewDocumentValue(doc), nil

}

return Value{}, &ErrUnsupportedType{x}
return Value{}, &ErrUnsupportedType{x, ""}
}

// NewBlobValue encodes x and returns a value.
Expand Down

0 comments on commit 056e27e

Please sign in to comment.