From 4a41f7acea9ac0670f0107dd63209b8d23b675a5 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Tue, 12 Sep 2023 18:55:31 +0200
Subject: [PATCH] feat: suuport slices for set #4

---
 .idea/markdown.xml   |  9 ++++++
 .idea/misc.xml       |  6 ++++
 .idea/modules.xml    |  8 ++++++
 .idea/pathfinder.iml | 10 +++++++
 .idea/vcs.xml        |  6 ++++
 get.go               |  5 +++-
 go.mod               |  1 +
 go.sum               |  2 ++
 issue_2_test.go      | 67 ++++++++++++++++++++++++++++++++++++++++++--
 set.go               | 66 +++++++++++++++++++++++++++++++++++--------
 10 files changed, 164 insertions(+), 16 deletions(-)
 create mode 100644 .idea/markdown.xml
 create mode 100644 .idea/misc.xml
 create mode 100644 .idea/modules.xml
 create mode 100644 .idea/pathfinder.iml
 create mode 100644 .idea/vcs.xml

diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..ec0b30f
--- /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 0000000..639900d
--- /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 0000000..921c18f
--- /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 0000000..25ed3f6
--- /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 0000000..35eb1dd
--- /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 c8f6603..90c2b1c 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 047406f..f1f0841 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 b410979..0a45411 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 4be4bec..1e3ef82 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 f5ca2f4..fa7443a 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)
-- 
GitLab