From d2611650062d6d7db0e154b3d53a3c7bd852ae48 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Thu, 28 Mar 2024 23:40:03 +0400 Subject: [PATCH 01/38] feat(callbacks): draft of the `setter.SetByCallback` --- setter/examples_test.go | 31 +++++++++++++++++++++++++++++++ setter/setter.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/setter/examples_test.go b/setter/examples_test.go index d697199..2df2416 100644 --- a/setter/examples_test.go +++ b/setter/examples_test.go @@ -22,6 +22,7 @@ package setter_test import ( "fmt" + "reflect" "github.com/gontainer/reflectpro/setter" ) @@ -104,3 +105,33 @@ func ExampleSet_unaddressableValue() { // Output: // {Jane Doe} } + +func ExampleSetByCallback() { + var person struct { + Name string `custom:"name"` + Age int `custom:"age"` + } + + config := map[string]any{ + "name": "Jane", + "age": 25, + } + + _ = setter.SetByCallback( + &person, + func(field reflect.StructField) (_ interface{}, ok bool) { + // check whether the given tag exists + tag, ok := field.Tag.Lookup("custom") + if !ok { + return nil, false + } + + // check whether we have the desired value in the config + if val, ok := config[tag]; ok { + return val, ok + } + + return nil, false + }, + ) +} diff --git a/setter/setter.go b/setter/setter.go index 6d9c0b1..be10e53 100644 --- a/setter/setter.go +++ b/setter/setter.go @@ -21,7 +21,9 @@ package setter import ( - "github.com/gontainer/reflectpro/internal/reflect" + "reflect" + + intReflect "github.com/gontainer/reflectpro/internal/reflect" ) /* @@ -37,5 +39,33 @@ Unexported fields are supported. fmt.Println(p) // {Jane} */ func Set(strct any, field string, val any, convert bool) error { - return reflect.Set(strct, field, val, convert) + return intReflect.Set(strct, field, val, convert) +} + +type CallbackOption func(*callbackConfig) + +type callbackConfig struct { + convertType bool + convertToPointer bool +} + +func ConvertType(cfg *callbackConfig) { + cfg.convertType = true +} + +func ConvertToPointer(cfg *callbackConfig) { + cfg.convertToPointer = true + +} + +// SetByCallback invokes callback on the all fields of the given struct. +// If you want to modify the value of the given field, the callback must return that value and `ok == true`. +func SetByCallback(strct any, callback func(field reflect.StructField) (_ any, ok bool), opts ...CallbackOption) error { + var cfg callbackConfig + + for _, o := range opts { + o(&cfg) + } + + return intReflect.SetByCallback(strct, callback, cfg.convertType, cfg.convertToPointer) } From d12b651438b41175e653d650b0a7349a17d04cfb Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 22:32:33 +0400 Subject: [PATCH 02/38] feat(fields): draft of the `fields.Iterate` --- fields/any.go | 23 +++ fields/any_test.go | 23 +++ fields/examples_test.go | 97 ++++++++++ fields/iterate.go | 178 ++++++++++++++++++ fields/iterate_test.go | 364 ++++++++++++++++++++++++++++++++++++ go.mod | 1 + internal/reflect/get_set.go | 127 +++++++++++++ setter/setter.go | 34 +--- 8 files changed, 815 insertions(+), 32 deletions(-) create mode 100644 fields/any.go create mode 100644 fields/any_test.go create mode 100644 fields/examples_test.go create mode 100644 fields/iterate.go create mode 100644 fields/iterate_test.go diff --git a/fields/any.go b/fields/any.go new file mode 100644 index 0000000..ababa45 --- /dev/null +++ b/fields/any.go @@ -0,0 +1,23 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package fields + +type any = interface{} //nolint diff --git a/fields/any_test.go b/fields/any_test.go new file mode 100644 index 0000000..4b93feb --- /dev/null +++ b/fields/any_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package fields_test + +type any = interface{} //nolint diff --git a/fields/examples_test.go b/fields/examples_test.go new file mode 100644 index 0000000..d92c6c7 --- /dev/null +++ b/fields/examples_test.go @@ -0,0 +1,97 @@ +package fields_test + +import ( + "fmt" + "reflect" + + "github.com/davecgh/go-spew/spew" + "github.com/gontainer/reflectpro/fields" +) + +type Exercise struct { + Name string +} + +type TrainingPlanMeta struct { + Name string +} + +type TrainingPlan struct { + TrainingPlanMeta + + Monday Exercise + Tuesday Exercise +} + +func comparePaths(reflectPath []reflect.StructField, names ...string) bool { + if len(reflectPath) != len(names) { + return false + } + + for i := 0; i < len(reflectPath); i++ { + if reflectPath[i].Name != names[i] { + return false + } + } + + return true +} + +func ExampleSet() { + p := TrainingPlan{} + + _ = fields.Iterate( + &p, + fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { + switch { + case comparePaths(path, "TrainingPlanMeta", "Name"): + return "My training plan", true + case comparePaths(path, "Monday", "Name"): + return "pushups", true + case comparePaths(path, "Tuesday", "name"): + return "pullups", true + } + + return nil, false + }), + fields.Recursive(), + ) + + spew.Dump(p) + + // Output: + // (fields_test.TrainingPlan) { + // TrainingPlanMeta: (fields_test.TrainingPlanMeta) { + // Name: (string) (len=16) "My training plan" + // }, + // Monday: (fields_test.Exercise) { + // Name: (string) (len=7) "pushups" + // }, + // Tuesday: (fields_test.Exercise) { + // Name: (string) "" + // } + // } +} + +type Phone struct { + os string +} + +func ExampleSetUnexported() { + p := Phone{} + _ = fields.Iterate( + &p, + fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { + if path[len(path)-1].Name == "os" { + return "Android", true + } + + return nil, false + }), + ) + + fmt.Println(p.os) + + // Output: + // Android +} diff --git a/fields/iterate.go b/fields/iterate.go new file mode 100644 index 0000000..fd3d71e --- /dev/null +++ b/fields/iterate.go @@ -0,0 +1,178 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package fields + +import ( + "fmt" + "reflect" + + intReflect "github.com/gontainer/reflectpro/internal/reflect" +) + +type config struct { + setter func(path []reflect.StructField, value any) (_ any, ok bool) + getter func(path []reflect.StructField, value any) + prefillNilStructs bool + convertTypes bool + convertToPtr bool + recursive bool +} + +func newConfig(opts ...Option) *config { + c := &config{ + setter: nil, + getter: nil, + prefillNilStructs: false, + convertTypes: false, + convertToPtr: false, + recursive: false, + } + for _, o := range opts { + o(c) + } + return c +} + +type Option func(*config) + +func PrefillNilStructs(v bool) Option { + return func(c *config) { + c.prefillNilStructs = v + } +} + +func Setter(fn func(path []reflect.StructField, value any) (_ any, ok bool)) Option { + return func(c *config) { + c.setter = fn + } +} + +func Getter(fn func(path []reflect.StructField, value any)) Option { + return func(c *config) { + c.getter = fn + } +} + +func ConvertTypes() Option { + return func(c *config) { + c.convertTypes = true + } +} + +func ConvertToPointers() Option { + return func(c *config) { + c.convertToPtr = true + } +} + +func Recursive() Option { + return func(c *config) { + c.recursive = true + } +} + +func Iterate(strct any, opts ...Option) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("fields.Iterate: %w", err) + } + }() + + return iterate(strct, newConfig(opts...), nil) +} + +func iterate(strct any, cfg *config, path []reflect.StructField) error { + var fn intReflect.FieldCallback + + var stopErr error + + fn = func(f reflect.StructField, value interface{}) (_ interface{}, ok bool) { + if stopErr != nil { + return nil, false + } + + // call getter + if cfg.getter != nil { + cfg.getter(append(path, f), value) + } + + setterHasBeenTriggered := false + + // call setter + if cfg.setter != nil { + newVal, ok := cfg.setter(append(path, f), value) + if ok { + value, setterHasBeenTriggered = newVal, true + } + } + + // set poiner to a zero-value + if !setterHasBeenTriggered && + cfg.prefillNilStructs && + value == nil && + f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { + value = reflect.New(f.Type.Elem()) + } + + if cfg.recursive { + if f.Type.Kind() == reflect.Struct || // value is a struct + (f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && value != nil) { // value is a pointer to a non-nil struct + + original := value + + newPath := append(path, f) // TODO maybe copy instead of append + if err := iterate(&value, cfg, newPath); err != nil { + // TODO decorate it + stopErr = err + + return nil, false + } + + if !reflect.DeepEqual(original, value) { + setterHasBeenTriggered = true + } + } + } + + if setterHasBeenTriggered { + return value, true + } + + return nil, false + } + + err := intReflect.IterateFields( + strct, + fn, + cfg.convertTypes, + cfg.convertToPtr, + ) + + if err != nil { + return err + } + + if stopErr != nil { + return stopErr + } + + return nil +} diff --git a/fields/iterate_test.go b/fields/iterate_test.go new file mode 100644 index 0000000..7c83a7e --- /dev/null +++ b/fields/iterate_test.go @@ -0,0 +1,364 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package fields_test + +import ( + "bytes" + "fmt" + "reflect" + "testing" + "unsafe" + + "github.com/gontainer/reflectpro/fields" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type CustomString string + +type Person struct { + Name string +} + +type Employee struct { + Person + Role string +} + +type TeamMeta struct { + Name string +} + +type Team struct { + Lead Employee + TeamMeta +} + +type C struct { + D string +} + +type B struct { + C C +} + +type A struct { + B B +} + +type XX struct { + _ int + _ string +} + +type YY struct { + *XX +} + +func pathEquals(p []reflect.StructField, s ...string) bool { + if len(p) != len(s) { + return false + } + + for i, f := range p { + if f.Name != s[i] { + return false + } + } + + return true +} + +func setValueByFieldIndex(ptrStruct any, fieldIndex int, value any) { + f := reflect.ValueOf(ptrStruct).Elem().Field(fieldIndex) + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + f.Set(reflect.ValueOf(value)) +} + +func newXXWithBlankValues(t *testing.T, first int, second string) *XX { + x := XX{} + setValueByFieldIndex(&x, 0, first) + setValueByFieldIndex(&x, 1, second) + + buff := bytes.NewBuffer(nil) + _, err := fmt.Fprint(buff, x) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("{%d %s}", first, second), buff.String()) + + return &x +} + +func TestIterate(t *testing.T) { + t.Parallel() + + t.Run("Setter", func(t *testing.T) { + scenarios := []struct { + name string + options []fields.Option + input any + output any + error string + }{ + { + name: "Person OK", + options: []fields.Option{ + fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + return "Jane", true + }), + }, + input: Person{}, + output: Person{ + Name: "Jane", + }, + error: "", + }, + { + name: "Person OK (convert types)", + options: []fields.Option{ + fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + return CustomString("Jane"), true + }), + fields.ConvertTypes(), + }, + input: Person{}, + output: Person{ + Name: "Jane", + }, + error: "", + }, + { + name: "Person error (convert types)", + options: []fields.Option{ + fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + return CustomString("Jane"), true + }), + }, + input: Person{}, + output: Person{ + Name: "Jane", + }, + error: "fields.Iterate: IterateFields: *interface {}: IterateFields: fields_test.Person: field 0 \"Name\": value of type fields_test.CustomString is not assignable to type string", + }, + { + name: "A.B.C.D OK", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + if pathEquals(path, "B", "C", "D") { + return "Hello", true + } + + return nil, false + }), + fields.Recursive(), + }, + input: A{}, + output: A{ + B: B{ + C: C{ + D: "Hello", + }, + }, + }, + error: "", + }, + { + name: "A.B.C.D error (convert types)", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + if pathEquals(path, "B", "C", "D") { + return 5, true + } + + return nil, false + }), + fields.Recursive(), + }, + input: A{}, + output: A{}, + error: `fields.Iterate: IterateFields: *interface {}: IterateFields: fields_test.C: field 0 "D": value of type int is not assignable to type string`, + }, + { + name: "Employee (embedded)", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ any, ok bool) { + switch { + case pathEquals(path, "Person", "Name"): + return "Jane", true + case pathEquals(path, "Role"): + return "Lead", true + } + + return nil, false + }), + fields.Recursive(), + }, + input: Employee{}, + output: Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, + error: "", + }, + { + name: "Team #1", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + switch { + case pathEquals(path, "Lead", "Person", "Name"): + return "Jane", true + case pathEquals(path, "Lead", "Role"): + return "Lead", true + case pathEquals(path, "TeamMeta", "Name"): + return "Hawkeye", true + } + + return nil, false + }), + fields.Recursive(), + }, + input: Team{}, + output: Team{ + Lead: Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, + TeamMeta: TeamMeta{ + Name: "Hawkeye", + }, + }, + error: "", + }, + { + name: "Team #2", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + switch { + case pathEquals(path, "Lead", "Role"): + return "Lead", true + case pathEquals(path, "Lead"): + return Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, true + case pathEquals(path, "TeamMeta", "Name"): + return "Hawkeye", true + } + + return nil, false + }), + fields.Recursive(), + }, + input: Team{}, + output: Team{ + Lead: Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, + TeamMeta: TeamMeta{ + Name: "Hawkeye", + }, + }, + error: "", + }, + { + name: "YY", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + if pathEquals(path, "XX") { + return &XX{}, true + } + + if pathEquals(path, "XX", "_") { + switch path[len(path)-1].Type.Kind() { + case reflect.Int: + return 5, true + case reflect.String: + return "five", true + } + } + + return nil, false + }), + fields.Recursive(), + }, + input: YY{}, + output: YY{ + XX: newXXWithBlankValues(t, 5, "five"), + }, + }, + { + name: "YY", + options: []fields.Option{ + fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + if pathEquals(path, "XX") { + return &XX{}, true + } + + if pathEquals(path, "XX", "_") { + switch path[len(path)-1].Type.Kind() { + case reflect.Int: + return 7, true + case reflect.String: + return "seven", true + } + } + + return nil, false + }), + fields.Recursive(), + }, + input: YY{}, + output: YY{ + XX: newXXWithBlankValues(t, 7, "seven"), + }, + }, + } + + for _, s := range scenarios { + s := s + + t.Run(s.name, func(t *testing.T) { + t.Parallel() + + input := s.input + err := fields.Iterate(&input, s.options...) + + if s.error != "" { + require.EqualError(t, err, s.error) + + return + } + + require.NoError(t, err) + + assert.Equal(t, s.output, input) + }) + } + }) +} diff --git a/go.mod b/go.mod index 4aaa2f7..ce2dd88 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gontainer/reflectpro go 1.14 require ( + github.com/davecgh/go-spew v1.1.1 github.com/gontainer/grouperror v1.0.1 github.com/stretchr/testify v1.8.2 ) diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index ee5c9a8..ba585ac 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -112,6 +112,133 @@ func Get(strct any, field string) (_ any, err error) { //nolint:cyclop,ireturn return f.Interface(), nil } +type FieldCallback = func(_ reflect.StructField, value any) (_ any, ok bool) + +func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr bool) (err error) { + strType := "" + + defer func() { + if err != nil { + if strType != "" { + err = fmt.Errorf("%s: %w", strType, err) + } + err = fmt.Errorf("IterateFields: %w", err) + } + }() + + reflectVal := reflect.ValueOf(strct) + if !reflectVal.IsValid() { + return fmt.Errorf("expected struct, %T given", strct) + } + + chain, err := ValueToKindChain(reflectVal) + if err != nil { + return err + } + + // TODO + //if chain[len(chain)-1] != reflect.Struct { + // return fmt.Errorf("expected struct, %T given", strct) + //} + + // see [Set] + for { + switch { + case chain.Prefixed(reflect.Ptr, reflect.Ptr): + reflectVal = reflectVal.Elem() + chain = chain[1:] + + continue + case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): + reflectVal = reflectVal.Elem().Elem() + chain = chain[2:] + + continue + } + + break + } + + valueFromField := func(strct reflect.Value, i int) any { + f := strct.Field(i) + + if !f.CanSet() { // handle unexported fields + if !f.CanAddr() { + tmpReflectVal := reflect.New(strct.Type()).Elem() + tmpReflectVal.Set(strct) + f = tmpReflectVal.Field(i) + } + + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + } + + return f.Interface() + } + + switch { + case chain.equalTo(reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + for i := 0; i < reflectVal.Type().NumField(); i++ { + if _, ok := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); ok { + return fmt.Errorf("pointer is required to set fields") + } + } + + case chain.equalTo(reflect.Ptr, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) + for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { + if newVal, ok := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); ok { + f := reflectVal.Elem().Field(i) + if !f.CanSet() { + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + } + + newRefVal, err := func() (reflect.Value, error) { + if convertToPtr && f.Kind() == reflect.Ptr && (newVal != nil || reflect.ValueOf(newVal).Kind() != reflect.Ptr) { + val, err := ValueOf(newVal, f.Elem().Type(), convert) + if err != nil { + return reflect.Value{}, err + } + + ptr := reflect.New(val.Type()) + ptr.Elem().Set(val) + + return ptr, nil + } + + return ValueOf(newVal, f.Type(), convert) + }() + + if err != nil { + return fmt.Errorf("field %d %+q: %w", i, reflectVal.Elem().Type().Field(i).Name, err) + } + + f.Set(newRefVal) + } + } + + case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + // TODO remove recursion + v := reflectVal.Elem() + tmp := reflect.New(v.Elem().Type()) + tmp.Elem().Set(v.Elem()) + if err := IterateFields(tmp.Interface(), callback, convert, convertToPtr); err != nil { + return err + } + v.Set(tmp.Elem()) + + default: + if err := ptrToNilStructError(strct); err != nil { + return err + } + + return fmt.Errorf("expected struct or pointer to struct, %T given", strct) + } + + return nil +} + //nolint:cyclop func Set(strct any, field string, val any, convert bool) (err error) { defer func() { diff --git a/setter/setter.go b/setter/setter.go index be10e53..6d9c0b1 100644 --- a/setter/setter.go +++ b/setter/setter.go @@ -21,9 +21,7 @@ package setter import ( - "reflect" - - intReflect "github.com/gontainer/reflectpro/internal/reflect" + "github.com/gontainer/reflectpro/internal/reflect" ) /* @@ -39,33 +37,5 @@ Unexported fields are supported. fmt.Println(p) // {Jane} */ func Set(strct any, field string, val any, convert bool) error { - return intReflect.Set(strct, field, val, convert) -} - -type CallbackOption func(*callbackConfig) - -type callbackConfig struct { - convertType bool - convertToPointer bool -} - -func ConvertType(cfg *callbackConfig) { - cfg.convertType = true -} - -func ConvertToPointer(cfg *callbackConfig) { - cfg.convertToPointer = true - -} - -// SetByCallback invokes callback on the all fields of the given struct. -// If you want to modify the value of the given field, the callback must return that value and `ok == true`. -func SetByCallback(strct any, callback func(field reflect.StructField) (_ any, ok bool), opts ...CallbackOption) error { - var cfg callbackConfig - - for _, o := range opts { - o(&cfg) - } - - return intReflect.SetByCallback(strct, callback, cfg.convertType, cfg.convertToPointer) + return reflect.Set(strct, field, val, convert) } From a8a3e3e83c4d9a8f766d3a43154fbde2f042680c Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 22:35:03 +0400 Subject: [PATCH 03/38] feat(fields): draft of the `fields.Iterate` --- fields/examples_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fields/examples_test.go b/fields/examples_test.go index d92c6c7..4dd9268 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -1,3 +1,23 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + package fields_test import ( From 2fea3f4213b5fb4a6ff7db5c4c244f7cd6748092 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 22:36:17 +0400 Subject: [PATCH 04/38] feat(fields): draft of the `fields.Iterate` --- setter/examples_test.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/setter/examples_test.go b/setter/examples_test.go index 2df2416..d697199 100644 --- a/setter/examples_test.go +++ b/setter/examples_test.go @@ -22,7 +22,6 @@ package setter_test import ( "fmt" - "reflect" "github.com/gontainer/reflectpro/setter" ) @@ -105,33 +104,3 @@ func ExampleSet_unaddressableValue() { // Output: // {Jane Doe} } - -func ExampleSetByCallback() { - var person struct { - Name string `custom:"name"` - Age int `custom:"age"` - } - - config := map[string]any{ - "name": "Jane", - "age": 25, - } - - _ = setter.SetByCallback( - &person, - func(field reflect.StructField) (_ interface{}, ok bool) { - // check whether the given tag exists - tag, ok := field.Tag.Lookup("custom") - if !ok { - return nil, false - } - - // check whether we have the desired value in the config - if val, ok := config[tag]; ok { - return val, ok - } - - return nil, false - }, - ) -} From 964d9534c74b85653bf726adfb5b7f462b307404 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 22:47:44 +0400 Subject: [PATCH 05/38] feat(fields): draft of the `fields.Iterate` decorate errors --- fields/iterate.go | 15 ++++++++------- fields/iterate_test.go | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index fd3d71e..b87ab29 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -102,10 +102,10 @@ func Iterate(strct any, opts ...Option) (err error) { func iterate(strct any, cfg *config, path []reflect.StructField) error { var fn intReflect.FieldCallback - var stopErr error + var getErr func() error fn = func(f reflect.StructField, value interface{}) (_ interface{}, ok bool) { - if stopErr != nil { + if getErr != nil { return nil, false } @@ -138,10 +138,11 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { original := value - newPath := append(path, f) // TODO maybe copy instead of append + newPath := append(path, f) if err := iterate(&value, cfg, newPath); err != nil { - // TODO decorate it - stopErr = err + getErr = func() error { + return fmt.Errorf("%s: %w", f.Name, err) + } return nil, false } @@ -170,8 +171,8 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return err } - if stopErr != nil { - return stopErr + if getErr != nil { + return getErr() } return nil diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 7c83a7e..0c1eb1a 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -193,7 +193,7 @@ func TestIterate(t *testing.T) { }, input: A{}, output: A{}, - error: `fields.Iterate: IterateFields: *interface {}: IterateFields: fields_test.C: field 0 "D": value of type int is not assignable to type string`, + error: `fields.Iterate: B: C: IterateFields: *interface {}: IterateFields: fields_test.C: field 0 "D": value of type int is not assignable to type string`, }, { name: "Employee (embedded)", From 374a6e948a24fe85157236e062857c00b7c41a65 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 23:30:44 +0400 Subject: [PATCH 06/38] feat(fields): `fields.Iterate` added `PrefillNilStructs` --- fields/examples_test.go | 31 +++++++++++++++++++++++++++++++ fields/iterate.go | 6 +++--- internal/reflect/get_set.go | 7 +++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 4dd9268..abcb4f5 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -23,6 +23,7 @@ package fields_test import ( "fmt" "reflect" + "time" "github.com/davecgh/go-spew/spew" "github.com/gontainer/reflectpro/fields" @@ -115,3 +116,33 @@ func ExampleSetUnexported() { // Output: // Android } + +type MyCache struct { + TTL time.Duration +} + +type MyConfig struct { + MyCache *MyCache +} + +func ExamplePrefillNilStructs() { + cfg := MyConfig{} + + _ = fields.Iterate( + &cfg, + fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { + if comparePaths(path, "MyCache", "TTL") { + return time.Minute, true + } + + return nil, false + }), + fields.PrefillNilStructs(true), + fields.Recursive(), + ) + + fmt.Println(cfg.MyCache.TTL) + + // Output: + // 1m0s +} diff --git a/fields/iterate.go b/fields/iterate.go index b87ab29..099331d 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -127,9 +127,9 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { // set poiner to a zero-value if !setterHasBeenTriggered && cfg.prefillNilStructs && - value == nil && - f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct { - value = reflect.New(f.Type.Elem()) + f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && + reflect.ValueOf(value).IsZero() { + value, setterHasBeenTriggered = reflect.New(f.Type.Elem()).Interface(), true } if cfg.recursive { diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index ba585ac..918d085 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -136,10 +136,9 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr return err } - // TODO - //if chain[len(chain)-1] != reflect.Struct { - // return fmt.Errorf("expected struct, %T given", strct) - //} + if chain[len(chain)-1] != reflect.Struct { + return fmt.Errorf("expected struct, %T given", strct) + } // see [Set] for { From 0824845b51c5a6f67b145d23e18a2cb08df92b53 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 23:46:39 +0400 Subject: [PATCH 07/38] feat(fields): `fields.Iterate` added `Path` --- fields/examples_test.go | 24 +++++---------------- fields/path.go | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 fields/path.go diff --git a/fields/examples_test.go b/fields/examples_test.go index abcb4f5..b5289d8 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -44,20 +44,6 @@ type TrainingPlan struct { Tuesday Exercise } -func comparePaths(reflectPath []reflect.StructField, names ...string) bool { - if len(reflectPath) != len(names) { - return false - } - - for i := 0; i < len(reflectPath); i++ { - if reflectPath[i].Name != names[i] { - return false - } - } - - return true -} - func ExampleSet() { p := TrainingPlan{} @@ -65,11 +51,11 @@ func ExampleSet() { &p, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { switch { - case comparePaths(path, "TrainingPlanMeta", "Name"): + case fields.Path(path).CompareStrings("TrainingPlanMeta", "Name"): return "My training plan", true - case comparePaths(path, "Monday", "Name"): + case fields.Path(path).CompareStrings("Monday", "Name"): return "pushups", true - case comparePaths(path, "Tuesday", "name"): + case fields.Path(path).CompareStrings("Tuesday", "name"): return "pullups", true } @@ -103,7 +89,7 @@ func ExampleSetUnexported() { _ = fields.Iterate( &p, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if path[len(path)-1].Name == "os" { + if fields.Path(path).CompareStrings("os") { return "Android", true } @@ -131,7 +117,7 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if comparePaths(path, "MyCache", "TTL") { + if fields.Path(path).CompareStrings("MyCache", "TTL") { return time.Minute, true } diff --git a/fields/path.go b/fields/path.go new file mode 100644 index 0000000..cdb9611 --- /dev/null +++ b/fields/path.go @@ -0,0 +1,48 @@ +package fields + +import ( + "reflect" +) + +type Path []reflect.StructField + +func (p Path) Names() []string { + if p == nil { + return nil + } + + r := make([]string, len(p)) + for i, x := range p { + r[i] = x.Name + } + + return r +} + +func (p Path) HasSuffix(path ...string) bool { + if len(p) < len(path) { + return false + } + + for i := 0; i < len(path); i++ { + if p[len(p)-1-i].Name != path[len(path)-1-i] { + return false + } + } + + return true +} + +func (p Path) CompareStrings(path ...string) bool { + if len(p) != len(path) { + return false + } + + for i := 0; i < len(p); i++ { + if p[i].Name != path[i] { + return false + } + } + + return true +} From 43c19efeb07ce7c5dc483f52630e8eedbc9021a4 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 23:47:22 +0400 Subject: [PATCH 08/38] feat(fields): `fields.Iterate` added `Path` --- fields/examples_test.go | 10 +++++----- fields/path.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index b5289d8..71df27d 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -51,11 +51,11 @@ func ExampleSet() { &p, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { switch { - case fields.Path(path).CompareStrings("TrainingPlanMeta", "Name"): + case fields.Path(path).CompareNames("TrainingPlanMeta", "Name"): return "My training plan", true - case fields.Path(path).CompareStrings("Monday", "Name"): + case fields.Path(path).CompareNames("Monday", "Name"): return "pushups", true - case fields.Path(path).CompareStrings("Tuesday", "name"): + case fields.Path(path).CompareNames("Tuesday", "name"): return "pullups", true } @@ -89,7 +89,7 @@ func ExampleSetUnexported() { _ = fields.Iterate( &p, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if fields.Path(path).CompareStrings("os") { + if fields.Path(path).CompareNames("os") { return "Android", true } @@ -117,7 +117,7 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if fields.Path(path).CompareStrings("MyCache", "TTL") { + if fields.Path(path).CompareNames("MyCache", "TTL") { return time.Minute, true } diff --git a/fields/path.go b/fields/path.go index cdb9611..797017c 100644 --- a/fields/path.go +++ b/fields/path.go @@ -33,7 +33,7 @@ func (p Path) HasSuffix(path ...string) bool { return true } -func (p Path) CompareStrings(path ...string) bool { +func (p Path) CompareNames(path ...string) bool { if len(p) != len(path) { return false } From 3e8364ed40770ab5e2c8ed5f1e4df364b032e5ea Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Mon, 23 Sep 2024 23:53:44 +0400 Subject: [PATCH 09/38] feat(fields): `fields.Iterate` replace `[]reflect.StructField` by `fields.Path` --- fields/examples_test.go | 17 ++++++----- fields/iterate.go | 8 +++--- fields/iterate_test.go | 62 ++++++++++++++++------------------------- 3 files changed, 36 insertions(+), 51 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 71df27d..4b2816f 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -22,7 +22,6 @@ package fields_test import ( "fmt" - "reflect" "time" "github.com/davecgh/go-spew/spew" @@ -49,13 +48,13 @@ func ExampleSet() { _ = fields.Iterate( &p, - fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { - case fields.Path(path).CompareNames("TrainingPlanMeta", "Name"): + case path.CompareNames("TrainingPlanMeta", "Name"): return "My training plan", true - case fields.Path(path).CompareNames("Monday", "Name"): + case path.CompareNames("Monday", "Name"): return "pushups", true - case fields.Path(path).CompareNames("Tuesday", "name"): + case path.CompareNames("Tuesday", "name"): return "pullups", true } @@ -88,8 +87,8 @@ func ExampleSetUnexported() { p := Phone{} _ = fields.Iterate( &p, - fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if fields.Path(path).CompareNames("os") { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + if path.CompareNames("os") { return "Android", true } @@ -116,8 +115,8 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, - fields.Setter(func(path []reflect.StructField, value any) (_ any, ok bool) { - if fields.Path(path).CompareNames("MyCache", "TTL") { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + if path.CompareNames("MyCache", "TTL") { return time.Minute, true } diff --git a/fields/iterate.go b/fields/iterate.go index 099331d..ae50e69 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -28,8 +28,8 @@ import ( ) type config struct { - setter func(path []reflect.StructField, value any) (_ any, ok bool) - getter func(path []reflect.StructField, value any) + setter func(_ Path, value any) (_ any, ok bool) + getter func(_ Path, value any) prefillNilStructs bool convertTypes bool convertToPtr bool @@ -59,13 +59,13 @@ func PrefillNilStructs(v bool) Option { } } -func Setter(fn func(path []reflect.StructField, value any) (_ any, ok bool)) Option { +func Setter(fn func(path Path, value any) (_ any, ok bool)) Option { return func(c *config) { c.setter = fn } } -func Getter(fn func(path []reflect.StructField, value any)) Option { +func Getter(fn func(_ Path, value any)) Option { return func(c *config) { c.getter = fn } diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 0c1eb1a..8508dca 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -73,20 +73,6 @@ type YY struct { *XX } -func pathEquals(p []reflect.StructField, s ...string) bool { - if len(p) != len(s) { - return false - } - - for i, f := range p { - if f.Name != s[i] { - return false - } - } - - return true -} - func setValueByFieldIndex(ptrStruct any, fieldIndex int, value any) { f := reflect.ValueOf(ptrStruct).Elem().Field(fieldIndex) f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() @@ -120,7 +106,7 @@ func TestIterate(t *testing.T) { { name: "Person OK", options: []fields.Option{ - fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { return "Jane", true }), }, @@ -133,7 +119,7 @@ func TestIterate(t *testing.T) { { name: "Person OK (convert types)", options: []fields.Option{ - fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { return CustomString("Jane"), true }), fields.ConvertTypes(), @@ -147,7 +133,7 @@ func TestIterate(t *testing.T) { { name: "Person error (convert types)", options: []fields.Option{ - fields.Setter(func(_ []reflect.StructField, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { return CustomString("Jane"), true }), }, @@ -160,8 +146,8 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D OK", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { - if pathEquals(path, "B", "C", "D") { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + if path.CompareNames("B", "C", "D") { return "Hello", true } @@ -182,8 +168,8 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D error (convert types)", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { - if pathEquals(path, "B", "C", "D") { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + if path.CompareNames("B", "C", "D") { return 5, true } @@ -198,11 +184,11 @@ func TestIterate(t *testing.T) { { name: "Employee (embedded)", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value interface{}) (_ any, ok bool) { switch { - case pathEquals(path, "Person", "Name"): + case path.CompareNames("Person", "Name"): return "Jane", true - case pathEquals(path, "Role"): + case path.CompareNames("Role"): return "Lead", true } @@ -222,13 +208,13 @@ func TestIterate(t *testing.T) { { name: "Team #1", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { switch { - case pathEquals(path, "Lead", "Person", "Name"): + case path.CompareNames("Lead", "Person", "Name"): return "Jane", true - case pathEquals(path, "Lead", "Role"): + case path.CompareNames("Lead", "Role"): return "Lead", true - case pathEquals(path, "TeamMeta", "Name"): + case path.CompareNames("TeamMeta", "Name"): return "Hawkeye", true } @@ -253,18 +239,18 @@ func TestIterate(t *testing.T) { { name: "Team #2", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { switch { - case pathEquals(path, "Lead", "Role"): + case path.CompareNames("Lead", "Role"): return "Lead", true - case pathEquals(path, "Lead"): + case path.CompareNames("Lead"): return Employee{ Person: Person{ Name: "Jane", }, Role: "Lead", }, true - case pathEquals(path, "TeamMeta", "Name"): + case path.CompareNames("TeamMeta", "Name"): return "Hawkeye", true } @@ -289,12 +275,12 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { - if pathEquals(path, "XX") { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + if path.CompareNames("XX") { return &XX{}, true } - if pathEquals(path, "XX", "_") { + if path.CompareNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: return 5, true @@ -315,12 +301,12 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path []reflect.StructField, value interface{}) (_ interface{}, ok bool) { - if pathEquals(path, "XX") { + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + if path.CompareNames("XX") { return &XX{}, true } - if pathEquals(path, "XX", "_") { + if path.CompareNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: return 7, true From f5ccbacef8441ee994eada2f78858944486e910b Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:02:51 +0400 Subject: [PATCH 10/38] feat(fields): `fields.Iterate` added `ExampleGetter` --- fields/examples_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/fields/examples_test.go b/fields/examples_test.go index 4b2816f..3e700a6 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/gontainer/reflectpro/copier" "github.com/gontainer/reflectpro/fields" ) @@ -131,3 +132,36 @@ func ExamplePrefillNilStructs() { // Output: // 1m0s } + +type CTO struct { + Salary int +} + +type Company struct { + CTO CTO +} + +func ExampleGetter() { + c := Company{ + CTO: CTO{ + Salary: 1000000, + }, + } + + var salary int + + _ = fields.Iterate( + c, + fields.Getter(func(p fields.Path, value interface{}) { + if p.CompareNames("CTO", "Salary") { + _ = copier.Copy(value, &salary, false) + } + }), + fields.Recursive(), + ) + + fmt.Println(salary) + + // Output: + // 1000000 +} From 82e120c46d20869d0eb217b2f7370853aec2cca4 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:18:18 +0400 Subject: [PATCH 11/38] feat(fields): `fields.Iterate` added `ExampleConvertToPointers` --- fields/examples_test.go | 25 +++++++++++++++++++++++++ internal/reflect/get_set.go | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 3e700a6..5c21977 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -165,3 +165,28 @@ func ExampleGetter() { // Output: // 1000000 } + +type FeatureConfig struct { + Active *bool +} + +func ExampleConvertToPointers() { + cfg := FeatureConfig{} + + _ = fields.Iterate( + &cfg, + fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + if path.CompareNames("Active") { + return true, true + } + + return nil, false + }), + fields.ConvertToPointers(), + ) + + fmt.Println(*cfg.Active) + + // Output: + // true +} diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 918d085..cac51dc 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -194,7 +194,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr newRefVal, err := func() (reflect.Value, error) { if convertToPtr && f.Kind() == reflect.Ptr && (newVal != nil || reflect.ValueOf(newVal).Kind() != reflect.Ptr) { - val, err := ValueOf(newVal, f.Elem().Type(), convert) + val, err := ValueOf(newVal, f.Type().Elem(), convert) if err != nil { return reflect.Value{}, err } From 8d5a8ffd2265aac10c93c9e148a617d27d6cb716 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:19:51 +0400 Subject: [PATCH 12/38] feat(fields): `fields.Iterate` use `any` --- fields/examples_test.go | 4 ++-- fields/iterate.go | 2 +- fields/iterate_test.go | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 5c21977..c7d26a3 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -152,7 +152,7 @@ func ExampleGetter() { _ = fields.Iterate( c, - fields.Getter(func(p fields.Path, value interface{}) { + fields.Getter(func(p fields.Path, value any) { if p.CompareNames("CTO", "Salary") { _ = copier.Copy(value, &salary, false) } @@ -175,7 +175,7 @@ func ExampleConvertToPointers() { _ = fields.Iterate( &cfg, - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { if path.CompareNames("Active") { return true, true } diff --git a/fields/iterate.go b/fields/iterate.go index ae50e69..38bd023 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -104,7 +104,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { var getErr func() error - fn = func(f reflect.StructField, value interface{}) (_ interface{}, ok bool) { + fn = func(f reflect.StructField, value any) (_ any, ok bool) { if getErr != nil { return nil, false } diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 8508dca..130cbfb 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -146,7 +146,7 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D OK", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { if path.CompareNames("B", "C", "D") { return "Hello", true } @@ -168,7 +168,7 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D error (convert types)", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { if path.CompareNames("B", "C", "D") { return 5, true } @@ -184,7 +184,7 @@ func TestIterate(t *testing.T) { { name: "Employee (embedded)", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { case path.CompareNames("Person", "Name"): return "Jane", true @@ -208,7 +208,7 @@ func TestIterate(t *testing.T) { { name: "Team #1", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { case path.CompareNames("Lead", "Person", "Name"): return "Jane", true @@ -239,7 +239,7 @@ func TestIterate(t *testing.T) { { name: "Team #2", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { case path.CompareNames("Lead", "Role"): return "Lead", true @@ -275,7 +275,7 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { if path.CompareNames("XX") { return &XX{}, true } @@ -301,7 +301,7 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path fields.Path, value interface{}) (_ interface{}, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { if path.CompareNames("XX") { return &XX{}, true } From 70bfddc7ec9c0861aa685a75f1680f835c12c359 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:28:08 +0400 Subject: [PATCH 13/38] feat(fields): `fields.Iterate` cleanup --- internal/reflect/get_set.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index cac51dc..33afefd 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -218,7 +218,6 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) - // TODO remove recursion v := reflectVal.Elem() tmp := reflect.New(v.Elem().Type()) tmp.Elem().Set(v.Elem()) From e26a3bea8b70bec22724c87d19e62945b1d37394 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:41:56 +0400 Subject: [PATCH 14/38] feat(fields): `fields.Iterate` added `ExampleReadJSON` --- fields/examples_test.go | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/fields/examples_test.go b/fields/examples_test.go index c7d26a3..e9d9d47 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -21,7 +21,9 @@ package fields_test import ( + "encoding/json" "fmt" + "strings" "time" "github.com/davecgh/go-spew/spew" @@ -190,3 +192,49 @@ func ExampleConvertToPointers() { // Output: // true } + +func ExampleReadJSON() { + var person struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Age uint `json:"age"` + Bio string `json:"-"` + } + + js := ` +{ + "firstname": "Jane", + "lastname": "Doe", + "age": 30, + "bio": "bio..." +}` + var data map[string]any + _ = json.Unmarshal([]byte(js), &data) + + _ = fields.Iterate( + &person, + fields.Setter(func(p fields.Path, value any) (_ any, ok bool) { + tag, ok := p[len(p)-1].Tag.Lookup("json") + if !ok { + return nil, false + } + + name := strings.Split(tag, ",")[0] + if name == "-" { + return nil, false + } + + if fromJSON, ok := data[name]; ok { + return fromJSON, true + } + + return nil, false + }), + fields.ConvertTypes(), + ) + + fmt.Printf("%+v\n", person) + + // Output: + // {Firstname:Jane Lastname:Doe Age:30 Bio:} +} From 083356f3015df21c1cfb1f335569f052a44798e2 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:49:27 +0400 Subject: [PATCH 15/38] feat(fields): `fields.Iterate` docs --- fields/path.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fields/path.go b/fields/path.go index 797017c..8f0fe5c 100644 --- a/fields/path.go +++ b/fields/path.go @@ -4,6 +4,12 @@ import ( "reflect" ) +// Path is built over [reflect.StructField], +// that exports us useful details like: +// - [reflect.StructField.Name] +// - [reflect.StructField.Anonymous] +// - [reflect.StructField.Tag] +// - [reflect.StructField.Type] type Path []reflect.StructField func (p Path) Names() []string { From 7888aa4fc6827906635d0b19bbffe61809d1a7cc Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:49:45 +0400 Subject: [PATCH 16/38] feat(fields): `fields.Iterate` examples --- fields/examples_test.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index e9d9d47..d4cd4ae 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -168,29 +168,27 @@ func ExampleGetter() { // 1000000 } -type FeatureConfig struct { - Active *bool -} - func ExampleConvertToPointers() { - cfg := FeatureConfig{} + var cfg struct { + TTL *time.Duration // expect a pointer + } _ = fields.Iterate( &cfg, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("Active") { - return true, true + if path.CompareNames("TTL") { + return time.Minute, true // return a value } return nil, false }), - fields.ConvertToPointers(), + fields.ConvertToPointers(), // this line will instruct the library to convert values to pointers ) - fmt.Println(*cfg.Active) + fmt.Println(*cfg.TTL) // Output: - // true + // 1m0s } func ExampleReadJSON() { @@ -201,6 +199,7 @@ func ExampleReadJSON() { Bio string `json:"-"` } + // read data from JSON... js := ` { "firstname": "Jane", @@ -211,6 +210,8 @@ func ExampleReadJSON() { var data map[string]any _ = json.Unmarshal([]byte(js), &data) + // populate the data from JSON to the `person` variable, + // use struct tags, to determine the correct relations _ = fields.Iterate( &person, fields.Setter(func(p fields.Path, value any) (_ any, ok bool) { From daeedee16e6644cfd9c93011e8cc1d9043745a4a Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:53:04 +0400 Subject: [PATCH 17/38] feat(fields): `fields.Iterate` `CompareNames` => `EqualNames` --- fields/examples_test.go | 14 +++++++------- fields/iterate_test.go | 28 ++++++++++++++-------------- fields/path.go | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index d4cd4ae..a8fc996 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -53,11 +53,11 @@ func ExampleSet() { &p, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { - case path.CompareNames("TrainingPlanMeta", "Name"): + case path.EqualNames("TrainingPlanMeta", "Name"): return "My training plan", true - case path.CompareNames("Monday", "Name"): + case path.EqualNames("Monday", "Name"): return "pushups", true - case path.CompareNames("Tuesday", "name"): + case path.EqualNames("Tuesday", "name"): return "pullups", true } @@ -91,7 +91,7 @@ func ExampleSetUnexported() { _ = fields.Iterate( &p, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("os") { + if path.EqualNames("os") { return "Android", true } @@ -119,7 +119,7 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("MyCache", "TTL") { + if path.EqualNames("MyCache", "TTL") { return time.Minute, true } @@ -155,7 +155,7 @@ func ExampleGetter() { _ = fields.Iterate( c, fields.Getter(func(p fields.Path, value any) { - if p.CompareNames("CTO", "Salary") { + if p.EqualNames("CTO", "Salary") { _ = copier.Copy(value, &salary, false) } }), @@ -176,7 +176,7 @@ func ExampleConvertToPointers() { _ = fields.Iterate( &cfg, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("TTL") { + if path.EqualNames("TTL") { return time.Minute, true // return a value } diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 130cbfb..b118576 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -147,7 +147,7 @@ func TestIterate(t *testing.T) { name: "A.B.C.D OK", options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("B", "C", "D") { + if path.EqualNames("B", "C", "D") { return "Hello", true } @@ -169,7 +169,7 @@ func TestIterate(t *testing.T) { name: "A.B.C.D error (convert types)", options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("B", "C", "D") { + if path.EqualNames("B", "C", "D") { return 5, true } @@ -186,9 +186,9 @@ func TestIterate(t *testing.T) { options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { - case path.CompareNames("Person", "Name"): + case path.EqualNames("Person", "Name"): return "Jane", true - case path.CompareNames("Role"): + case path.EqualNames("Role"): return "Lead", true } @@ -210,11 +210,11 @@ func TestIterate(t *testing.T) { options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { - case path.CompareNames("Lead", "Person", "Name"): + case path.EqualNames("Lead", "Person", "Name"): return "Jane", true - case path.CompareNames("Lead", "Role"): + case path.EqualNames("Lead", "Role"): return "Lead", true - case path.CompareNames("TeamMeta", "Name"): + case path.EqualNames("TeamMeta", "Name"): return "Hawkeye", true } @@ -241,16 +241,16 @@ func TestIterate(t *testing.T) { options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { switch { - case path.CompareNames("Lead", "Role"): + case path.EqualNames("Lead", "Role"): return "Lead", true - case path.CompareNames("Lead"): + case path.EqualNames("Lead"): return Employee{ Person: Person{ Name: "Jane", }, Role: "Lead", }, true - case path.CompareNames("TeamMeta", "Name"): + case path.EqualNames("TeamMeta", "Name"): return "Hawkeye", true } @@ -276,11 +276,11 @@ func TestIterate(t *testing.T) { name: "YY", options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("XX") { + if path.EqualNames("XX") { return &XX{}, true } - if path.CompareNames("XX", "_") { + if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: return 5, true @@ -302,11 +302,11 @@ func TestIterate(t *testing.T) { name: "YY", options: []fields.Option{ fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { - if path.CompareNames("XX") { + if path.EqualNames("XX") { return &XX{}, true } - if path.CompareNames("XX", "_") { + if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: return 7, true diff --git a/fields/path.go b/fields/path.go index 8f0fe5c..374f188 100644 --- a/fields/path.go +++ b/fields/path.go @@ -39,7 +39,7 @@ func (p Path) HasSuffix(path ...string) bool { return true } -func (p Path) CompareNames(path ...string) bool { +func (p Path) EqualNames(path ...string) bool { if len(p) != len(path) { return false } From c2ce7a307da0505df551c72629f6ee835709c592 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:54:39 +0400 Subject: [PATCH 18/38] feat(fields): `fields.Iterate` typo --- fields/iterate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fields/iterate.go b/fields/iterate.go index 38bd023..cdfce87 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -124,7 +124,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { } } - // set poiner to a zero-value + // set pointer to a zero-value if !setterHasBeenTriggered && cfg.prefillNilStructs && f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && From 49f80eabccc60204f184b2f0ea031202315a10fc Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:56:29 +0400 Subject: [PATCH 19/38] feat(fields): `fields.Iterate` addlicense --- fields/path.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fields/path.go b/fields/path.go index 374f188..0b46b8c 100644 --- a/fields/path.go +++ b/fields/path.go @@ -1,3 +1,23 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + package fields import ( From 8c707c9411e8ad4212bdd99c01d695b769aa0b63 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 00:59:46 +0400 Subject: [PATCH 20/38] feat(fields): `fields.Iterate` docs --- fields/examples_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fields/examples_test.go b/fields/examples_test.go index a8fc996..820e7f9 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -116,6 +116,13 @@ type MyConfig struct { func ExamplePrefillNilStructs() { cfg := MyConfig{} + /* + cfg.MyCache equals nil, but line `fields.PrefillNilStructs(true)` instructs the library + to inject a pointer to the zero-value automatically, so we don't need to execute the following line manually: + + cfg.MyCache = &MyCache{} + */ + _ = fields.Iterate( &cfg, fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { From bcbbab07779a9f630bd5481c84493e4024d2e332 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:00:15 +0400 Subject: [PATCH 21/38] feat(fields): `fields.Iterate` docs --- fields/examples_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 820e7f9..34b7759 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -117,7 +117,7 @@ func ExamplePrefillNilStructs() { cfg := MyConfig{} /* - cfg.MyCache equals nil, but line `fields.PrefillNilStructs(true)` instructs the library + `cfg.MyCache` equals nil, but the line `fields.PrefillNilStructs(true)` instructs the library to inject a pointer to the zero-value automatically, so we don't need to execute the following line manually: cfg.MyCache = &MyCache{} From 2900d038d97c705142ad8481b38d76ccc43a6819 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:02:11 +0400 Subject: [PATCH 22/38] feat(fields): `fields.Iterate` examples --- fields/examples_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 34b7759..339262c 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -51,7 +51,7 @@ func ExampleSet() { _ = fields.Iterate( &p, - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { switch { case path.EqualNames("TrainingPlanMeta", "Name"): return "My training plan", true @@ -90,7 +90,7 @@ func ExampleSetUnexported() { p := Phone{} _ = fields.Iterate( &p, - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { if path.EqualNames("os") { return "Android", true } @@ -125,7 +125,7 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { if path.EqualNames("MyCache", "TTL") { return time.Minute, true } @@ -182,7 +182,7 @@ func ExampleConvertToPointers() { _ = fields.Iterate( &cfg, - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { if path.EqualNames("TTL") { return time.Minute, true // return a value } @@ -221,7 +221,7 @@ func ExampleReadJSON() { // use struct tags, to determine the correct relations _ = fields.Iterate( &person, - fields.Setter(func(p fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(p fields.Path, _ any) (_ any, ok bool) { tag, ok := p[len(p)-1].Tag.Lookup("json") if !ok { return nil, false From a2d4b8eef29e28dcb2ea2a7f9ba84c44d2cbb487 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:05:14 +0400 Subject: [PATCH 23/38] feat(fields): `fields.Iterate` cleanup --- fields/iterate.go | 2 +- internal/reflect/get_set.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index cdfce87..953e1ed 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -104,7 +104,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { var getErr func() error - fn = func(f reflect.StructField, value any) (_ any, ok bool) { + fn = func(f reflect.StructField, value any) (_ any, set bool) { if getErr != nil { return nil, false } diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 33afefd..80fb958 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -112,7 +112,7 @@ func Get(strct any, field string) (_ any, err error) { //nolint:cyclop,ireturn return f.Interface(), nil } -type FieldCallback = func(_ reflect.StructField, value any) (_ any, ok bool) +type FieldCallback = func(_ reflect.StructField, value any) (_ any, set bool) func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr bool) (err error) { strType := "" @@ -178,7 +178,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr case chain.equalTo(reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) for i := 0; i < reflectVal.Type().NumField(); i++ { - if _, ok := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); ok { + if _, set := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); set { return fmt.Errorf("pointer is required to set fields") } } @@ -186,7 +186,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr case chain.equalTo(reflect.Ptr, reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { - if newVal, ok := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); ok { + if newVal, set := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); set { f := reflectVal.Elem().Field(i) if !f.CanSet() { f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() From 59b7e402731c04792595a2442bdd390eac27ff74 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:19:08 +0400 Subject: [PATCH 24/38] feat(fields): `fields.Iterate` tests `TestIterateFields` --- internal/reflect/get_set_test.go | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index 212044a..8647552 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -21,10 +21,13 @@ package reflect_test import ( + "fmt" + stdReflect "reflect" "testing" "github.com/gontainer/reflectpro/internal/reflect" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGet(t *testing.T) { @@ -438,3 +441,62 @@ type wallet struct { type storage struct { wallets []wallet } + +func TestIterateFields(t *testing.T) { + t.Parallel() + + t.Run("Set", func(t *testing.T) { + t.Parallel() + + scenarios := []struct { + strct any + callback reflect.FieldCallback + convert bool + convertToPtr bool + + expected any + error string + }{ + { + strct: person{}, + callback: func(f stdReflect.StructField, value interface{}) (_ any, set bool) { + if f.Name == "Name" { + return "Jane", true + } + + if f.Name == "age" { + return uint(30), true + } + + return nil, false + }, + convert: true, + convertToPtr: false, + expected: person{ + Name: "Jane", + age: 30, + }, + }, + } + + for i, s := range scenarios { + s := s + + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + strct := s.strct + err := reflect.IterateFields(&strct, s.callback, s.convert, s.convertToPtr) + + if s.error != "" { + require.EqualError(t, err, s.error) + + return + } + + require.NoError(t, err) + assert.Equal(t, s.expected, strct) + }) + } + }) +} From 9e96a4bfeaae767ef5a25322d88327fe45697d18 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:20:07 +0400 Subject: [PATCH 25/38] feat(fields): `fields.Iterate` tests `TestIterateFields` --- internal/reflect/get_set_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index 8647552..0939782 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -477,6 +477,23 @@ func TestIterateFields(t *testing.T) { age: 30, }, }, + { + strct: person{}, + callback: func(f stdReflect.StructField, value interface{}) (_ any, set bool) { + if f.Name == "Name" { + return "Jane", true + } + + if f.Name == "age" { + return uint(30), true + } + + return nil, false + }, + convert: false, + convertToPtr: false, + error: `IterateFields: *interface {}: IterateFields: reflect_test.person: field 1 "age": value of type uint is not assignable to type uint8`, + }, } for i, s := range scenarios { From 03ec1ff938f102a443c89d80cb1ff1e1beaff35e Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:22:36 +0400 Subject: [PATCH 26/38] feat(fields): `fields.Iterate` use `any` --- internal/reflect/get_set_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index 0939782..df04cb3 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -459,7 +459,7 @@ func TestIterateFields(t *testing.T) { }{ { strct: person{}, - callback: func(f stdReflect.StructField, value interface{}) (_ any, set bool) { + callback: func(f stdReflect.StructField, value any) (_ any, set bool) { if f.Name == "Name" { return "Jane", true } @@ -479,7 +479,7 @@ func TestIterateFields(t *testing.T) { }, { strct: person{}, - callback: func(f stdReflect.StructField, value interface{}) (_ any, set bool) { + callback: func(f stdReflect.StructField, value any) (_ any, set bool) { if f.Name == "Name" { return "Jane", true } From 5417c1b01eb8c8a54b8f02144146e5ccb255b86d Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:34:51 +0400 Subject: [PATCH 27/38] feat(fields): `fields.Iterate` add argument `v` to: * `ConvertTypes` * `ConvertToPointers` * `Recursive` --- fields/examples_test.go | 10 +++++----- fields/iterate.go | 12 ++++++------ fields/iterate_test.go | 16 ++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 339262c..05e45c0 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -63,7 +63,7 @@ func ExampleSet() { return nil, false }), - fields.Recursive(), + fields.Recursive(true), ) spew.Dump(p) @@ -133,7 +133,7 @@ func ExamplePrefillNilStructs() { return nil, false }), fields.PrefillNilStructs(true), - fields.Recursive(), + fields.Recursive(true), ) fmt.Println(cfg.MyCache.TTL) @@ -166,7 +166,7 @@ func ExampleGetter() { _ = copier.Copy(value, &salary, false) } }), - fields.Recursive(), + fields.Recursive(true), ) fmt.Println(salary) @@ -189,7 +189,7 @@ func ExampleConvertToPointers() { return nil, false }), - fields.ConvertToPointers(), // this line will instruct the library to convert values to pointers + fields.ConvertToPointers(true), // this line will instruct the library to convert values to pointers ) fmt.Println(*cfg.TTL) @@ -238,7 +238,7 @@ func ExampleReadJSON() { return nil, false }), - fields.ConvertTypes(), + fields.ConvertTypes(true), ) fmt.Printf("%+v\n", person) diff --git a/fields/iterate.go b/fields/iterate.go index 953e1ed..48babea 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -71,21 +71,21 @@ func Getter(fn func(_ Path, value any)) Option { } } -func ConvertTypes() Option { +func ConvertTypes(v bool) Option { return func(c *config) { - c.convertTypes = true + c.convertTypes = v } } -func ConvertToPointers() Option { +func ConvertToPointers(v bool) Option { return func(c *config) { - c.convertToPtr = true + c.convertToPtr = v } } -func Recursive() Option { +func Recursive(v bool) Option { return func(c *config) { - c.recursive = true + c.recursive = v } } diff --git a/fields/iterate_test.go b/fields/iterate_test.go index b118576..7ad2f59 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -122,7 +122,7 @@ func TestIterate(t *testing.T) { fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { return CustomString("Jane"), true }), - fields.ConvertTypes(), + fields.ConvertTypes(true), }, input: Person{}, output: Person{ @@ -153,7 +153,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: A{}, output: A{ @@ -175,7 +175,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: A{}, output: A{}, @@ -194,7 +194,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: Employee{}, output: Employee{ @@ -220,7 +220,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: Team{}, output: Team{ @@ -256,7 +256,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: Team{}, output: Team{ @@ -291,7 +291,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: YY{}, output: YY{ @@ -317,7 +317,7 @@ func TestIterate(t *testing.T) { return nil, false }), - fields.Recursive(), + fields.Recursive(true), }, input: YY{}, output: YY{ From 1344b9427ccdf02454d4ae8406e2498a956f6ec8 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:35:54 +0400 Subject: [PATCH 28/38] feat(fields): `fields.Iterate` cleanup --- fields/iterate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fields/iterate.go b/fields/iterate.go index 48babea..c3688e2 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -134,7 +134,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { if cfg.recursive { if f.Type.Kind() == reflect.Struct || // value is a struct - (f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && value != nil) { // value is a pointer to a non-nil struct + (f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && !reflect.ValueOf(value).IsZero()) { // value is a pointer to a non-nil struct original := value From 877b72da8c5172b587a8258d07a01d070cba49e4 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:36:55 +0400 Subject: [PATCH 29/38] feat(fields): `fields.Iterate` cleanup --- fields/iterate.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index c3688e2..4a7e850 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -138,8 +138,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { original := value - newPath := append(path, f) - if err := iterate(&value, cfg, newPath); err != nil { + if err := iterate(&value, cfg, append(path, f)); err != nil { getErr = func() error { return fmt.Errorf("%s: %w", f.Name, err) } From 4d7a91c6de07abf880c5b6dab178fbfe88ca2262 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:38:57 +0400 Subject: [PATCH 30/38] feat(fields): `fields.Iterate` cleanup --- fields/iterate.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 4a7e850..1393938 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -102,10 +102,10 @@ func Iterate(strct any, opts ...Option) (err error) { func iterate(strct any, cfg *config, path []reflect.StructField) error { var fn intReflect.FieldCallback - var getErr func() error + var finalErr error fn = func(f reflect.StructField, value any) (_ any, set bool) { - if getErr != nil { + if finalErr != nil { return nil, false } @@ -139,9 +139,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { original := value if err := iterate(&value, cfg, append(path, f)); err != nil { - getErr = func() error { - return fmt.Errorf("%s: %w", f.Name, err) - } + finalErr = fmt.Errorf("%s: %w", f.Name, err) return nil, false } @@ -170,8 +168,8 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return err } - if getErr != nil { - return getErr() + if finalErr != nil { + return finalErr } return nil From 13ef990a1ad274972a32f8799020b31d3adf662d Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:45:18 +0400 Subject: [PATCH 31/38] feat(fields): `fields.Iterate` * rename `ok` to `set` * added `ExampleBlank` --- fields/examples_test.go | 29 ++++++++++++++++++++++++----- fields/iterate.go | 2 +- fields/iterate_test.go | 20 ++++++++++---------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index 05e45c0..e125c64 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -51,7 +51,7 @@ func ExampleSet() { _ = fields.Iterate( &p, - fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { switch { case path.EqualNames("TrainingPlanMeta", "Name"): return "My training plan", true @@ -90,7 +90,7 @@ func ExampleSetUnexported() { p := Phone{} _ = fields.Iterate( &p, - fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { if path.EqualNames("os") { return "Android", true } @@ -125,7 +125,7 @@ func ExamplePrefillNilStructs() { _ = fields.Iterate( &cfg, - fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { if path.EqualNames("MyCache", "TTL") { return time.Minute, true } @@ -182,7 +182,7 @@ func ExampleConvertToPointers() { _ = fields.Iterate( &cfg, - fields.Setter(func(path fields.Path, _ any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { if path.EqualNames("TTL") { return time.Minute, true // return a value } @@ -221,7 +221,7 @@ func ExampleReadJSON() { // use struct tags, to determine the correct relations _ = fields.Iterate( &person, - fields.Setter(func(p fields.Path, _ any) (_ any, ok bool) { + fields.Setter(func(p fields.Path, _ any) (_ any, set bool) { tag, ok := p[len(p)-1].Tag.Lookup("json") if !ok { return nil, false @@ -246,3 +246,22 @@ func ExampleReadJSON() { // Output: // {Firstname:Jane Lastname:Doe Age:30 Bio:} } + +func ExampleBlank() { + var data struct { + _ int // fields.Iterate can access blank identifier + } + + _ = fields.Iterate(&data, fields.Setter(func(path fields.Path, value any) (_ any, set bool) { + if path.EqualNames("_") { + return 10, true + } + + return nil, false + })) + + fmt.Println(data) + + // Output: + // {10} +} diff --git a/fields/iterate.go b/fields/iterate.go index 1393938..b688ae7 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -59,7 +59,7 @@ func PrefillNilStructs(v bool) Option { } } -func Setter(fn func(path Path, value any) (_ any, ok bool)) Option { +func Setter(fn func(path Path, value any) (_ any, set bool)) Option { return func(c *config) { c.setter = fn } diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 7ad2f59..1f3ce07 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -106,7 +106,7 @@ func TestIterate(t *testing.T) { { name: "Person OK", options: []fields.Option{ - fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, set bool) { return "Jane", true }), }, @@ -119,7 +119,7 @@ func TestIterate(t *testing.T) { { name: "Person OK (convert types)", options: []fields.Option{ - fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, set bool) { return CustomString("Jane"), true }), fields.ConvertTypes(true), @@ -133,7 +133,7 @@ func TestIterate(t *testing.T) { { name: "Person error (convert types)", options: []fields.Option{ - fields.Setter(func(_ fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(_ fields.Path, value any) (_ any, set bool) { return CustomString("Jane"), true }), }, @@ -146,7 +146,7 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D OK", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { if path.EqualNames("B", "C", "D") { return "Hello", true } @@ -168,7 +168,7 @@ func TestIterate(t *testing.T) { { name: "A.B.C.D error (convert types)", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { if path.EqualNames("B", "C", "D") { return 5, true } @@ -184,7 +184,7 @@ func TestIterate(t *testing.T) { { name: "Employee (embedded)", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { switch { case path.EqualNames("Person", "Name"): return "Jane", true @@ -208,7 +208,7 @@ func TestIterate(t *testing.T) { { name: "Team #1", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { switch { case path.EqualNames("Lead", "Person", "Name"): return "Jane", true @@ -239,7 +239,7 @@ func TestIterate(t *testing.T) { { name: "Team #2", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { switch { case path.EqualNames("Lead", "Role"): return "Lead", true @@ -275,7 +275,7 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { if path.EqualNames("XX") { return &XX{}, true } @@ -301,7 +301,7 @@ func TestIterate(t *testing.T) { { name: "YY", options: []fields.Option{ - fields.Setter(func(path fields.Path, value any) (_ any, ok bool) { + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { if path.EqualNames("XX") { return &XX{}, true } From 09c26a41ad6e75dfa9f2a796d52584c89f9a4965 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Tue, 24 Sep 2024 01:47:59 +0400 Subject: [PATCH 32/38] feat(fields): `fields.Iterate` cleanup --- go.mod | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ce2dd88..6b3b5ec 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/gontainer/reflectpro go 1.14 require ( - github.com/davecgh/go-spew v1.1.1 github.com/gontainer/grouperror v1.0.1 github.com/stretchr/testify v1.8.2 ) + +require ( // tests + github.com/davecgh/go-spew v1.1.1 +) From c06dda7f44c876be56f3848e6e140bea1de1fc23 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 00:27:59 +0400 Subject: [PATCH 33/38] feat(fields): `fields.Iterate` cleanup --- fields/examples_test.go | 11 +++++++---- fields/iterate.go | 2 ++ fields/iterate_test.go | 7 +++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index e125c64..bec4599 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -46,7 +46,7 @@ type TrainingPlan struct { Tuesday Exercise } -func ExampleSet() { +func ExampleIterate_set() { p := TrainingPlan{} _ = fields.Iterate( @@ -86,7 +86,7 @@ type Phone struct { os string } -func ExampleSetUnexported() { +func ExampleIterate_setUnexported() { p := Phone{} _ = fields.Iterate( &p, @@ -150,7 +150,7 @@ type Company struct { CTO CTO } -func ExampleGetter() { +func ExampleIterate_get() { c := Company{ CTO: CTO{ Salary: 1000000, @@ -247,11 +247,13 @@ func ExampleReadJSON() { // {Firstname:Jane Lastname:Doe Age:30 Bio:} } -func ExampleBlank() { +func ExampleIterate_blank() { var data struct { _ int // fields.Iterate can access blank identifier } + fmt.Println(data) + _ = fields.Iterate(&data, fields.Setter(func(path fields.Path, value any) (_ any, set bool) { if path.EqualNames("_") { return 10, true @@ -263,5 +265,6 @@ func ExampleBlank() { fmt.Println(data) // Output: + // {0} // {10} } diff --git a/fields/iterate.go b/fields/iterate.go index b688ae7..42f9ea6 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -99,6 +99,7 @@ func Iterate(strct any, opts ...Option) (err error) { return iterate(strct, newConfig(opts...), nil) } +//nolint:gocognit func iterate(strct any, cfg *config, path []reflect.StructField) error { var fn intReflect.FieldCallback @@ -132,6 +133,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { value, setterHasBeenTriggered = reflect.New(f.Type.Elem()).Interface(), true } + //nolint:gocognit if cfg.recursive { if f.Type.Kind() == reflect.Struct || // value is a struct (f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && !reflect.ValueOf(value).IsZero()) { // value is a pointer to a non-nil struct diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 1f3ce07..b07a615 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -92,6 +92,7 @@ func newXXWithBlankValues(t *testing.T, first int, second string) *XX { return &x } +//nolint:gocognit,goconst,lll func TestIterate(t *testing.T) { t.Parallel() @@ -106,7 +107,7 @@ func TestIterate(t *testing.T) { { name: "Person OK", options: []fields.Option{ - fields.Setter(func(_ fields.Path, value any) (_ any, set bool) { + fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) { return "Jane", true }), }, @@ -119,7 +120,7 @@ func TestIterate(t *testing.T) { { name: "Person OK (convert types)", options: []fields.Option{ - fields.Setter(func(_ fields.Path, value any) (_ any, set bool) { + fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) { return CustomString("Jane"), true }), fields.ConvertTypes(true), @@ -280,6 +281,7 @@ func TestIterate(t *testing.T) { return &XX{}, true } + //nolint:revive,exhaustive if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: @@ -306,6 +308,7 @@ func TestIterate(t *testing.T) { return &XX{}, true } + //nolint:revive,exhaustive if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: From c72d460b85c2ad0819f6457561c561392da6a46a Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 00:50:56 +0400 Subject: [PATCH 34/38] feat(fields): `fields.Iterate` refactor --- fields/iterate_test.go | 7 ++ internal/reflect/common.go | 65 +++++++++++++++ internal/reflect/get_set.go | 158 +----------------------------------- internal/reflect/iterate.go | 125 ++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 157 deletions(-) create mode 100644 internal/reflect/common.go create mode 100644 internal/reflect/iterate.go diff --git a/fields/iterate_test.go b/fields/iterate_test.go index b07a615..3bb1439 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -327,6 +327,13 @@ func TestIterate(t *testing.T) { XX: newXXWithBlankValues(t, 7, "seven"), }, }, + { + name: "invalid input", + options: nil, + input: 100, + output: nil, + error: "some error", + }, } for _, s := range scenarios { diff --git a/internal/reflect/common.go b/internal/reflect/common.go new file mode 100644 index 0000000..be81569 --- /dev/null +++ b/internal/reflect/common.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package reflect + +import ( + "reflect" +) + +func valueToReducedStructChain(strct any) (reflect.Value, kindChain, error) { + reflectVal := reflect.ValueOf(strct) + + chain, err := ValueToKindChain(reflectVal) + if err != nil { + return reflect.Value{}, nil, err + } + + /* + removes prepending duplicate [reflect.Ptr] & [reflect.Interface] elements + e.g.: + s := &struct{ val int }{} + Set(&s, ... // chain == {Ptr, Ptr, Struct} + + or: + var s any = &struct{ val int }{} + var s2 any = &s + var s3 any = &s + Set(&s3, ... // chain == {Ptr, Interface, Ptr, Interface, Ptr, Interface, Struct} + */ + for { + switch { + case chain.Prefixed(reflect.Ptr, reflect.Ptr): + reflectVal = reflectVal.Elem() + chain = chain[1:] + + continue + case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): + reflectVal = reflectVal.Elem().Elem() + chain = chain[2:] + + continue + } + + break + } + + return reflectVal, chain, nil +} diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 80fb958..38679c3 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -112,131 +112,6 @@ func Get(strct any, field string) (_ any, err error) { //nolint:cyclop,ireturn return f.Interface(), nil } -type FieldCallback = func(_ reflect.StructField, value any) (_ any, set bool) - -func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr bool) (err error) { - strType := "" - - defer func() { - if err != nil { - if strType != "" { - err = fmt.Errorf("%s: %w", strType, err) - } - err = fmt.Errorf("IterateFields: %w", err) - } - }() - - reflectVal := reflect.ValueOf(strct) - if !reflectVal.IsValid() { - return fmt.Errorf("expected struct, %T given", strct) - } - - chain, err := ValueToKindChain(reflectVal) - if err != nil { - return err - } - - if chain[len(chain)-1] != reflect.Struct { - return fmt.Errorf("expected struct, %T given", strct) - } - - // see [Set] - for { - switch { - case chain.Prefixed(reflect.Ptr, reflect.Ptr): - reflectVal = reflectVal.Elem() - chain = chain[1:] - - continue - case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): - reflectVal = reflectVal.Elem().Elem() - chain = chain[2:] - - continue - } - - break - } - - valueFromField := func(strct reflect.Value, i int) any { - f := strct.Field(i) - - if !f.CanSet() { // handle unexported fields - if !f.CanAddr() { - tmpReflectVal := reflect.New(strct.Type()).Elem() - tmpReflectVal.Set(strct) - f = tmpReflectVal.Field(i) - } - - f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() - } - - return f.Interface() - } - - switch { - case chain.equalTo(reflect.Struct): - strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) - for i := 0; i < reflectVal.Type().NumField(); i++ { - if _, set := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); set { - return fmt.Errorf("pointer is required to set fields") - } - } - - case chain.equalTo(reflect.Ptr, reflect.Struct): - strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) - for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { - if newVal, set := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); set { - f := reflectVal.Elem().Field(i) - if !f.CanSet() { - f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() - } - - newRefVal, err := func() (reflect.Value, error) { - if convertToPtr && f.Kind() == reflect.Ptr && (newVal != nil || reflect.ValueOf(newVal).Kind() != reflect.Ptr) { - val, err := ValueOf(newVal, f.Type().Elem(), convert) - if err != nil { - return reflect.Value{}, err - } - - ptr := reflect.New(val.Type()) - ptr.Elem().Set(val) - - return ptr, nil - } - - return ValueOf(newVal, f.Type(), convert) - }() - - if err != nil { - return fmt.Errorf("field %d %+q: %w", i, reflectVal.Elem().Type().Field(i).Name, err) - } - - f.Set(newRefVal) - } - } - - case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): - strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) - v := reflectVal.Elem() - tmp := reflect.New(v.Elem().Type()) - tmp.Elem().Set(v.Elem()) - if err := IterateFields(tmp.Interface(), callback, convert, convertToPtr); err != nil { - return err - } - v.Set(tmp.Elem()) - - default: - if err := ptrToNilStructError(strct); err != nil { - return err - } - - return fmt.Errorf("expected struct or pointer to struct, %T given", strct) - } - - return nil -} - //nolint:cyclop func Set(strct any, field string, val any, convert bool) (err error) { defer func() { @@ -249,42 +124,11 @@ func Set(strct any, field string, val any, convert bool) (err error) { return err } - reflectVal := reflect.ValueOf(strct) - - chain, err := ValueToKindChain(reflectVal) + reflectVal, chain, err := valueToReducedStructChain(strct) if err != nil { return err } - /* - removes prepending duplicate Ptr & Interface elements - e.g.: - s := &struct{ val int }{} - Set(&s, ... // chain == {Ptr, Ptr, Struct} - - or: - var s any = &struct{ val int }{} - var s2 any = &s - var s3 any = &s - Set(&s3, ... // chain == {Ptr, Interface, Ptr, Interface, Ptr, Interface, Struct} - */ - for { - switch { - case chain.Prefixed(reflect.Ptr, reflect.Ptr): - reflectVal = reflectVal.Elem() - chain = chain[1:] - - continue - case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): - reflectVal = reflectVal.Elem().Elem() - chain = chain[2:] - - continue - } - - break - } - switch { // s := struct{ val int }{} // Set(&s... diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go new file mode 100644 index 0000000..0ea15c3 --- /dev/null +++ b/internal/reflect/iterate.go @@ -0,0 +1,125 @@ +// Copyright (c) 2023–present Bartłomiej Krukowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package reflect + +import ( + "fmt" + "reflect" + "unsafe" +) + +type FieldCallback = func(_ reflect.StructField, value any) (_ any, set bool) + +func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr bool) (err error) { + strType := "" + + defer func() { + if err != nil { + if strType != "" { + err = fmt.Errorf("%s: %w", strType, err) + } + err = fmt.Errorf("IterateFields: %w", err) + } + }() + + reflectVal, chain, err := valueToReducedStructChain(strct) + if err != nil { + return err + } + + valueFromField := func(strct reflect.Value, i int) any { + f := strct.Field(i) + + if !f.CanSet() { // handle unexported fields + if !f.CanAddr() { + tmpReflectVal := reflect.New(strct.Type()).Elem() + tmpReflectVal.Set(strct) + f = tmpReflectVal.Field(i) + } + + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + } + + return f.Interface() + } + + switch { + case chain.equalTo(reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + for i := 0; i < reflectVal.Type().NumField(); i++ { + if _, set := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); set { + return fmt.Errorf("pointer is required to set fields") + } + } + + case chain.equalTo(reflect.Ptr, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) + for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { + if newVal, set := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); set { + f := reflectVal.Elem().Field(i) + if !f.CanSet() { + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + } + + newRefVal, err := func() (reflect.Value, error) { + if convertToPtr && f.Kind() == reflect.Ptr && (newVal != nil || reflect.ValueOf(newVal).Kind() != reflect.Ptr) { + val, err := ValueOf(newVal, f.Type().Elem(), convert) + if err != nil { + return reflect.Value{}, err + } + + ptr := reflect.New(val.Type()) + ptr.Elem().Set(val) + + return ptr, nil + } + + return ValueOf(newVal, f.Type(), convert) + }() + + if err != nil { + return fmt.Errorf("field %d %+q: %w", i, reflectVal.Elem().Type().Field(i).Name, err) + } + + f.Set(newRefVal) + } + } + + case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + v := reflectVal.Elem() + tmp := reflect.New(v.Elem().Type()) + tmp.Elem().Set(v.Elem()) + if err := IterateFields(tmp.Interface(), callback, convert, convertToPtr); err != nil { + return err + } + v.Set(tmp.Elem()) + + default: + if err := ptrToNilStructError(strct); err != nil { + return err + } + + return fmt.Errorf("expected struct or pointer to struct, %T given", strct) + } + + return nil +} From e8e754d618191497fc9f8df36b04b2df67593855 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 00:51:47 +0400 Subject: [PATCH 35/38] feat(fields): `fields.Iterate` refactor --- internal/reflect/common.go | 2 +- internal/reflect/get_set.go | 2 +- internal/reflect/iterate.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/reflect/common.go b/internal/reflect/common.go index be81569..064e892 100644 --- a/internal/reflect/common.go +++ b/internal/reflect/common.go @@ -24,7 +24,7 @@ import ( "reflect" ) -func valueToReducedStructChain(strct any) (reflect.Value, kindChain, error) { +func reducedStructValueOf(strct any) (reflect.Value, kindChain, error) { reflectVal := reflect.ValueOf(strct) chain, err := ValueToKindChain(reflectVal) diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 38679c3..2901bec 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -124,7 +124,7 @@ func Set(strct any, field string, val any, convert bool) (err error) { return err } - reflectVal, chain, err := valueToReducedStructChain(strct) + reflectVal, chain, err := reducedStructValueOf(strct) if err != nil { return err } diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 0ea15c3..3cd25bb 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -40,7 +40,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr } }() - reflectVal, chain, err := valueToReducedStructChain(strct) + reflectVal, chain, err := reducedStructValueOf(strct) if err != nil { return err } From 7479a405da68bb2997f6491ad946e24e6fbfb250 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 00:52:57 +0400 Subject: [PATCH 36/38] feat(fields): `fields.Iterate` refactor --- fields/iterate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 3bb1439..91a8dac 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -332,7 +332,7 @@ func TestIterate(t *testing.T) { options: nil, input: 100, output: nil, - error: "some error", + error: "fields.Iterate: IterateFields: expected struct or pointer to struct, *interface {} given", }, } From 050df5bc8bed05a714489fbff7581fe3f2a72ca1 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 00:54:31 +0400 Subject: [PATCH 37/38] feat(fields): `fields.Iterate` refactor --- fields/iterate_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 91a8dac..e516c28 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -281,7 +281,7 @@ func TestIterate(t *testing.T) { return &XX{}, true } - //nolint:revive,exhaustive + //nolint:exhaustive if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: @@ -308,7 +308,7 @@ func TestIterate(t *testing.T) { return &XX{}, true } - //nolint:revive,exhaustive + //nolint:exhaustive if path.EqualNames("XX", "_") { switch path[len(path)-1].Type.Kind() { case reflect.Int: From be9f1977adae682e91dccb246f4289fe0878e175 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:11:21 +0400 Subject: [PATCH 38/38] feat(fields): `fields.Iterate` refactor --- internal/reflect/iterate.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 3cd25bb..25840e2 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -64,6 +64,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr switch { case chain.equalTo(reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + for i := 0; i < reflectVal.Type().NumField(); i++ { if _, set := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)); set { return fmt.Errorf("pointer is required to set fields") @@ -72,6 +73,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr case chain.equalTo(reflect.Ptr, reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) + for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { if newVal, set := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)); set { f := reflectVal.Elem().Field(i) @@ -108,9 +110,11 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr v := reflectVal.Elem() tmp := reflect.New(v.Elem().Type()) tmp.Elem().Set(v.Elem()) + if err := IterateFields(tmp.Interface(), callback, convert, convertToPtr); err != nil { return err } + v.Set(tmp.Elem()) default: