Skip to content

Commit

Permalink
Merge pull request #41 from mindstand/arrays
Browse files Browse the repository at this point in the history
Arrays
  • Loading branch information
Eric Solender authored Oct 7, 2020
2 parents 87580cd + ca40c9e commit bfaa53b
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 105 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Decorators that can be used
- `index` -- marks field to have an index applied to it.
- `unique` -- marks field to have unique constraint.
- `pk` -- marks field as a primary key. Can only have one pk, composite pk's are not supported.
- `properties` -- marks that field is using a map. GoGM only supports properties fields of `map[string]interface{}`
- `properties` -- marks that field is using a map. GoGM only supports properties fields of `map[string]interface{}`, `map[string]<primitive>`, `map[string][]<primitive>` and `[]<primitive>`
- `-` -- marks that field will be ignored by the ogm

#### Not on relationship member variables
Expand Down Expand Up @@ -76,14 +76,16 @@ type VertexA struct {
// provides required node fields
gogm.BaseNode

TestField string `gogm:"name=test_field"`
TestTypeDefString tdString `gogm:"name=test_type_def_string"`
TestTypeDefInt tdInt `gogm:"name=test_type_def_int"`
SingleA *VertexB `gogm:"direction=incoming;relationship=test_rel"`
ManyA []*VertexB `gogm:"direction=incoming;relationship=testm2o"`
MultiA []*VertexB `gogm:"direction=incoming;relationship=multib"`
SingleSpecA *EdgeC `gogm:"direction=outgoing;relationship=special_single"`
MultiSpecA []*EdgeC `gogm:"direction=outgoing;relationship=special_multi"`
TestField string `gogm:"name=test_field"`
TestTypeDefString tdString `gogm:"name=test_type_def_string"`
TestTypeDefInt tdInt `gogm:"name=test_type_def_int"`
MapProperty map[string]string `gogm:"name=map_property;properties"`
SliceProperty []string `gogm:"name=slice_property;properties"`
SingleA *VertexB `gogm:"direction=incoming;relationship=test_rel"`
ManyA []*VertexB `gogm:"direction=incoming;relationship=testm2o"`
MultiA []*VertexB `gogm:"direction=incoming;relationship=multib"`
SingleSpecA *EdgeC `gogm:"direction=outgoing;relationship=special_single"`
MultiSpecA []*EdgeC `gogm:"direction=outgoing;relationship=special_multi"`
}

type VertexB struct {
Expand Down
96 changes: 74 additions & 22 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,25 +553,6 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string]
continue
}

if fieldConfig.Properties {
mapType := reflect.MapOf(reflect.TypeOf(""), emptyInterfaceType)
mapVal := reflect.MakeMap(mapType)

for k, v := range props {
if !strings.Contains(k, fieldConfig.Name) {
//not one of our map fields
continue
}

mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1)

mapVal.SetMapIndex(reflect.ValueOf(mapKey), reflect.ValueOf(v))
}

reflect.Indirect(val).FieldByName(field).Set(mapVal)
continue
}

var raw interface{}
var ok bool

Expand All @@ -585,10 +566,81 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string]

rawVal := reflect.ValueOf(raw)

if raw == nil || rawVal.IsZero() {
continue //its already initialized to 0 value, no need to do anything
indirect := reflect.Indirect(val)
if fieldConfig.Properties && fieldConfig.PropConfig != nil {
if fieldConfig.PropConfig.IsMap {
var sub reflect.Type
if fieldConfig.PropConfig.IsMapSlice {
sub = reflect.SliceOf(fieldConfig.PropConfig.SubType)
} else {
sub = fieldConfig.PropConfig.SubType
}
mapType := reflect.MapOf(reflect.TypeOf(""), sub)
mapVal := reflect.MakeMap(mapType)
for k, v := range props {
if !strings.Contains(k, fieldConfig.Name) {
//not one of our map fields
continue
}

mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1)

if fieldConfig.PropConfig.IsMapSlice {
if v == nil {
// skip if nil
continue
}

sliceVal := reflect.ValueOf(v)

if sliceVal.IsZero() {
// cant do anything with a zero value
continue
}
rawLen := sliceVal.Len()
sl := reflect.MakeSlice(reflect.SliceOf(fieldConfig.PropConfig.SubType), rawLen, sliceVal.Cap())

for i := 0; i < rawLen; i++ {
slVal := sliceVal.Index(i)
if fieldConfig.PropConfig.SubType == slVal.Type() {
sl.Index(i).Set(slVal)
} else {
sl.Index(i).Set(slVal.Elem().Convert(fieldConfig.PropConfig.SubType))
}
}
mapVal.SetMapIndex(reflect.ValueOf(mapKey), sl)
} else {
vVal := reflect.ValueOf(v)
if fieldConfig.PropConfig.SubType == vVal.Type() {
mapVal.SetMapIndex(reflect.ValueOf(mapKey), vVal)
} else {
mapVal.SetMapIndex(reflect.ValueOf(mapKey), vVal.Convert(fieldConfig.PropConfig.SubType))
}
}
}
indirect.FieldByName(field).Set(mapVal)
} else {
if raw == nil || rawVal.IsZero() {
// cant do anything with a zero value
continue
}
rawLen := rawVal.Len()
sl := reflect.MakeSlice(reflect.SliceOf(fieldConfig.PropConfig.SubType), rawLen, rawVal.Cap())

for i := 0; i < rawLen; i++ {
slVal := rawVal.Index(i)
if fieldConfig.PropConfig.SubType == slVal.Type() {
sl.Index(i).Set(slVal)
} else {
sl.Index(i).Set(slVal.Elem().Convert(fieldConfig.PropConfig.SubType))
}
}
indirect.FieldByName(field).Set(sl)
}
} else {
indirect := reflect.Indirect(val)
if raw == nil || rawVal.IsZero() {
continue
}
if indirect.FieldByName(field).Type() == rawVal.Type() {
indirect.FieldByName(field).Set(rawVal)
} else {
Expand Down
59 changes: 37 additions & 22 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,19 @@ type f struct {

type a struct {
BaseNode
TestField string `gogm:"name=test_field"`
TestTypeDefString tdString `gogm:"name=test_type_def_string"`
TestTypeDefInt tdInt `gogm:"name=test_type_def_int"`
SingleA *b `gogm:"direction=incoming;relationship=test_rel"`
ManyA []*b `gogm:"direction=incoming;relationship=testm2o"`
MultiA []*b `gogm:"direction=incoming;relationship=multib"`
SingleSpecA *c `gogm:"direction=outgoing;relationship=special_single"`
MultiSpecA []*c `gogm:"direction=outgoing;relationship=special_multi"`
Created time.Time `gogm:"name=created"`
PropTest0 map[string]interface{} `gogm:"properties;name=props0"`
PropTest1 map[string]string `gogm:"properties;name=props1"`
PropsTest2 []string `gogm:"properties;name=props2"`
PropsTest3 []int `gogm:"properties;name=props3"`
TestField string `gogm:"name=test_field"`
TestTypeDefString tdString `gogm:"name=test_type_def_string"`
TestTypeDefInt tdInt `gogm:"name=test_type_def_int"`
SingleA *b `gogm:"direction=incoming;relationship=test_rel"`
ManyA []*b `gogm:"direction=incoming;relationship=testm2o"`
MultiA []*b `gogm:"direction=incoming;relationship=multib"`
SingleSpecA *c `gogm:"direction=outgoing;relationship=special_single"`
MultiSpecA []*c `gogm:"direction=outgoing;relationship=special_multi"`
Created time.Time `gogm:"name=created"`
}

type b struct {
Expand Down Expand Up @@ -203,9 +207,12 @@ func (c *c) SetEndNode(v interface{}) error {
}

type propsTest struct {
Id int64 `gogm:"name=id"`
UUID string `gogm:"pk;name=uuid"`
Props map[string]interface{} `gogm:"name=props;properties"`
Id int64 `gogm:"name=id"`
UUID string `gogm:"pk;name=uuid"`
PropTest0 map[string]interface{} `gogm:"properties;name=props0"`
PropTest1 map[string]string `gogm:"properties;name=props1"`
PropsTest2 []string `gogm:"properties;name=props2"`
PropsTest3 []int `gogm:"properties;name=props3"`
}

func TestDecode(t *testing.T) {
Expand Down Expand Up @@ -647,10 +654,12 @@ func TestInnerDecode(t *testing.T) {
id: 1,
labels: []string{"propsTest"},
props: map[string]interface{}{
"uuid": var5uuid,
"props.test.test": "test",
"props.test2": "test2",
"props.test3": "test3",
"uuid": var5uuid,
"props0.test.test": "test",
"props0.test2": 1,
"props1.test": "test",
"props2": []string{"test"},
"props3": []int{1, 2},
},
},
},
Expand All @@ -663,19 +672,25 @@ func TestInnerDecode(t *testing.T) {
r := propsTest{
Id: 1,
UUID: var5uuid,
Props: map[string]interface{}{
PropTest0: map[string]interface{}{
"test.test": "test",
"test2": "test2",
"test3": "test3",
"test2": 1,
},
PropTest1: map[string]string{
"test": "test",
},
PropsTest2: []string{"test"},
PropsTest3: []int{1, 2},
}

req.Nil(innerDecode(vars5, &readin5))
req.EqualValues(r.Id, readin5.Id)
req.EqualValues(r.UUID, readin5.UUID)
req.EqualValues(r.Props["test"], readin5.Props["test"])
req.EqualValues(r.Props["test2"], readin5.Props["test2"])
req.EqualValues(r.Props["test3"], readin5.Props["test3"])
req.EqualValues(r.PropTest0["test"], readin5.PropTest0["test"])
req.EqualValues(r.PropTest0["test2"], readin5.PropTest0["test2"])
req.EqualValues(r.PropTest1["test"], readin5.PropTest1["test"])
req.EqualValues(r.PropsTest2, readin5.PropsTest2)
req.EqualValues(r.PropsTest3, readin5.PropsTest3)

//multi single
vars6 := [][]interface{}{
Expand Down
74 changes: 59 additions & 15 deletions decorator.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const (
//specifies is the field is a primary key
primaryKeyField = "pk"

//specifies if the field is map of type `map[string]interface{}`
//specifies if the field is map of type `map[string]interface{} or []<primitive>`
propertiesField = "properties"

//specifies if the field is to be ignored
Expand All @@ -73,6 +73,13 @@ const (
assignmentOperator = "="
)

type propConfig struct {
// IsMap if false assume slice
IsMap bool
IsMapSlice bool
SubType reflect.Type
}

//decorator config defines configuration of GoGM field
type decoratorConfig struct {
// holds reflect type for the field
Expand All @@ -97,6 +104,8 @@ type decoratorConfig struct {
PrimaryKey bool `json:"primary_key"`
// specify if the field holds properties
Properties bool `json:"properties"`

PropConfig *propConfig `json:"prop_config"`
// specifies if the field contains time value
// IsTime bool `json:"is_time"`
// specifies if the field contains a typedef of another type
Expand Down Expand Up @@ -160,7 +169,6 @@ func (d *decoratorConfig) Validate() error {
if d.Ignore {
if d.Relationship != "" || d.Unique || d.Index || d.ManyRelationship || d.UsesEdgeNode ||
d.PrimaryKey || d.Properties || d.Name != d.FieldName {
log.Println(d)
return NewInvalidDecoratorConfigError("ignore tag cannot be combined with any other tag", "")
}

Expand All @@ -174,22 +182,41 @@ func (d *decoratorConfig) Validate() error {

kind := d.Type.Kind()

//check for valid properties
if kind == reflect.Map || d.Properties {
if !d.Properties {
return NewInvalidDecoratorConfigError("properties must be added to gogm config on field with a map", d.Name)
}

if kind != reflect.Map || d.Type != reflect.TypeOf(map[string]interface{}{}) {
return NewInvalidDecoratorConfigError("properties must be a map with signature map[string]interface{}", d.Name)
}

// properties supports map and slices
if (kind == reflect.Map || kind == reflect.Slice) && d.Properties && d.Relationship == "" {
if d.PrimaryKey || d.Relationship != "" || d.Direction != 0 || d.Index || d.Unique {
return NewInvalidDecoratorConfigError("field marked as properties can only have name defined", d.Name)
}

//valid properties
return nil
if kind == reflect.Slice {
sliceType := reflect.SliceOf(d.Type)
sliceKind := sliceType.Elem().Elem().Kind()
if _, err := getPrimitiveType(sliceKind); err != nil && sliceKind != reflect.Interface {
return NewInvalidDecoratorConfigError("property slice not of type <primitive>", d.Name)
}
} else if kind == reflect.Map {
// check if the key is a string
if d.Type.Kind() == reflect.String {
return NewInvalidDecoratorConfigError("property map key not of type string", d.Name)
}

mapType := d.Type.Elem()
mapKind := mapType.Kind()
if mapKind == reflect.Slice {
mapElem := mapType.Elem().Kind()
if _, err := getPrimitiveType(mapElem); err != nil {
return NewInvalidDecoratorConfigError("property map not of type <primitive> or []<primitive>", d.Name)
}
} else if _, err := getPrimitiveType(mapKind); err != nil && mapType.Kind() != reflect.Interface {
return NewInvalidDecoratorConfigError("property map not of type <primitive> or []<primitive> or interface{} or []interface{}", d.Name)
}
} else {
return NewInvalidDecoratorConfigError("property muss be map[string]<primitive> or map[string][]<primitive> or []primitive", d.Name)
}
} else if d.Properties {
return NewInvalidDecoratorConfigError("property must be map[string]<primitive> or map[string][]<primitive> or []primitive", d.Name)
} else if kind == reflect.Map {
return NewInvalidDecoratorConfigError("field with map must be marked as a property", d.Name)
}

//check if type is pointer
Expand All @@ -199,7 +226,7 @@ func (d *decoratorConfig) Validate() error {
}

//check valid relationship
if d.Direction != 0 || d.Relationship != "" || (kind == reflect.Struct && d.Type != timeType) || kind == reflect.Slice {
if d.Direction != 0 || d.Relationship != "" || (kind == reflect.Struct && d.Type != timeType) || (kind == reflect.Slice && !d.Properties) {
if d.Relationship == "" {
return NewInvalidDecoratorConfigError("relationship has to be defined when creating a relationship", d.FieldName)
}
Expand Down Expand Up @@ -350,6 +377,23 @@ func newDecoratorConfig(decorator, name string, varType reflect.Type) (*decorato
toReturn.Ignore = true
continue
case propertiesField:
conf := propConfig{}
conf.IsMapSlice = false
k := varType.Kind()
if k == reflect.Slice {
conf.IsMap = false
conf.SubType = varType.Elem()
} else if k == reflect.Map {
conf.IsMap = true
sub := varType.Elem()
if sub.Kind() == reflect.Slice {
conf.IsMapSlice = true
conf.SubType = sub.Elem()
} else {
conf.SubType = sub
}
}
toReturn.PropConfig = &conf
toReturn.Properties = true
continue
case indexField:
Expand Down
Loading

0 comments on commit bfaa53b

Please sign in to comment.