From e3e6721562148baee8183e27e12465efe937636d Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Tue, 6 Oct 2020 01:52:58 -0400 Subject: [PATCH 1/6] added support for slices --- decoder.go | 37 ++++++++++++++--------- decoder_test.go | 59 +++++++++++++++++++++++-------------- decorator.go | 72 +++++++++++++++++++++++++++++++++++---------- decorator_test.go | 54 ++++++++++++++++++++++++++-------- integration_test.go | 11 ++++++- save.go | 2 ++ util.go | 25 ++++++++-------- util_test.go | 6 ++-- 8 files changed, 186 insertions(+), 80 deletions(-) diff --git a/decoder.go b/decoder.go index 47cc51b..c6580a5 100644 --- a/decoder.go +++ b/decoder.go @@ -554,22 +554,31 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] } 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 + if fieldConfig.PropConfig == nil { + return nil, errors.New("property config is nil for property field") + } + if fieldConfig.PropConfig.IsMap { + for k, v := range props { + if !strings.Contains(k, fieldConfig.Name) { + //not one of our map fields + continue + } + + 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) + + mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1) + mapVal.SetMapIndex(reflect.ValueOf(mapKey), reflect.ValueOf(v)) + reflect.Indirect(val).FieldByName(field).Set(mapVal) } - - mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1) - - mapVal.SetMapIndex(reflect.ValueOf(mapKey), reflect.ValueOf(v)) + continue } - - reflect.Indirect(val).FieldByName(field).Set(mapVal) - continue } var raw interface{} diff --git a/decoder_test.go b/decoder_test.go index fb2b200..42fe798 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -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 { @@ -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) { @@ -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}, }, }, }, @@ -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{}{ diff --git a/decorator.go b/decorator.go index c82a328..f237f2d 100644 --- a/decorator.go +++ b/decorator.go @@ -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 []` propertiesField = "properties" //specifies if the field is to be ignored @@ -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 @@ -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 @@ -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", "") } @@ -174,22 +182,39 @@ 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 ", 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 or []", d.Name) + } + } else if _, err := getPrimitiveType(mapKind); err != nil && mapType.Kind() != reflect.Interface { + return NewInvalidDecoratorConfigError("property map not of type or [] or interface{} or []interface{}", d.Name) + } + } else { + return NewInvalidDecoratorConfigError("property muss be map[string] or map[string][] or []primitive", d.Name) + } + } else if d.Properties { + return NewInvalidDecoratorConfigError("property must be map[string] or map[string][] or []primitive", d.Name) } //check if type is pointer @@ -199,7 +224,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) } @@ -350,6 +375,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: diff --git a/decorator_test.go b/decorator_test.go index a0402e7..75c5fb0 100644 --- a/decorator_test.go +++ b/decorator_test.go @@ -332,14 +332,18 @@ type embedTest struct { type validStruct struct { embedTest - IndexField string `gogm:"index;name=index_field"` - UniqueField int `gogm:"unique;name=unique_field"` - OneToOne *validStruct `gogm:"relationship=one2one;direction=incoming"` - ManyToOne []*a `gogm:"relationship=many2one;direction=outgoing"` - SpecialOne *c `gogm:"relationship=specC;direction=outgoing"` - SpecialMany []*c `gogm:"relationship=manyC;direction=outgoing"` - Props map[string]interface{} `gogm:"properties;name=props"` - IgnoreMe int `gogm:"-"` + IndexField string `gogm:"index;name=index_field"` + UniqueField int `gogm:"unique;name=unique_field"` + OneToOne *validStruct `gogm:"relationship=one2one;direction=incoming"` + ManyToOne []*a `gogm:"relationship=many2one;direction=outgoing"` + SpecialOne *c `gogm:"relationship=specC;direction=outgoing"` + SpecialMany []*c `gogm:"relationship=manyC;direction=outgoing"` + PropsMapInterface map[string]interface{} `gogm:"properties;name=props1"` + PropsMapPrimitive map[string]int `gogm:"properties;name=props2"` + PropsMapSlicePrimitive map[string][]int `gogm:"properties;name=props3"` + PropsSliceInterface []interface{} `gogm:"properties;name=props4"` + PropsPrimitive []int `gogm:"properties;name=props5"` + IgnoreMe int `gogm:"-"` } func (v *validStruct) GetId() int64 { @@ -395,7 +399,9 @@ type invalidStructProperties struct { Id int64 `gogm:"name=id"` UUID string `gogm:"pk;name=uuid"` - Props map[string]string `gogm:"name=props"` //should have properties decorator + Props map[string]*validStruct `gogm:"properties;name=props"` + Props1 map[string][]*validStruct `gogm:"properties;name=props1"` + Props2 []*validStruct `gogm:"properties;name=props2"` } func (i *invalidStructProperties) GetLabels() []string { @@ -507,12 +513,36 @@ func TestGetStructDecoratorConfig(t *testing.T) { ManyRelationship: true, Type: reflect.TypeOf([]*a{}), }, - "Props": { - FieldName: "Props", + "PropsMapInterface": { + FieldName: "PropsMapInterface", Properties: true, - Name: "props", + Name: "props1", Type: reflect.TypeOf(map[string]interface{}{}), }, + "PropsMapPrimitive": { + FieldName: "PropsMapPrimitive", + Properties: true, + Name: "props2", + Type: reflect.TypeOf(map[string]int{}), + }, + "PropsMapSlicePrimitive": { + FieldName: "PropsMapSlicePrimitive", + Properties: true, + Name: "props3", + Type: reflect.TypeOf(map[string][]int{}), + }, + "PropsSliceInterface": { + FieldName: "PropsSliceInterface", + Properties: true, + Name: "props4", + Type: reflect.TypeOf([]interface{}{}), + }, + "PropsPrimitive": { + FieldName: "PropsPrimitive", + Properties: true, + Name: "props5", + Type: reflect.TypeOf([]int{}), + }, "IgnoreMe": { FieldName: "IgnoreMe", Name: "IgnoreMe", diff --git a/integration_test.go b/integration_test.go index be61a3b..0b8d869 100644 --- a/integration_test.go +++ b/integration_test.go @@ -77,7 +77,7 @@ func TestIntegration(t *testing.T) { conf := Config{ Username: "neo4j", - Password: "changeme", + Password: "password", Host: "0.0.0.0", IsCluster: false, Port: 7687, @@ -118,6 +118,15 @@ func testSave(sess *Session, req *require.Assertions) { req.Nil(sess.Begin()) a2 := &a{ TestField: "test", + PropTest0: map[string]interface{}{ + "test.test": "test", + "test2": 1, + }, + PropTest1: map[string]string{ + "test": "test", + }, + PropsTest2: []string{"test", "test"}, + PropsTest3: []int{1, 2}, } b2 := &b{ diff --git a/save.go b/save.go index 8747861..92c75b3 100644 --- a/save.go +++ b/save.go @@ -322,6 +322,8 @@ func createNodes(runFunc neoRunFunc, crNodes map[string]map[string]nodeCreateCon }) if err != nil { return nil, err + } else if res.Err() != nil { + return nil, res.Err() } for res.Next() { diff --git a/util.go b/util.go index 9cf70f3..29bd78b 100644 --- a/util.go +++ b/util.go @@ -130,28 +130,27 @@ func toCypherParamsMap(val reflect.Value, config structDecoratorConfig) (map[str continue } + field := val.FieldByName(conf.FieldName) + if conf.Properties { //check if field is a map - if conf.Type.Kind() == reflect.Map { - //try to cast it - propsMap, ok := val.FieldByName(conf.FieldName).Interface().(map[string]interface{}) - if ok { - //if it works, create the fields - for k, v := range propsMap { - ret[conf.Name+"."+k] = v - } - } else { - return nil, errors.New("unable to convert map to map[string]interface{}") + if conf.Type.Kind() == reflect.Map && field.Kind() == reflect.Map { + for _, e := range field.MapKeys() { + v := field.MapIndex(e) + es := e.Interface().(string) + ret[conf.Name+"."+es] = v.Interface() } + } else if conf.Type.Kind() == reflect.Slice && field.Kind() == reflect.Slice { + ret[conf.Name] = field.Interface() } else { - return nil, errors.New("properties type is not a map") + return nil, fmt.Errorf("properties type is not a map or slice, %T", field.Interface()) } } else { //check if field is type aliased if conf.IsTypeDef { - ret[conf.Name] = val.FieldByName(conf.FieldName).Convert(conf.TypedefActual).Interface() + ret[conf.Name] = field.Convert(conf.TypedefActual).Interface() } else { - ret[conf.Name] = val.FieldByName(conf.FieldName).Interface() + ret[conf.Name] = field.Interface() } } } diff --git a/util_test.go b/util_test.go index f3900eb..706789e 100644 --- a/util_test.go +++ b/util_test.go @@ -114,7 +114,7 @@ func TestToCypherParamsMap(t *testing.T) { p := propsTest{ Id: 1, UUID: "testuuid", - Props: map[string]interface{}{ + PropTest0: map[string]interface{}{ "test": "testvalue", }, } @@ -125,8 +125,8 @@ func TestToCypherParamsMap(t *testing.T) { params, err = toCypherParamsMap(reflect.ValueOf(&p), *config) require.Nil(t, err) require.EqualValues(t, map[string]interface{}{ - "uuid": "testuuid", - "props.test": "testvalue", + "uuid": "testuuid", + "props0.test": "testvalue", }, params) } From cb8e466f9d7f9e0e038469bb9430124f7a2c939d Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Tue, 6 Oct 2020 01:53:18 -0400 Subject: [PATCH 2/6] small fix --- integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.go b/integration_test.go index 0b8d869..50e83cd 100644 --- a/integration_test.go +++ b/integration_test.go @@ -77,7 +77,7 @@ func TestIntegration(t *testing.T) { conf := Config{ Username: "neo4j", - Password: "password", + Password: "changeme", Host: "0.0.0.0", IsCluster: false, Port: 7687, From 56d3b18c0fb8fa8ad5342fcd30de7e18083acce0 Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Tue, 6 Oct 2020 02:11:00 -0400 Subject: [PATCH 3/6] fix build errors --- decorator.go | 2 ++ decorator_test.go | 34 ++++++++++++++++++++++++++++++++-- util_test.go | 7 +++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/decorator.go b/decorator.go index f237f2d..52021a4 100644 --- a/decorator.go +++ b/decorator.go @@ -215,6 +215,8 @@ func (d *decoratorConfig) Validate() error { } } else if d.Properties { return NewInvalidDecoratorConfigError("property must be map[string] or map[string][] 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 diff --git a/decorator_test.go b/decorator_test.go index 75c5fb0..9fdb8a1 100644 --- a/decorator_test.go +++ b/decorator_test.go @@ -296,6 +296,11 @@ func TestNewDecoratorConfig(t *testing.T) { Properties: true, Name: "test", Type: reflect.TypeOf(map[string]interface{}{}), + PropConfig: &propConfig{ + IsMap: true, + IsMapSlice: false, + SubType: emptyInterfaceType, + }, } compare, err = newDecoratorConfig(decProps, "", reflect.TypeOf(map[string]interface{}{})) @@ -341,7 +346,7 @@ type validStruct struct { PropsMapInterface map[string]interface{} `gogm:"properties;name=props1"` PropsMapPrimitive map[string]int `gogm:"properties;name=props2"` PropsMapSlicePrimitive map[string][]int `gogm:"properties;name=props3"` - PropsSliceInterface []interface{} `gogm:"properties;name=props4"` + PropsSliceInterface []string `gogm:"properties;name=props4"` PropsPrimitive []int `gogm:"properties;name=props5"` IgnoreMe int `gogm:"-"` } @@ -518,30 +523,55 @@ func TestGetStructDecoratorConfig(t *testing.T) { Properties: true, Name: "props1", Type: reflect.TypeOf(map[string]interface{}{}), + PropConfig: &propConfig{ + IsMap: true, + IsMapSlice: false, + SubType: emptyInterfaceType, + }, }, "PropsMapPrimitive": { FieldName: "PropsMapPrimitive", Properties: true, Name: "props2", Type: reflect.TypeOf(map[string]int{}), + PropConfig: &propConfig{ + IsMap: true, + IsMapSlice: false, + SubType: reflect.TypeOf(int(0)), + }, }, "PropsMapSlicePrimitive": { FieldName: "PropsMapSlicePrimitive", Properties: true, Name: "props3", Type: reflect.TypeOf(map[string][]int{}), + PropConfig: &propConfig{ + IsMap: true, + IsMapSlice: true, + SubType: reflect.TypeOf(int(0)), + }, }, "PropsSliceInterface": { FieldName: "PropsSliceInterface", Properties: true, Name: "props4", - Type: reflect.TypeOf([]interface{}{}), + Type: reflect.TypeOf([]string{}), + PropConfig: &propConfig{ + IsMap: false, + IsMapSlice: false, + SubType: reflect.TypeOf(""), + }, }, "PropsPrimitive": { FieldName: "PropsPrimitive", Properties: true, Name: "props5", Type: reflect.TypeOf([]int{}), + PropConfig: &propConfig{ + IsMap: false, + IsMapSlice: false, + SubType: reflect.TypeOf(int(0)), + }, }, "IgnoreMe": { FieldName: "IgnoreMe", diff --git a/util_test.go b/util_test.go index 706789e..a9a2939 100644 --- a/util_test.go +++ b/util_test.go @@ -108,6 +108,8 @@ func TestToCypherParamsMap(t *testing.T) { "test_type_def_int": 0, "test_type_def_string": "", "test_field": "testvalue", + "props2": []string(nil), + "props3": []int(nil), "created": time.Time{}, }, params) @@ -117,6 +119,9 @@ func TestToCypherParamsMap(t *testing.T) { PropTest0: map[string]interface{}{ "test": "testvalue", }, + PropTest1: nil, + PropsTest2: nil, + PropsTest3: nil, } config, err = getStructDecoratorConfig(&p, mappedRelations) @@ -127,6 +132,8 @@ func TestToCypherParamsMap(t *testing.T) { require.EqualValues(t, map[string]interface{}{ "uuid": "testuuid", "props0.test": "testvalue", + "props2": []string(nil), + "props3": []int(nil), }, params) } From ea164b05b40e11dc3a92dadaf8fa774d6dfeef2a Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Wed, 7 Oct 2020 02:34:31 -0400 Subject: [PATCH 4/6] update test for all property scenarios and updated documentation --- README.md | 20 ++++++------ decoder.go | 77 ++++++++++++++++++++++++++++----------------- examples/example.go | 18 ++++++----- integration_test.go | 51 ++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 23d9edb..1aeb279 100644 --- a/README.md +++ b/README.md @@ -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]`, `map[string][]` and `[]` - `-` -- marks that field will be ignored by the ogm #### Not on relationship member variables @@ -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 { diff --git a/decoder.go b/decoder.go index c6580a5..efd4776 100644 --- a/decoder.go +++ b/decoder.go @@ -553,51 +553,70 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] continue } - if fieldConfig.Properties { - if fieldConfig.PropConfig == nil { - return nil, errors.New("property config is nil for property field") + var raw interface{} + var ok bool + + raw, ok = props[fieldConfig.Name] + if !ok { + if fieldConfig.IsTypeDef { + log.Debugf("skipping field %s since it is typedeffed and not defined", fieldConfig.Name) + continue } + } + + rawVal := reflect.ValueOf(raw) + + 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 } - var sub reflect.Type + mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1) + if fieldConfig.PropConfig.IsMapSlice { - sub = reflect.SliceOf(fieldConfig.PropConfig.SubType) + sliceVal := reflect.ValueOf(v) + rawLen := sliceVal.Len() + sl := reflect.MakeSlice(reflect.SliceOf(fieldConfig.PropConfig.SubType), rawLen, sliceVal.Cap()) + + for i := 0; i < rawLen; i++ { + sl.Index(i).Set(sliceVal.Index(i).Elem().Convert(fieldConfig.PropConfig.SubType)) + } + mapVal.SetMapIndex(reflect.ValueOf(mapKey), sl) } else { - sub = fieldConfig.PropConfig.SubType + 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)) + } } - mapType := reflect.MapOf(reflect.TypeOf(""), sub) - mapVal := reflect.MakeMap(mapType) + } + indirect.FieldByName(field).Set(mapVal) + } else { + rawLen := rawVal.Len() + sl := reflect.MakeSlice(reflect.SliceOf(fieldConfig.PropConfig.SubType), rawLen, rawVal.Cap()) - mapKey := strings.Replace(k, fieldConfig.Name+".", "", 1) - mapVal.SetMapIndex(reflect.ValueOf(mapKey), reflect.ValueOf(v)) - reflect.Indirect(val).FieldByName(field).Set(mapVal) + for i := 0; i < rawLen; i++ { + sl.Index(i).Set(rawVal.Index(i).Elem().Convert(fieldConfig.PropConfig.SubType)) } - continue + indirect.FieldByName(field).Set(sl) } - } - - var raw interface{} - var ok bool - - raw, ok = props[fieldConfig.Name] - if !ok { - if fieldConfig.IsTypeDef { - log.Debugf("skipping field %s since it is typedeffed and not defined", fieldConfig.Name) + } else { + if raw == nil || rawVal.IsZero() { continue } - } - - rawVal := reflect.ValueOf(raw) - - if raw == nil || rawVal.IsZero() { - continue //its already initialized to 0 value, no need to do anything - } else { - indirect := reflect.Indirect(val) if indirect.FieldByName(field).Type() == rawVal.Type() { indirect.FieldByName(field).Set(rawVal) } else { diff --git a/examples/example.go b/examples/example.go index a4cb6aa..c68d141 100644 --- a/examples/example.go +++ b/examples/example.go @@ -15,14 +15,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 { diff --git a/integration_test.go b/integration_test.go index 50e83cd..40163f8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -68,6 +68,18 @@ func TestRawQuery(t *testing.T) { req.NotEmpty(raw) } +type propTest struct { + BaseNode + + MapInterface map[string]interface{} `gogm:"name=prop1;properties"` + MapPrim map[string]string `gogm:"name=prop2;properties"` + MapTdPrim map[string]tdString `gogm:"name=prop3;properties"` + MapSlicePrim map[string][]string `gogm:"name=prop4;properties"` + MapSliceTdPrim map[string][]tdString `gogm:"name=prop5;properties"` + SlicePrim []string `gogm:"name=prop6;properties"` + SliceTdPrim []tdString `gogm:"name=prop7;properties"` +} + func TestIntegration(t *testing.T) { if testing.Short() { t.Skip() @@ -77,7 +89,7 @@ func TestIntegration(t *testing.T) { conf := Config{ Username: "neo4j", - Password: "changeme", + Password: "password", Host: "0.0.0.0", IsCluster: false, Port: 7687, @@ -85,7 +97,7 @@ func TestIntegration(t *testing.T) { IndexStrategy: IGNORE_INDEX, } - req.Nil(Init(&conf, &a{}, &b{}, &c{})) + req.Nil(Init(&conf, &a{}, &b{}, &c{}, &propTest{})) log.Println("opening session") @@ -197,4 +209,39 @@ func testSave(sess *Session, req *require.Assertions) { req.Nil(sess.Begin()) req.Nil(sess.SaveDepth(singleSave, 1)) req.Nil(sess.Commit()) + + // property test + prop1 := propTest{ + BaseNode: BaseNode{}, + MapInterface: map[string]interface{}{ + "test": int64(1), + }, + MapPrim: map[string]string{ + "test": "test1", + }, + MapTdPrim: map[string]tdString{ + "test": "test2", + }, + MapSlicePrim: map[string][]string{ + "test": {"test1", "test2"}, + }, + MapSliceTdPrim: map[string][]tdString{ + "test": {"test1", "test2"}, + }, + SlicePrim: []string{"test2"}, + SliceTdPrim: []tdString{"test3"}, + } + + req.Nil(sess.SaveDepth(&prop1, 0)) + + var prop2 propTest + req.Nil(sess.Load(&prop2, prop1.UUID)) + + req.EqualValues(prop1.MapInterface, prop2.MapInterface) + req.EqualValues(prop1.MapPrim, prop2.MapPrim) + req.EqualValues(prop1.MapTdPrim, prop2.MapTdPrim) + req.EqualValues(prop1.MapSlicePrim, prop2.MapSlicePrim) + req.EqualValues(prop1.MapSliceTdPrim, prop2.MapSliceTdPrim) + req.EqualValues(prop1.SlicePrim, prop2.SlicePrim) + req.EqualValues(prop1.SliceTdPrim, prop2.SliceTdPrim) } From ce8e0743b3641aa07d76c2828f73ec298b048c49 Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Wed, 7 Oct 2020 02:45:48 -0400 Subject: [PATCH 5/6] fix broken test --- decoder.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/decoder.go b/decoder.go index efd4776..e705de6 100644 --- a/decoder.go +++ b/decoder.go @@ -586,12 +586,27 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] 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++ { - sl.Index(i).Set(sliceVal.Index(i).Elem().Convert(fieldConfig.PropConfig.SubType)) + 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 { @@ -605,11 +620,20 @@ func convertToValue(graphId int64, conf structDecoratorConfig, props map[string] } 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++ { - sl.Index(i).Set(rawVal.Index(i).Elem().Convert(fieldConfig.PropConfig.SubType)) + 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) } From ca40c9ed5d14a3233ca656964e20f710439d7ff9 Mon Sep 17 00:00:00 2001 From: Eric Solender Date: Wed, 7 Oct 2020 02:48:12 -0400 Subject: [PATCH 6/6] change auth --- integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.go b/integration_test.go index 40163f8..6b6f2e8 100644 --- a/integration_test.go +++ b/integration_test.go @@ -89,7 +89,7 @@ func TestIntegration(t *testing.T) { conf := Config{ Username: "neo4j", - Password: "password", + Password: "changeme", Host: "0.0.0.0", IsCluster: false, Port: 7687,