diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec0b30fa7ea2824af6923493653e32595b0907a8 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="MarkdownSettings"> + <enabledExtensions> + <entry key="MermaidLanguageExtension" value="false" /> + <entry key="PlantUMLLanguageExtension" value="true" /> + </enabledExtensions> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..639900d13c6182e452e33a3bd638e70a0146c785 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..921c18fcf853bd017944147bb2073b01d3561cdf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/pathfinder.iml" filepath="$PROJECT_DIR$/.idea/pathfinder.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/pathfinder.iml b/.idea/pathfinder.iml new file mode 100644 index 0000000000000000000000000000000000000000..25ed3f6e7b6e344b6ca91ebcc5d005f35357f9cf --- /dev/null +++ b/.idea/pathfinder.iml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="Go" enabled="true" /> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/get.go b/get.go index c8f6603bfa66a679e8956131ae7aef109404cd0d..90c2b1cc25a621455263d8abd8e48844ecf1fcdd 100644 --- a/get.go +++ b/get.go @@ -9,7 +9,10 @@ import ( "strings" ) -// This function returns the value of a field in a struct, given a path to the field. +// GetValue returns the value of a field in a struct, given a path to the field. +// The path can contain dots to access nested fields. +// The object must be a pointer to a struct, a struct, a map, a slice or an array, +// otherwise an error is returned. func GetValue[D any](obj D, keyWithDots string) (any, error) { keySlice := strings.Split(keyWithDots, ".") v := reflect.ValueOf(obj) diff --git a/go.mod b/go.mod index 047406f2c4ec9cbb01044b91112e198c4f7a6877..f1f0841c1e1bef35c1eea035cb525543cae38066 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index b410979a43764db2bd7f77cc065eb839b7d20181..0a45411e2f82b7c821a164bde55de7be4998bd2a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/issue_2_test.go b/issue_2_test.go index 4be4bec9b40bf9ca2bb3ed49e1dca51f8899d7dd..1e3ef823e19040d00619cb9e2d29a2fc8b449d84 100644 --- a/issue_2_test.go +++ b/issue_2_test.go @@ -5,6 +5,7 @@ package pathfinder import ( "fmt" + "github.com/google/gofuzz" "github.com/stretchr/testify/assert" "testing" ) @@ -22,7 +23,7 @@ type Issue2Struct struct { } func TestGetValueWithArray(t *testing.T) { - invalidValue := Issue2Struct{ + testStructForGet := Issue2Struct{ Issue1Sub1: Issue2SubStruct{ Issue2SubField: Issue2SubSubStruct{ Issue2SubSubField: []string{"test0", "test1", "test2"}, @@ -32,16 +33,76 @@ func TestGetValueWithArray(t *testing.T) { // iterate from 0 to 2 for i := 0; i < 2; i++ { - result, err := GetValue(invalidValue, "Issue1Sub1.Issue2SubField.Issue2SubSubField."+fmt.Sprintf("%d", i)) + result, err := GetValue(testStructForGet, "Issue1Sub1.Issue2SubField.Issue2SubSubField."+fmt.Sprintf("%d", i)) assert.Nil(t, err) assert.Equal(t, result, "test"+fmt.Sprintf("%d", i)) } i := 3 - result, err := GetValue(invalidValue, "Issue1Sub1.Issue2SubField.Issue2SubSubField."+fmt.Sprintf("%d", i)) + result, err := GetValue(testStructForGet, "Issue1Sub1.Issue2SubField.Issue2SubSubField."+fmt.Sprintf("%d", i)) assert.NotNil(t, err) assert.Nil(t, result) } + +func TestGetValueWithArrayFuzz(t *testing.T) { + f := fuzz.New() + + testStructForGet := Issue2Struct{ + Issue1Sub1: Issue2SubStruct{ + Issue2SubField: Issue2SubSubStruct{ + Issue2SubSubField: []string{"test0", "test1", "test2"}, + }, + }, + } + + for i := 0; i < 100; i++ { + var randomIndex int + f.Fuzz(&randomIndex) + randomIndex = randomIndex % len(testStructForGet.Issue1Sub1.Issue2SubField.Issue2SubSubField) + if randomIndex < 0 { + randomIndex = -randomIndex + } + + result, err := GetValue(testStructForGet, "Issue1Sub1.Issue2SubField.Issue2SubSubField."+fmt.Sprintf("%d", randomIndex)) + + if randomIndex < 3 { + assert.Nil(t, err) + assert.Equal(t, result, "test"+fmt.Sprintf("%d", randomIndex)) + } else { + assert.NotNil(t, err) + assert.Nil(t, result) + } + } +} + +func TestSetValueWithArray(t *testing.T) { + testStructForSet := Issue2Struct{ + Issue1Sub1: Issue2SubStruct{ + Issue2SubField: Issue2SubSubStruct{ + Issue2SubSubField: []string{"test0", "test1", "test2"}, + }, + }, + } + + // iterate from 0 to 2 + for i := 0; i < 2; i++ { + + newValue := "test~~" + fmt.Sprintf("%d", i) + k := "Issue1Sub1.Issue2SubField.Issue2SubSubField." + fmt.Sprintf("%d", i) + err := SetValue(&testStructForSet, k, newValue) + + assert.Nil(t, err) + + result, err := GetValue(testStructForSet, k) + assert.Equal(t, result, newValue) + } + + i := 3 + k := "Issue1Sub1.Issue2SubField.Issue2SubSubField." + fmt.Sprintf("%d", i) + err := SetValue(testStructForSet, k, "test3") + assert.NotNil(t, err) + +} diff --git a/set.go b/set.go index f5ca2f43314c861ad15eeb3abcf15d50c3463f31..fa7443a85a4b32aea8cfe26ae3f9e89ec5b03624 100644 --- a/set.go +++ b/set.go @@ -10,7 +10,8 @@ import ( "strings" ) -// This function sets the value of a field in a struct, given a path to the field. +// SetValue sets the value of a field in a struct, given a path to the field. +// The object must be a pointer to a struct, otherwise an error is returned. func SetValue[D any](obj D, keyWithDots string, newValue any) error { keySlice := strings.Split(keyWithDots, ".") @@ -21,7 +22,13 @@ func SetValue[D any](obj D, keyWithDots string, newValue any) error { if v.Kind() == reflect.Invalid { return newInvalidPathError(keyWithDots) } - v = v.Addr() + + if v.CanAddr() { + v = v.Addr() + } else { + return newCannotSetError(keyWithDots) + } + } if v.Kind() != reflect.Ptr { @@ -46,20 +53,37 @@ func SetValue[D any](obj D, keyWithDots string, newValue any) error { } // non-supporter type at the top of the path - if v.Kind() != reflect.Struct { + switch v.Kind() { + case reflect.Struct: + + v = v.FieldByName(keySlice[len(keySlice)-1]) + if !v.IsValid() { + return newInvalidPathError(keyWithDots) + } + + if !v.CanSet() { + return newCannotSetError(keyWithDots) + } + + case reflect.Map: return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) - } + case reflect.Slice: - v = v.FieldByName(keySlice[len(keySlice)-1]) - if !v.IsValid() { - return newInvalidPathError(keyWithDots) - } + // index is a number and get v from slice with index + index, err := strconv.Atoi(keySlice[len(keySlice)-1]) + if err != nil { + return newInvalidPathError(keyWithDots) + } - if !v.CanSet() { - return newCannotSetError(keyWithDots) - } + // index out of range + if index >= v.Len() { + return newInvalidPathError(keyWithDots) + } - switch v.Kind() { + v = v.Index(index) + + case reflect.Array: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) case reflect.Ptr: if newValue == nil { v.Set(reflect.Zero(v.Type())) @@ -67,6 +91,24 @@ func SetValue[D any](obj D, keyWithDots string, newValue any) error { v.Set(reflect.ValueOf(&newValue)) } return nil + case reflect.Interface: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Chan: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Func: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.UnsafePointer: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Uintptr: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Complex64: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Complex128: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + case reflect.Invalid: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) + default: + return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type()) } newValueType := reflect.TypeOf(newValue)