From 5fa23d3feffe8af0db77e7251b11a2c81efa77cc Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:14:45 +0400 Subject: [PATCH 01/12] feat(fields): read/set all the fields from the given struct --- fields/any.go | 23 ++ fields/any_test.go | 23 ++ fields/examples_test.go | 270 +++++++++++++++++++++++ fields/iterate.go | 178 +++++++++++++++ fields/iterate_test.go | 360 +++++++++++++++++++++++++++++++ fields/path.go | 74 +++++++ go.mod | 4 + internal/reflect/common.go | 65 ++++++ internal/reflect/get_set.go | 33 +-- internal/reflect/get_set_test.go | 79 +++++++ internal/reflect/iterate.go | 129 +++++++++++ 11 files changed, 1206 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 create mode 100644 fields/path.go create mode 100644 internal/reflect/common.go create mode 100644 internal/reflect/iterate.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..bec4599 --- /dev/null +++ b/fields/examples_test.go @@ -0,0 +1,270 @@ +// 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 ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/gontainer/reflectpro/copier" + "github.com/gontainer/reflectpro/fields" +) + +type Exercise struct { + Name string +} + +type TrainingPlanMeta struct { + Name string +} + +type TrainingPlan struct { + TrainingPlanMeta + + Monday Exercise + Tuesday Exercise +} + +func ExampleIterate_set() { + p := TrainingPlan{} + + _ = fields.Iterate( + &p, + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { + switch { + case path.EqualNames("TrainingPlanMeta", "Name"): + return "My training plan", true + case path.EqualNames("Monday", "Name"): + return "pushups", true + case path.EqualNames("Tuesday", "name"): + return "pullups", true + } + + return nil, false + }), + fields.Recursive(true), + ) + + 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 ExampleIterate_setUnexported() { + p := Phone{} + _ = fields.Iterate( + &p, + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { + if path.EqualNames("os") { + return "Android", true + } + + return nil, false + }), + ) + + fmt.Println(p.os) + + // Output: + // Android +} + +type MyCache struct { + TTL time.Duration +} + +type MyConfig struct { + MyCache *MyCache +} + +func ExamplePrefillNilStructs() { + cfg := MyConfig{} + + /* + `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{} + */ + + _ = fields.Iterate( + &cfg, + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { + if path.EqualNames("MyCache", "TTL") { + return time.Minute, true + } + + return nil, false + }), + fields.PrefillNilStructs(true), + fields.Recursive(true), + ) + + fmt.Println(cfg.MyCache.TTL) + + // Output: + // 1m0s +} + +type CTO struct { + Salary int +} + +type Company struct { + CTO CTO +} + +func ExampleIterate_get() { + c := Company{ + CTO: CTO{ + Salary: 1000000, + }, + } + + var salary int + + _ = fields.Iterate( + c, + fields.Getter(func(p fields.Path, value any) { + if p.EqualNames("CTO", "Salary") { + _ = copier.Copy(value, &salary, false) + } + }), + fields.Recursive(true), + ) + + fmt.Println(salary) + + // Output: + // 1000000 +} + +func ExampleConvertToPointers() { + var cfg struct { + TTL *time.Duration // expect a pointer + } + + _ = fields.Iterate( + &cfg, + fields.Setter(func(path fields.Path, _ any) (_ any, set bool) { + if path.EqualNames("TTL") { + return time.Minute, true // return a value + } + + return nil, false + }), + fields.ConvertToPointers(true), // this line will instruct the library to convert values to pointers + ) + + fmt.Println(*cfg.TTL) + + // Output: + // 1m0s +} + +func ExampleReadJSON() { + var person struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Age uint `json:"age"` + Bio string `json:"-"` + } + + // read data from JSON... + js := ` +{ + "firstname": "Jane", + "lastname": "Doe", + "age": 30, + "bio": "bio..." +}` + 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, _ any) (_ any, set 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(true), + ) + + fmt.Printf("%+v\n", person) + + // Output: + // {Firstname:Jane Lastname:Doe Age:30 Bio:} +} + +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 + } + + return nil, false + })) + + fmt.Println(data) + + // Output: + // {0} + // {10} +} diff --git a/fields/iterate.go b/fields/iterate.go new file mode 100644 index 0000000..42f9ea6 --- /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, value any) (_ any, ok bool) + getter func(_ Path, 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 Path, value any) (_ any, set bool)) Option { + return func(c *config) { + c.setter = fn + } +} + +func Getter(fn func(_ Path, value any)) Option { + return func(c *config) { + c.getter = fn + } +} + +func ConvertTypes(v bool) Option { + return func(c *config) { + c.convertTypes = v + } +} + +func ConvertToPointers(v bool) Option { + return func(c *config) { + c.convertToPtr = v + } +} + +func Recursive(v bool) Option { + return func(c *config) { + c.recursive = v + } +} + +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) +} + +//nolint:gocognit +func iterate(strct any, cfg *config, path []reflect.StructField) error { + var fn intReflect.FieldCallback + + var finalErr error + + fn = func(f reflect.StructField, value any) (_ any, set bool) { + if finalErr != 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 pointer to a zero-value + if !setterHasBeenTriggered && + cfg.prefillNilStructs && + f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && + reflect.ValueOf(value).IsZero() { + 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 + + original := value + + if err := iterate(&value, cfg, append(path, f)); err != nil { + finalErr = fmt.Errorf("%s: %w", f.Name, 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 finalErr != nil { + return finalErr + } + + return nil +} diff --git a/fields/iterate_test.go b/fields/iterate_test.go new file mode 100644 index 0000000..e516c28 --- /dev/null +++ b/fields/iterate_test.go @@ -0,0 +1,360 @@ +// 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 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 +} + +//nolint:gocognit,goconst,lll +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(_ fields.Path, _ any) (_ any, set bool) { + return "Jane", true + }), + }, + input: Person{}, + output: Person{ + Name: "Jane", + }, + error: "", + }, + { + name: "Person OK (convert types)", + options: []fields.Option{ + fields.Setter(func(_ fields.Path, _ any) (_ any, set bool) { + return CustomString("Jane"), true + }), + fields.ConvertTypes(true), + }, + input: Person{}, + output: Person{ + Name: "Jane", + }, + error: "", + }, + { + name: "Person error (convert types)", + options: []fields.Option{ + fields.Setter(func(_ fields.Path, value any) (_ any, set 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 fields.Path, value any) (_ any, set bool) { + if path.EqualNames("B", "C", "D") { + return "Hello", true + } + + return nil, false + }), + fields.Recursive(true), + }, + 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 fields.Path, value any) (_ any, set bool) { + if path.EqualNames("B", "C", "D") { + return 5, true + } + + return nil, false + }), + fields.Recursive(true), + }, + input: A{}, + output: A{}, + 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)", + options: []fields.Option{ + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { + switch { + case path.EqualNames("Person", "Name"): + return "Jane", true + case path.EqualNames("Role"): + return "Lead", true + } + + return nil, false + }), + fields.Recursive(true), + }, + input: Employee{}, + output: Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, + error: "", + }, + { + name: "Team #1", + options: []fields.Option{ + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { + switch { + case path.EqualNames("Lead", "Person", "Name"): + return "Jane", true + case path.EqualNames("Lead", "Role"): + return "Lead", true + case path.EqualNames("TeamMeta", "Name"): + return "Hawkeye", true + } + + return nil, false + }), + fields.Recursive(true), + }, + 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 fields.Path, value any) (_ any, set bool) { + switch { + case path.EqualNames("Lead", "Role"): + return "Lead", true + case path.EqualNames("Lead"): + return Employee{ + Person: Person{ + Name: "Jane", + }, + Role: "Lead", + }, true + case path.EqualNames("TeamMeta", "Name"): + return "Hawkeye", true + } + + return nil, false + }), + fields.Recursive(true), + }, + 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 fields.Path, value any) (_ any, set bool) { + if path.EqualNames("XX") { + return &XX{}, true + } + + //nolint:exhaustive + if path.EqualNames("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(true), + }, + input: YY{}, + output: YY{ + XX: newXXWithBlankValues(t, 5, "five"), + }, + }, + { + name: "YY", + options: []fields.Option{ + fields.Setter(func(path fields.Path, value any) (_ any, set bool) { + if path.EqualNames("XX") { + return &XX{}, true + } + + //nolint:exhaustive + if path.EqualNames("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(true), + }, + input: YY{}, + output: YY{ + XX: newXXWithBlankValues(t, 7, "seven"), + }, + }, + { + name: "invalid input", + options: nil, + input: 100, + output: nil, + error: "fields.Iterate: IterateFields: expected struct or pointer to struct, *interface {} given", + }, + } + + 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/fields/path.go b/fields/path.go new file mode 100644 index 0000000..0b46b8c --- /dev/null +++ b/fields/path.go @@ -0,0 +1,74 @@ +// 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 ( + "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 { + 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) EqualNames(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 +} diff --git a/go.mod b/go.mod index 4aaa2f7..6b3b5ec 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,7 @@ require ( github.com/gontainer/grouperror v1.0.1 github.com/stretchr/testify v1.8.2 ) + +require ( // tests + github.com/davecgh/go-spew v1.1.1 +) diff --git a/internal/reflect/common.go b/internal/reflect/common.go new file mode 100644 index 0000000..064e892 --- /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 reducedStructValueOf(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 ee5c9a8..2901bec 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -124,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 := reducedStructValueOf(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/get_set_test.go b/internal/reflect/get_set_test.go index 212044a..df04cb3 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,79 @@ 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 any) (_ 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, + }, + }, + { + strct: person{}, + callback: func(f stdReflect.StructField, value any) (_ 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 { + 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) + }) + } + }) +} diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go new file mode 100644 index 0000000..25840e2 --- /dev/null +++ b/internal/reflect/iterate.go @@ -0,0 +1,129 @@ +// 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 := reducedStructValueOf(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 6f04fcba655bad9eb752f40aae123c146ed5f8f9 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:17:59 +0400 Subject: [PATCH 02/12] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/get_set_test.go | 79 ----------------------------- internal/reflect/iterate_test.go | 87 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 79 deletions(-) create mode 100644 internal/reflect/iterate_test.go diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index df04cb3..212044a 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -21,13 +21,10 @@ 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) { @@ -441,79 +438,3 @@ 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 any) (_ 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, - }, - }, - { - strct: person{}, - callback: func(f stdReflect.StructField, value any) (_ 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 { - 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) - }) - } - }) -} diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go new file mode 100644 index 0000000..7473e2f --- /dev/null +++ b/internal/reflect/iterate_test.go @@ -0,0 +1,87 @@ +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 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 any) (_ 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, + }, + }, + { + strct: person{}, + callback: func(f stdReflect.StructField, value any) (_ 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 { + 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 6a53f5dace9948182683a0f0fc83a76507c7b35a Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:20:49 +0400 Subject: [PATCH 03/12] feat(fields): read/set all the fields from the given struct docs --- internal/reflect/iterate.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 25840e2..fa99a16 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -28,6 +28,12 @@ import ( type FieldCallback = func(_ reflect.StructField, value any) (_ any, set bool) +// IterateFields traverses the fields of a struct, applying the callback function. +// Parameters: +// - strct: The struct to iterate over +// - callback: Function to call for each field +// - convert: If true, attempts type conversion +// - convertToPtr: If true, converts values returned by the callback to pointers when required func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr bool) (err error) { strType := "" From dccfac5f2c02a5bdbe82cf397aa68d07850fad4f Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:22:07 +0400 Subject: [PATCH 04/12] feat(fields): read/set all the fields from the given struct `make addlicense` --- internal/reflect/iterate_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go index 7473e2f..0a47376 100644 --- a/internal/reflect/iterate_test.go +++ b/internal/reflect/iterate_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 reflect_test import ( From e5c080721d6fec9366767a4e9be543190aebd6bd Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:34:22 +0400 Subject: [PATCH 05/12] feat(fields): read/set all the fields from the given struct reefactor --- fields/iterate.go | 62 +++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 42f9ea6..52c5ac0 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -99,7 +99,6 @@ 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 @@ -117,38 +116,19 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { setterHasBeenTriggered := false - // call setter - if cfg.setter != nil { - newVal, ok := cfg.setter(append(path, f), value) - if ok { - value, setterHasBeenTriggered = newVal, true - } - } - - // set pointer to a zero-value - if !setterHasBeenTriggered && - cfg.prefillNilStructs && - f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && - reflect.ValueOf(value).IsZero() { - value, setterHasBeenTriggered = reflect.New(f.Type.Elem()).Interface(), true - } + value, setterHasBeenTriggered = trySetValue(f, value, cfg, path) - //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 + if cfg.recursive && isStructOrNonNilStructPtr(f.Type, value) { + original := value - original := value + if err := iterate(&value, cfg, append(path, f)); err != nil { + finalErr = fmt.Errorf("%s: %w", f.Name, err) - if err := iterate(&value, cfg, append(path, f)); err != nil { - finalErr = fmt.Errorf("%s: %w", f.Name, err) - - return nil, false - } + return nil, false + } - if !reflect.DeepEqual(original, value) { - setterHasBeenTriggered = true - } + if !reflect.DeepEqual(original, value) { + setterHasBeenTriggered = true } } @@ -176,3 +156,27 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return nil } + +func trySetValue(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (_ any, set bool) { + // Call setter + if cfg.setter != nil { + if newVal, ok := cfg.setter(append(path, f), value); ok { + return newVal, true + } + } + + // Set pointer to a zero-value struct + if cfg.prefillNilStructs && + f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct && + reflect.ValueOf(value).IsZero() { + return reflect.New(f.Type.Elem()).Interface(), true + } + + return value, false +} + +// isStructOrNonNilStructPtr checks if the given type is a struct or a non-nil pointer to a struct. +func isStructOrNonNilStructPtr(t reflect.Type, v any) bool { + return t.Kind() == reflect.Struct || + (t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct && !reflect.ValueOf(v).IsZero()) +} From 54538f4e03eebe8a534faadfa558ba3d921b0759 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:37:09 +0400 Subject: [PATCH 06/12] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/iterate.go | 1 + internal/reflect/iterate_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index fa99a16..3b3d2c9 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -42,6 +42,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr if strType != "" { err = fmt.Errorf("%s: %w", strType, err) } + err = fmt.Errorf("IterateFields: %w", err) } }() diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go index 0a47376..aa3b071 100644 --- a/internal/reflect/iterate_test.go +++ b/internal/reflect/iterate_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/require" ) +//nolint:lll func TestIterateFields(t *testing.T) { t.Parallel() From 3309b699dce346314a7fae27ffea87cbe1d5a80a Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:39:41 +0400 Subject: [PATCH 07/12] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fields/iterate.go b/fields/iterate.go index 52c5ac0..419957c 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -45,9 +45,11 @@ func newConfig(opts ...Option) *config { convertToPtr: false, recursive: false, } + for _, o := range opts { o(c) } + return c } @@ -114,7 +116,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { cfg.getter(append(path, f), value) } - setterHasBeenTriggered := false + var setterHasBeenTriggered bool value, setterHasBeenTriggered = trySetValue(f, value, cfg, path) From 0dc3b9968d885812d34a3e831e27c88cb6013726 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:42:22 +0400 Subject: [PATCH 08/12] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fields/iterate.go b/fields/iterate.go index 419957c..fe7e613 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -101,6 +101,7 @@ func Iterate(strct any, opts ...Option) (err error) { return iterate(strct, newConfig(opts...), nil) } +//nolint:wrapcheck func iterate(strct any, cfg *config, path []reflect.StructField) error { var fn intReflect.FieldCallback @@ -159,6 +160,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return nil } +//nolint:ireturn func trySetValue(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (_ any, set bool) { // Call setter if cfg.setter != nil { From e2cdc729c55b9cfb125814643583054fe2a9e8b0 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:44:33 +0400 Subject: [PATCH 09/12] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 1 - 1 file changed, 1 deletion(-) diff --git a/fields/iterate.go b/fields/iterate.go index fe7e613..d2bba68 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -160,7 +160,6 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return nil } -//nolint:ireturn func trySetValue(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (_ any, set bool) { // Call setter if cfg.setter != nil { From 6faffdf9803ef58d99a95eb85bd33a8b9259aeec Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:46:28 +0400 Subject: [PATCH 10/12] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fields/iterate.go b/fields/iterate.go index d2bba68..9befe26 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -160,7 +160,15 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return nil } -func trySetValue(f reflect.StructField, value any, cfg *config, path []reflect.StructField) (_ any, set bool) { +func trySetValue( + f reflect.StructField, + value any, + cfg *config, + path []reflect.StructField, +) ( + _ any, + set bool, +) { // Call setter if cfg.setter != nil { if newVal, ok := cfg.setter(append(path, f), value); ok { From 8677e45eb0789972cfdf06bea314587afb3ebc22 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:49:28 +0400 Subject: [PATCH 11/12] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/iterate.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 3b3d2c9..8b0312b 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -52,22 +52,6 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr 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()) @@ -134,3 +118,19 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr return nil } + +func valueFromField(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() +} From 832c63fa21841d824b1be2d5c17d48704a29830d Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:50:59 +0400 Subject: [PATCH 12/12] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 2 +- internal/reflect/iterate.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 9befe26..525e496 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -160,7 +160,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return nil } -func trySetValue( +func trySetValue( //nolint:ireturn f reflect.StructField, value any, cfg *config, diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 8b0312b..815fdc2 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -119,7 +119,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr return nil } -func valueFromField(strct reflect.Value, i int) any { +func valueFromField(strct reflect.Value, i int) any { //nolint:ireturn f := strct.Field(i) if !f.CanSet() { // handle unexported fields