From b464afae017af53b89cdd6d457882e3f83ca714a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Tue, 2 Jul 2024 11:29:47 +0200 Subject: [PATCH] v0.0.479 AccessStruct --- goextVersion.go | 4 +- reflectext/structAccess.go | 185 +++++++++++++++++++++++++ reflectext/structAccess_test.go | 235 ++++++++++++++++++++++++++++++++ 3 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 reflectext/structAccess.go create mode 100644 reflectext/structAccess_test.go diff --git a/goextVersion.go b/goextVersion.go index eb01537..603d1c8 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.478" +const GoextVersion = "0.0.479" -const GoextVersionTimestamp = "2024-07-01T17:23:00+0200" +const GoextVersionTimestamp = "2024-07-02T11:29:47+0200" diff --git a/reflectext/structAccess.go b/reflectext/structAccess.go new file mode 100644 index 0000000..e3c8c89 --- /dev/null +++ b/reflectext/structAccess.go @@ -0,0 +1,185 @@ +package reflectext + +import ( + "errors" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "reflect" + "strconv" + "strings" +) + +var ErrAccessStructInvalidFieldType = errors.New("invalid field type") +var ErrAccessStructFieldInPathWasNil = errors.New("a field in the path was nil") +var ErrAccessStructInvalidArrayIndex = errors.New("invalid array index") +var ErrAccessStructInvalidMapKey = errors.New("invalid map key") +var ErrAccessStructArrayAccess = errors.New("trying to access array") +var ErrAccessStructMapAccess = errors.New("trying to access map") +var ErrAccessStructMissingField = errors.New("missing field") + +type AccessStructOpt struct { + ReturnNilOnMissingFields bool // return nil (instead of error) when a field in the path is missing (aka the supplied path is wrong) + ReturnNilOnNilPtrFields bool // return nil (instead of error) when a field in the path is nil + ReturnNilOnWrongFinalFieldType bool // return nil (instead of error) when the (final) field is not of the requested generic type + ReturnNilOnWrongIntermedFieldType bool // return nil (instead of error) when the intermediate field has an invalid type + ReturnNilOnInvalidArrayIndizes bool // return nil (instead of error) when trying to acces an array with an invalid index (not a number or out of range) + ReturnNilOnMissingMapKeys bool // return nil (instead of error) when trying to access a map with a missing key + UsedTagForKeys *string // Use this tag for key names in the struct (instead of the StructField.Name) + PreventArrayAccess bool // do not access array indizes - throw an error instead + PreventMapAccess bool // do not access maps - throw an error instead +} + +func AccessJSONStruct[TResult any](v any, path string) (TResult, error) { + return AccessStructByStringPath[TResult](v, path, AccessStructOpt{UsedTagForKeys: langext.Ptr("json")}) +} + +func AccessStruct[TResult any](v any, path string) (TResult, error) { + return AccessStructByStringPath[TResult](v, path, AccessStructOpt{}) +} + +func AccessStructByArrayPath[TResult any](v any, path []string, opts ...AccessStructOpt) (TResult, error) { + opt := AccessStructOpt{} + if len(opts) > 0 { + opt = opts[0] + } + + resultVal, err := accessStructByPath(reflect.ValueOf(v), path, opt) + if err != nil { + return *new(TResult), err + } + + if resultValCast, ok := resultVal.(TResult); ok { + return resultValCast, nil + } else if opt.ReturnNilOnWrongFinalFieldType { + return *new(TResult), nil + } else { + return *new(TResult), ErrAccessStructInvalidFieldType + } +} + +func AccessStructByStringPath[TResult any](v any, path string, opts ...AccessStructOpt) (TResult, error) { + opt := AccessStructOpt{} + if len(opts) > 0 { + opt = opts[0] + } + arrpath := strings.Split(path, ".") + + resultVal, err := accessStructByPath(reflect.ValueOf(v), arrpath, opt) + if err != nil { + return *new(TResult), err + } + + if resultValCast, ok := resultVal.(TResult); ok { + return resultValCast, nil + } else if opt.ReturnNilOnWrongFinalFieldType { + return *new(TResult), nil + } else { + return *new(TResult), ErrAccessStructInvalidFieldType + } +} + +func accessStructByPath(val reflect.Value, path []string, opt AccessStructOpt) (any, error) { + if len(path) == 0 { + return val.Interface(), nil + } + + currPath := path[0] + + if val.Kind() == reflect.Ptr { + if val.IsNil() { + if opt.ReturnNilOnNilPtrFields { + return nil, nil + } else { + return nil, ErrAccessStructFieldInPathWasNil + } + } + return accessStructByPath(val.Elem(), path, opt) + } + + if val.Kind() == reflect.Array || val.Kind() == reflect.Slice { + if opt.PreventArrayAccess { + return nil, ErrAccessStructArrayAccess + } + + if val.IsNil() { + if opt.ReturnNilOnNilPtrFields { + return nil, nil + } else { + return nil, ErrAccessStructFieldInPathWasNil + } + } + + arrIdx, err := strconv.ParseInt(currPath, 10, 64) + if err != nil { + if opt.ReturnNilOnInvalidArrayIndizes { + return nil, nil + } else { + return nil, ErrAccessStructInvalidArrayIndex + } + } + if arrIdx < 0 || int(arrIdx) >= val.Len() { + if opt.ReturnNilOnInvalidArrayIndizes { + return nil, nil + } else { + return nil, ErrAccessStructInvalidArrayIndex + } + } + return accessStructByPath(val.Index(int(arrIdx)), path[1:], opt) + } + + if val.Kind() == reflect.Map { + if opt.PreventMapAccess { + return nil, ErrAccessStructMapAccess + } + + if val.IsNil() { + if opt.ReturnNilOnNilPtrFields { + return nil, nil + } else { + return nil, ErrAccessStructFieldInPathWasNil + } + } + + mapval := val.MapIndex(reflect.ValueOf(currPath)) + if !mapval.IsValid() || mapval.IsZero() { + if opt.ReturnNilOnMissingMapKeys { + return nil, nil + } else { + return nil, ErrAccessStructInvalidMapKey + } + } + + return accessStructByPath(mapval, path[1:], opt) + } + + if val.Kind() == reflect.Struct { + if opt.UsedTagForKeys != nil { + for i := 0; i < val.NumField(); i++ { + if val.Type().Field(i).Tag.Get(*opt.UsedTagForKeys) == currPath { + return accessStructByPath(val.Field(i), path[1:], opt) + } + } + if opt.ReturnNilOnMissingFields { + return nil, nil + } else { + return nil, ErrAccessStructMissingField + } + } else { + for i := 0; i < val.NumField(); i++ { + if val.Type().Field(i).Name == currPath { + return accessStructByPath(val.Field(i), path[1:], opt) + } + } + if opt.ReturnNilOnMissingFields { + return nil, nil + } else { + return nil, ErrAccessStructMissingField + } + } + } + + if opt.ReturnNilOnWrongIntermedFieldType { + return nil, nil + } else { + return nil, ErrAccessStructMissingField + } +} diff --git a/reflectext/structAccess_test.go b/reflectext/structAccess_test.go new file mode 100644 index 0000000..895dd63 --- /dev/null +++ b/reflectext/structAccess_test.go @@ -0,0 +1,235 @@ +package reflectext + +import "testing" + +type TestStruct struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func TestAccessStructByArrayPath_HappyPath(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + result, err := AccessStructByArrayPath[string](testStruct, []string{"Name"}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "John" { + t.Errorf("Expected 'John', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_InvalidField(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + _, err := AccessStructByArrayPath[string](testStruct, []string{"Invalid"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +func TestAccessStructByStringPath_HappyPath(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + result, err := AccessStructByStringPath[string](testStruct, "Name") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "John" { + t.Errorf("Expected 'John', got '%s'", result) + } +} + +func TestAccessStructByStringPath_InvalidField(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + _, err := AccessStructByStringPath[string](testStruct, "Invalid") + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +type RecursiveStruct struct { + Name string + Sub *RecursiveStruct + SubSlice []RecursiveStruct +} + +func TestAccessStructByArrayPath_RecursiveStruct(t *testing.T) { + testStruct := RecursiveStruct{Name: "John", Sub: &RecursiveStruct{Name: "Jane"}} + result, err := AccessStructByArrayPath[string](*testStruct.Sub, []string{"Name"}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Jane" { + t.Errorf("Expected 'Jane', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_RecursiveStructSlice(t *testing.T) { + testStruct := RecursiveStruct{Name: "John", SubSlice: []RecursiveStruct{{Name: "Jane"}}} + result, err := AccessStructByArrayPath[string](testStruct.SubSlice[0], []string{"Name"}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Jane" { + t.Errorf("Expected 'Jane', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_WrongType(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + _, err := AccessStructByArrayPath[int](testStruct, []string{"Name"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +func TestAccessStructByArrayPath_InvalidPath(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + _, err := AccessStructByArrayPath[string](testStruct, []string{"Name", "Invalid"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +type NestedStruct struct { + Name string + Sub *TestStruct +} + +func TestAccessStructByStringPath_NestedStruct(t *testing.T) { + testStruct := NestedStruct{Name: "John", Sub: &TestStruct{Name: "Jane", Age: 30}} + result, err := AccessStructByStringPath[string](testStruct, "Sub.Name") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Jane" { + t.Errorf("Expected 'Jane', got '%s'", result) + } +} + +type DeepNestedStruct struct { + Name string + Sub *NestedStruct +} + +func TestAccessStructByStringPath_DeepNestedStruct(t *testing.T) { + testStruct := DeepNestedStruct{Name: "John", Sub: &NestedStruct{Name: "Jane", Sub: &TestStruct{Name: "Doe", Age: 30}}} + result, err := AccessStructByStringPath[string](testStruct, "Sub.Sub.Name") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Doe" { + t.Errorf("Expected 'Doe', got '%s'", result) + } +} + +type MapStruct struct { + Name string + Age int +} + +type TestStructWithMap struct { + MapField map[string]MapStruct +} + +func TestAccessStructByArrayPath_MapField(t *testing.T) { + testStruct := TestStructWithMap{ + MapField: map[string]MapStruct{ + "key": {Name: "John", Age: 30}, + }, + } + result, err := AccessStructByArrayPath[string](testStruct, []string{"MapField", "key", "Name"}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "John" { + t.Errorf("Expected 'John', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_InvalidMapKey(t *testing.T) { + testStruct := TestStructWithMap{ + MapField: map[string]MapStruct{ + "key": {Name: "John", Age: 30}, + }, + } + _, err := AccessStructByArrayPath[string](testStruct, []string{"MapField", "invalid", "Name"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +type ArrayStruct struct { + Name string + Arr []TestStruct +} + +func TestAccessStructByArrayPath_ArrayField(t *testing.T) { + testStruct := ArrayStruct{ + Name: "John", + Arr: []TestStruct{{Name: "Jane", Age: 30}}, + } + result, err := AccessStructByArrayPath[string](testStruct, []string{"Arr", "0", "Name"}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Jane" { + t.Errorf("Expected 'Jane', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_InvalidArrayIndex(t *testing.T) { + testStruct := ArrayStruct{ + Name: "John", + Arr: []TestStruct{{Name: "Jane", Age: 30}}, + } + _, err := AccessStructByArrayPath[string](testStruct, []string{"Arr", "1", "Name"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +type FunctionStruct struct { + Name string + Func func() string +} + +func TestAccessStructByArrayPath_FunctionField(t *testing.T) { + testStruct := FunctionStruct{Name: "John", Func: func() string { return "Hello" }} + _, err := AccessStructByArrayPath[string](testStruct, []string{"Func"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +func TestAccessStructByArrayPath_NonExistentPath(t *testing.T) { + testStruct := TestStruct{Name: "John", Age: 30} + _, err := AccessStructByArrayPath[string](testStruct, []string{"NonExistent"}) + if err == nil { + t.Errorf("Expected error, got nil") + } +} + +type NestedStructWithTag struct { + Name string `json:"name"` + Sub *TestStruct `json:"sub"` +} + +func TestAccessStructByArrayPath_UsedTagForKeys(t *testing.T) { + testStruct := NestedStructWithTag{Name: "John", Sub: &TestStruct{Name: "Jane", Age: 30}} + tag := "json" + result, err := AccessStructByArrayPath[string](testStruct, []string{"sub", "name"}, AccessStructOpt{UsedTagForKeys: &tag}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != "Jane" { + t.Errorf("Expected 'Jane', got '%s'", result) + } +} + +func TestAccessStructByArrayPath_UsedTagForKeysInvalid(t *testing.T) { + testStruct := NestedStructWithTag{Name: "John", Sub: &TestStruct{Name: "Jane", Age: 30}} + tag := "json" + _, err := AccessStructByArrayPath[string](testStruct, []string{"sub", "invalid"}, AccessStructOpt{UsedTagForKeys: &tag}) + if err == nil { + t.Errorf("Expected error, got nil") + } +}