From 4e1889ebce867de39001ede06826e0d982809eb2 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:54:36 +0400 Subject: [PATCH 01/15] 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 | 193 +++++++++++++++++ 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/iterate.go | 136 ++++++++++++ internal/reflect/iterate_test.go | 108 ++++++++++ 11 files changed, 1257 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 create mode 100644 internal/reflect/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..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..525e496 --- /dev/null +++ b/fields/iterate.go @@ -0,0 +1,193 @@ +// 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:wrapcheck +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) + } + + var setterHasBeenTriggered bool + + value, setterHasBeenTriggered = trySetValue(f, value, cfg, path) + + if cfg.recursive && isStructOrNonNilStructPtr(f.Type, value) { + 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 +} + +func trySetValue( //nolint:ireturn + 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()) +} 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/iterate.go b/internal/reflect/iterate.go new file mode 100644 index 0000000..815fdc2 --- /dev/null +++ b/internal/reflect/iterate.go @@ -0,0 +1,136 @@ +// 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) + +// 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 := "" + + 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 + } + + 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 +} + +func valueFromField(strct reflect.Value, i int) any { //nolint:ireturn + 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() +} diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go new file mode 100644 index 0000000..aa3b071 --- /dev/null +++ b/internal/reflect/iterate_test.go @@ -0,0 +1,108 @@ +// 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 ( + "fmt" + stdReflect "reflect" + "testing" + + "github.com/gontainer/reflectpro/internal/reflect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//nolint:lll +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 80961a3a7ffc60b2755571a30488a484b7d5b3ba Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:57:40 +0400 Subject: [PATCH 02/15] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fields/iterate_test.go b/fields/iterate_test.go index e516c28..5fc50ed 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -80,6 +80,8 @@ func setValueByFieldIndex(ptrStruct any, fieldIndex int, value any) { } func newXXWithBlankValues(t *testing.T, first int, second string) *XX { + t.Helper() + x := XX{} setValueByFieldIndex(&x, 0, first) setValueByFieldIndex(&x, 1, second) From 7e627c2f4a9a3dc7202ab57be5f7e35157070938 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 01:58:28 +0400 Subject: [PATCH 03/15] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/fields/iterate_test.go b/fields/iterate_test.go index 5fc50ed..e516c28 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -80,8 +80,6 @@ func setValueByFieldIndex(ptrStruct any, fieldIndex int, value any) { } func newXXWithBlankValues(t *testing.T, first int, second string) *XX { - t.Helper() - x := XX{} setValueByFieldIndex(&x, 0, first) setValueByFieldIndex(&x, 1, second) From 6c9d077d507127b44e6451eb798a400087763f8a Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 02:02:40 +0400 Subject: [PATCH 04/15] feat(fields): read/set all the fields from the given struct refactor --- fields/examples_test.go | 3 ++- fields/iterate_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fields/examples_test.go b/fields/examples_test.go index bec4599..c80ef74 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -198,7 +198,7 @@ func ExampleConvertToPointers() { // 1m0s } -func ExampleReadJSON() { +func Example_readJSON() { var person struct { Firstname string `json:"firstname"` Lastname string `json:"lastname"` @@ -215,6 +215,7 @@ func ExampleReadJSON() { "bio": "bio..." }` var data map[string]any + _ = json.Unmarshal([]byte(js), &data) // populate the data from JSON to the `person` variable, diff --git a/fields/iterate_test.go b/fields/iterate_test.go index e516c28..18c98b8 100644 --- a/fields/iterate_test.go +++ b/fields/iterate_test.go @@ -79,7 +79,7 @@ func setValueByFieldIndex(ptrStruct any, fieldIndex int, value any) { f.Set(reflect.ValueOf(value)) } -func newXXWithBlankValues(t *testing.T, first int, second string) *XX { +func newXXWithBlankValues(t *testing.T, first int, second string) *XX { //nolint:thelper x := XX{} setValueByFieldIndex(&x, 0, first) setValueByFieldIndex(&x, 1, second) From a4e80d80cd88e734d0d9f2402e4c64c8aa7220f1 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 02:30:28 +0400 Subject: [PATCH 05/15] feat(fields): read/set all the fields from the given struct refactor --- caller/internal/caller/call.go | 2 +- fields/iterate.go | 14 +++--- internal/reflect/common.go | 4 +- internal/reflect/get_set.go | 4 +- internal/reflect/get_set_test.go | 4 +- internal/reflect/iterate.go | 73 ++++++++++++++++++++++++++++++-- internal/reflect/iterate_test.go | 18 ++++---- setter/setter.go | 2 +- 8 files changed, 91 insertions(+), 30 deletions(-) diff --git a/caller/internal/caller/call.go b/caller/internal/caller/call.go index 4468fa1..a274847 100644 --- a/caller/internal/caller/call.go +++ b/caller/internal/caller/call.go @@ -152,7 +152,7 @@ func validateAndForceCallMethod( return nil, err } - // see [intReflect.Set] + // see [intReflect.set] for { switch { case chain.Prefixed(reflect.Ptr, reflect.Ptr): diff --git a/fields/iterate.go b/fields/iterate.go index 525e496..65a9352 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -107,11 +107,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { var finalErr error - fn = func(f reflect.StructField, value any) (_ any, set bool) { - if finalErr != nil { - return nil, false - } - + fn = func(f reflect.StructField, value any) intReflect.FieldCallbackResult { // call getter if cfg.getter != nil { cfg.getter(append(path, f), value) @@ -127,7 +123,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { if err := iterate(&value, cfg, append(path, f)); err != nil { finalErr = fmt.Errorf("%s: %w", f.Name, err) - return nil, false + return intReflect.FieldCallbackResultStop() } if !reflect.DeepEqual(original, value) { @@ -136,10 +132,10 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { } if setterHasBeenTriggered { - return value, true + return intReflect.FieldCallbackResultSet(value) } - return nil, false + return intReflect.FieldCallbackResultDontSet() } err := intReflect.IterateFields( @@ -176,7 +172,7 @@ func trySetValue( //nolint:ireturn } } - // Set pointer to a zero-value struct + // 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() { diff --git a/internal/reflect/common.go b/internal/reflect/common.go index 064e892..f5432d1 100644 --- a/internal/reflect/common.go +++ b/internal/reflect/common.go @@ -36,13 +36,13 @@ func reducedStructValueOf(strct any) (reflect.Value, kindChain, error) { removes prepending duplicate [reflect.Ptr] & [reflect.Interface] elements e.g.: s := &struct{ val int }{} - Set(&s, ... // chain == {Ptr, Ptr, Struct} + 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} + set(&s3, ... // chain == {Ptr, Interface, Ptr, Interface, Ptr, Interface, Struct} */ for { switch { diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 2901bec..50c15d8 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -131,7 +131,7 @@ func Set(strct any, field string, val any, convert bool) (err error) { switch { // s := struct{ val int }{} - // Set(&s... + // set(&s... case chain.equalTo(reflect.Ptr, reflect.Struct): return setOnValue( reflectVal.Elem(), @@ -141,7 +141,7 @@ func Set(strct any, field string, val any, convert bool) (err error) { ) // var s any = struct{ val int }{} - // Set(&s... + // set(&s... case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): v := reflectVal.Elem() tmp := reflect.New(v.Elem().Type()).Elem() diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index 212044a..8cb4bb6 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -272,7 +272,7 @@ func TestSet(t *testing.T) { assert.NoError(t, reflect.Set(&p, "Name", "Jane", false)) assert.Equal(t, person{Name: "Jane"}, p) }) - t.Run("var a any = struct{}; a2 := &a; setter.Set(&a2...", func(t *testing.T) { + t.Run("var a any = struct{}; a2 := &a; setter.set(&a2...", func(t *testing.T) { t.Parallel() var p any = person{} @@ -280,7 +280,7 @@ func TestSet(t *testing.T) { assert.NoError(t, reflect.Set(&p2, "Name", "Jane", false)) assert.Equal(t, person{Name: "Jane"}, p) }) - t.Run("var a1 any = struct{}; var a2 any = &a1; var a3 any = &a2; ...; setter.Set(&aN...", func(t *testing.T) { + t.Run("var a1 any = struct{}; var a2 any = &a1; var a3 any = &a2; ...; setter.set(&aN...", func(t *testing.T) { t.Parallel() var p any = person{} diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 815fdc2..58366fd 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -26,7 +26,37 @@ import ( "unsafe" ) -type FieldCallback = func(_ reflect.StructField, value any) (_ any, set bool) +type FieldCallbackResult struct { + value any + set bool + continue_ bool +} + +func FieldCallbackResultSet(value any) FieldCallbackResult { + return FieldCallbackResult{ + value: value, + set: true, + continue_: true, + } +} + +func FieldCallbackResultDontSet() FieldCallbackResult { + return FieldCallbackResult{ + value: nil, + set: false, + continue_: true, + } +} + +func FieldCallbackResultStop() FieldCallbackResult { + return FieldCallbackResult{ + value: nil, + set: false, + continue_: false, + } +} + +type FieldCallback = func(_ reflect.StructField, value any) FieldCallbackResult // IterateFields traverses the fields of a struct, applying the callback function. // Parameters: @@ -57,21 +87,31 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr 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 { + result := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)) + + if result.set { return fmt.Errorf("pointer is required to set fields") } + + if !result.continue_ { + return nil + } } 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 { + result := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)) + + if result.set { f := reflectVal.Elem().Field(i) if !f.CanSet() { f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() } + newVal := result.value + 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) @@ -94,6 +134,10 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr f.Set(newRefVal) } + + if !result.continue_ { + return nil + } } case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): @@ -102,7 +146,28 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr tmp := reflect.New(v.Elem().Type()) tmp.Elem().Set(v.Elem()) - if err := IterateFields(tmp.Interface(), callback, convert, convertToPtr); err != nil { + // TODO find a better solution + stop := false + + newCallback := func(f reflect.StructField, value any) FieldCallbackResult { + if stop { + return FieldCallbackResult{ + value: nil, + set: false, + continue_: false, + } + } + + result := callback(f, value) + + if !result.continue_ { + stop = true + } + + return result + } + + if err := IterateFields(tmp.Interface(), newCallback, convert, convertToPtr); err != nil { return err } diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go index aa3b071..f385d71 100644 --- a/internal/reflect/iterate_test.go +++ b/internal/reflect/iterate_test.go @@ -34,7 +34,7 @@ import ( func TestIterateFields(t *testing.T) { t.Parallel() - t.Run("Set", func(t *testing.T) { + t.Run("set", func(t *testing.T) { t.Parallel() scenarios := []struct { @@ -48,16 +48,16 @@ func TestIterateFields(t *testing.T) { }{ { strct: person{}, - callback: func(f stdReflect.StructField, value any) (_ any, set bool) { + callback: func(f stdReflect.StructField, value any) reflect.FieldCallbackResult { if f.Name == "Name" { - return "Jane", true + return reflect.FieldCallbackResultSet("Jane") } if f.Name == "age" { - return uint(30), true + return reflect.FieldCallbackResultSet(uint(30)) } - return nil, false + return reflect.FieldCallbackResultDontSet() }, convert: true, convertToPtr: false, @@ -68,16 +68,16 @@ func TestIterateFields(t *testing.T) { }, { strct: person{}, - callback: func(f stdReflect.StructField, value any) (_ any, set bool) { + callback: func(f stdReflect.StructField, value any) reflect.FieldCallbackResult { if f.Name == "Name" { - return "Jane", true + return reflect.FieldCallbackResultSet("Jane") } if f.Name == "age" { - return uint(30), true + return reflect.FieldCallbackResultSet(uint(30)) } - return nil, false + return reflect.FieldCallbackResultDontSet() }, convert: false, convertToPtr: false, diff --git a/setter/setter.go b/setter/setter.go index 6d9c0b1..f95184c 100644 --- a/setter/setter.go +++ b/setter/setter.go @@ -33,7 +33,7 @@ Unexported fields are supported. Name string } p := Person{} - _ = setter.Set(&p, "Name", "Jane", false) + _ = setter.set(&p, "Name", "Jane", false) fmt.Println(p) // {Jane} */ func Set(strct any, field string, val any, convert bool) error { From 6b56d7e1abf631f4ca2d40e194bfcdcd8d90b090 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 02:32:09 +0400 Subject: [PATCH 06/15] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/iterate.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 58366fd..588f1c5 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -27,32 +27,32 @@ import ( ) type FieldCallbackResult struct { - value any - set bool - continue_ bool + value any + set bool + stop bool } func FieldCallbackResultSet(value any) FieldCallbackResult { return FieldCallbackResult{ - value: value, - set: true, - continue_: true, + value: value, + set: true, + stop: false, } } func FieldCallbackResultDontSet() FieldCallbackResult { return FieldCallbackResult{ - value: nil, - set: false, - continue_: true, + value: nil, + set: false, + stop: false, } } func FieldCallbackResultStop() FieldCallbackResult { return FieldCallbackResult{ - value: nil, - set: false, - continue_: false, + value: nil, + set: false, + stop: true, } } @@ -93,7 +93,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr return fmt.Errorf("pointer is required to set fields") } - if !result.continue_ { + if result.stop { return nil } } @@ -135,7 +135,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr f.Set(newRefVal) } - if !result.continue_ { + if result.stop { return nil } } @@ -152,15 +152,15 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr newCallback := func(f reflect.StructField, value any) FieldCallbackResult { if stop { return FieldCallbackResult{ - value: nil, - set: false, - continue_: false, + value: nil, + set: false, + stop: true, } } result := callback(f, value) - if !result.continue_ { + if result.stop { stop = true } From c75f1dc6ea634938e7d46749471c06d8ca623ea0 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 02:38:17 +0400 Subject: [PATCH 07/15] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/common.go | 4 ++-- internal/reflect/get_set.go | 3 ++- internal/reflect/iterate.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/reflect/common.go b/internal/reflect/common.go index f5432d1..32d7fb0 100644 --- a/internal/reflect/common.go +++ b/internal/reflect/common.go @@ -24,8 +24,8 @@ import ( "reflect" ) -func reducedStructValueOf(strct any) (reflect.Value, kindChain, error) { - reflectVal := reflect.ValueOf(strct) +func ReducedValueOf(val any) (reflect.Value, kindChain, error) { + reflectVal := reflect.ValueOf(val) chain, err := ValueToKindChain(reflectVal) if err != nil { diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index 50c15d8..a2cf37f 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 := reducedStructValueOf(strct) + reflectVal, chain, err := ReducedValueOf(strct) if err != nil { return err } @@ -213,6 +213,7 @@ func ValueToKindChain(v reflect.Value) (kindChain, error) { //nolint // unexport r = make(kindChain, 0, 5) //nolint:gomnd ptrs := make(map[uintptr]struct{}) + // TODO use ReducedValueOf for { if v.Kind() == reflect.Ptr && !v.IsNil() { ptr := v.Elem().UnsafeAddr() diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 588f1c5..0108f23 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -77,7 +77,7 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr } }() - reflectVal, chain, err := reducedStructValueOf(strct) + reflectVal, chain, err := ReducedValueOf(strct) if err != nil { return err } From b3e3b53c2f6ec03131f4f429f8abbbea7b76d1f5 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 02:59:12 +0400 Subject: [PATCH 08/15] feat(fields): read/set all the fields from the given struct refactor --- caller/internal/caller/call.go | 2 +- internal/reflect/common.go | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/caller/internal/caller/call.go b/caller/internal/caller/call.go index a274847..8ed5c78 100644 --- a/caller/internal/caller/call.go +++ b/caller/internal/caller/call.go @@ -147,7 +147,7 @@ func validateAndForceCallMethod( return nil, fmt.Errorf("expected %s, %T given", reflect.Ptr.String(), object) } - chain, err := intReflect.ValueToKindChain(val) + val, chain, err := intReflect.ReducedValue(val) if err != nil { return nil, err } diff --git a/internal/reflect/common.go b/internal/reflect/common.go index 32d7fb0..35462ad 100644 --- a/internal/reflect/common.go +++ b/internal/reflect/common.go @@ -24,10 +24,8 @@ import ( "reflect" ) -func ReducedValueOf(val any) (reflect.Value, kindChain, error) { - reflectVal := reflect.ValueOf(val) - - chain, err := ValueToKindChain(reflectVal) +func ReducedValue(val reflect.Value) (reflect.Value, kindChain, error) { + chain, err := ValueToKindChain(val) if err != nil { return reflect.Value{}, nil, err } @@ -47,12 +45,12 @@ func ReducedValueOf(val any) (reflect.Value, kindChain, error) { for { switch { case chain.Prefixed(reflect.Ptr, reflect.Ptr): - reflectVal = reflectVal.Elem() + val = val.Elem() chain = chain[1:] continue case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): - reflectVal = reflectVal.Elem().Elem() + val = val.Elem().Elem() chain = chain[2:] continue @@ -61,5 +59,9 @@ func ReducedValueOf(val any) (reflect.Value, kindChain, error) { break } - return reflectVal, chain, nil + return val, chain, nil +} + +func ReducedValueOf(val any) (reflect.Value, kindChain, error) { + return ReducedValue(reflect.ValueOf(val)) } From aa03e943746e64fa449345447a9f9005f40c488d Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:02:14 +0400 Subject: [PATCH 09/15] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 65a9352..9fae0df 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -113,9 +113,9 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { cfg.getter(append(path, f), value) } - var setterHasBeenTriggered bool + var valueHasChanged bool - value, setterHasBeenTriggered = trySetValue(f, value, cfg, path) + value, valueHasChanged = trySetValue(f, value, cfg, path) if cfg.recursive && isStructOrNonNilStructPtr(f.Type, value) { original := value @@ -126,12 +126,10 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return intReflect.FieldCallbackResultStop() } - if !reflect.DeepEqual(original, value) { - setterHasBeenTriggered = true - } + valueHasChanged = valueHasChanged || !reflect.DeepEqual(original, value) } - if setterHasBeenTriggered { + if valueHasChanged { return intReflect.FieldCallbackResultSet(value) } From d0fa47e7610c0b01e7bf978f7e8db655832a2457 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:29:26 +0400 Subject: [PATCH 10/15] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 1 + internal/reflect/iterate.go | 191 +++++++++++++++++++++--------------- 2 files changed, 114 insertions(+), 78 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 9fae0df..6dcd012 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -126,6 +126,7 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { return intReflect.FieldCallbackResultStop() } + // TODO do not use DeepEqual, return in the result whether the value has changed valueHasChanged = valueHasChanged || !reflect.DeepEqual(original, value) } diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 0108f23..892af6e 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -82,120 +82,155 @@ func IterateFields(strct any, callback FieldCallback, convert bool, convertToPtr return err } + var iterator func( + reflectVal reflect.Value, + callback FieldCallback, + convert bool, + convertToPtr bool, + ) error + switch { case chain.equalTo(reflect.Struct): strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + iterator = iterateStruct - for i := 0; i < reflectVal.Type().NumField(); i++ { - result := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)) + case chain.equalTo(reflect.Ptr, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) + iterator = iteratePtrStruct - if result.set { - return fmt.Errorf("pointer is required to set fields") - } + case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + iterator = iteratePtrInterfaceStruct - if result.stop { - return nil - } + default: + if err := ptrToNilStructError(strct); err != nil { + return err } - 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++ { - result := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)) + return fmt.Errorf("expected struct or pointer to struct, %T given", strct) + } - if result.set { - f := reflectVal.Elem().Field(i) - if !f.CanSet() { - f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() - } + if err := iterator(reflectVal, callback, convert, convertToPtr); err != nil { + return err + } - newVal := result.value + return nil +} - 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 - } +func valueFromField(strct reflect.Value, i int) any { //nolint:ireturn + f := strct.Field(i) - ptr := reflect.New(val.Type()) - ptr.Elem().Set(val) + if !f.CanSet() { // handle unexported fields + if !f.CanAddr() { + tmpReflectVal := reflect.New(strct.Type()).Elem() + tmpReflectVal.Set(strct) + f = tmpReflectVal.Field(i) + } - return ptr, nil - } + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + } - return ValueOf(newVal, f.Type(), convert) - }() + return f.Interface() +} - if err != nil { - return fmt.Errorf("field %d %+q: %w", i, reflectVal.Elem().Type().Field(i).Name, err) - } +func iterateStruct(reflectVal reflect.Value, callback FieldCallback, convert bool, convertToPtr bool) error { + for i := 0; i < reflectVal.Type().NumField(); i++ { + result := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)) - f.Set(newRefVal) - } + if result.set { + return fmt.Errorf("pointer is required to set fields") + } - if result.stop { - return nil - } + if result.stop { + return nil } + } - 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()) - - // TODO find a better solution - stop := false - - newCallback := func(f reflect.StructField, value any) FieldCallbackResult { - if stop { - return FieldCallbackResult{ - value: nil, - set: false, - stop: true, - } - } + return nil +} - result := callback(f, value) +func iteratePtrStruct(reflectVal reflect.Value, callback FieldCallback, convert bool, convertToPtr bool) error { + for i := 0; i < reflectVal.Elem().Type().NumField(); i++ { + result := callback(reflectVal.Elem().Type().Field(i), valueFromField(reflectVal.Elem(), i)) - if result.stop { - stop = true + if result.set { + f := reflectVal.Elem().Field(i) + if !f.CanSet() { + f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() } - return result - } + newVal := result.value - if err := IterateFields(tmp.Interface(), newCallback, convert, convertToPtr); err != nil { - return err - } + 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 + } - v.Set(tmp.Elem()) + ptr := reflect.New(val.Type()) + ptr.Elem().Set(val) - default: - if err := ptrToNilStructError(strct); err != nil { - return err + 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) } - return fmt.Errorf("expected struct or pointer to struct, %T given", strct) + if result.stop { + return nil + } } return nil } -func valueFromField(strct reflect.Value, i int) any { //nolint:ireturn - f := strct.Field(i) +func iteratePtrInterfaceStruct(reflectVal reflect.Value, callback FieldCallback, convert bool, convertToPtr bool) error { + v := reflectVal.Elem() + tmp := reflect.New(v.Elem().Type()) + tmp.Elem().Set(v.Elem()) + + var ( + stop = false + set = false + ) + + newCallback := func(f reflect.StructField, value any) FieldCallbackResult { + if stop { + return FieldCallbackResult{ + value: nil, + set: false, + stop: true, + } + } - if !f.CanSet() { // handle unexported fields - if !f.CanAddr() { - tmpReflectVal := reflect.New(strct.Type()).Elem() - tmpReflectVal.Set(strct) - f = tmpReflectVal.Field(i) + result := callback(f, value) + + if result.stop { + stop = true } - f = reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() + if result.set { + set = true + } + + return result } - return f.Interface() + if err := IterateFields(tmp.Interface(), newCallback, convert, convertToPtr); err != nil { + return err + } + + if set { + v.Set(tmp.Elem()) + } + + return nil } From 6a4fe740dee7808af6961d1463d1ee58f7878006 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:31:29 +0400 Subject: [PATCH 11/15] feat(fields): read/set all the fields from the given struct refactor --- fields/examples_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/fields/examples_test.go b/fields/examples_test.go index c80ef74..c02be75 100644 --- a/fields/examples_test.go +++ b/fields/examples_test.go @@ -214,6 +214,7 @@ func Example_readJSON() { "age": 30, "bio": "bio..." }` + var data map[string]any _ = json.Unmarshal([]byte(js), &data) From a3d81838ec992df95df86eac56018ee80f94f7f1 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:38:00 +0400 Subject: [PATCH 12/15] feat(fields): read/set all the fields from the given struct refactor --- fields/iterate.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/fields/iterate.go b/fields/iterate.go index 6dcd012..2101277 100644 --- a/fields/iterate.go +++ b/fields/iterate.go @@ -28,7 +28,7 @@ import ( ) type config struct { - setter func(_ Path, value any) (_ any, ok bool) + setter func(_ Path, value any) (_ any, set bool) getter func(_ Path, value any) prefillNilStructs bool convertTypes bool @@ -118,16 +118,25 @@ func iterate(strct any, cfg *config, path []reflect.StructField) error { value, valueHasChanged = trySetValue(f, value, cfg, path) if cfg.recursive && isStructOrNonNilStructPtr(f.Type, value) { - original := value + // TODO add tests with setting nested fields + cpCfg := *cfg + if cpCfg.setter != nil { + cpCfg.setter = func(p Path, value any) (_ any, set bool) { + defer func() { + if set { + valueHasChanged = true + } + }() + + return cfg.setter(p, value) + } + } - if err := iterate(&value, cfg, append(path, f)); err != nil { + if err := iterate(&value, &cpCfg, append(path, f)); err != nil { finalErr = fmt.Errorf("%s: %w", f.Name, err) return intReflect.FieldCallbackResultStop() } - - // TODO do not use DeepEqual, return in the result whether the value has changed - valueHasChanged = valueHasChanged || !reflect.DeepEqual(original, value) } if valueHasChanged { From d07dae55f97942b0f37f3d267f2c4372535e7621 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:41:42 +0400 Subject: [PATCH 13/15] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/common.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/reflect/common.go b/internal/reflect/common.go index 35462ad..b9955c9 100644 --- a/internal/reflect/common.go +++ b/internal/reflect/common.go @@ -24,6 +24,7 @@ import ( "reflect" ) +//nolint:revive func ReducedValue(val reflect.Value) (reflect.Value, kindChain, error) { chain, err := ValueToKindChain(val) if err != nil { @@ -62,6 +63,7 @@ func ReducedValue(val reflect.Value) (reflect.Value, kindChain, error) { return val, chain, nil } +//nolint:revive func ReducedValueOf(val any) (reflect.Value, kindChain, error) { return ReducedValue(reflect.ValueOf(val)) } From d975ecdf85977d002b3ad4e8c16c410f54f75fd6 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:45:46 +0400 Subject: [PATCH 14/15] feat(fields): read/set all the fields from the given struct refactor --- internal/reflect/get_set.go | 5 ++--- internal/reflect/get_set_test.go | 4 ++-- internal/reflect/iterate.go | 8 +++++++- setter/setter.go | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index a2cf37f..69dca13 100644 --- a/internal/reflect/get_set.go +++ b/internal/reflect/get_set.go @@ -131,7 +131,7 @@ func Set(strct any, field string, val any, convert bool) (err error) { switch { // s := struct{ val int }{} - // set(&s... + // Set(&s... case chain.equalTo(reflect.Ptr, reflect.Struct): return setOnValue( reflectVal.Elem(), @@ -141,7 +141,7 @@ func Set(strct any, field string, val any, convert bool) (err error) { ) // var s any = struct{ val int }{} - // set(&s... + // Set(&s... case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): v := reflectVal.Elem() tmp := reflect.New(v.Elem().Type()).Elem() @@ -213,7 +213,6 @@ func ValueToKindChain(v reflect.Value) (kindChain, error) { //nolint // unexport r = make(kindChain, 0, 5) //nolint:gomnd ptrs := make(map[uintptr]struct{}) - // TODO use ReducedValueOf for { if v.Kind() == reflect.Ptr && !v.IsNil() { ptr := v.Elem().UnsafeAddr() diff --git a/internal/reflect/get_set_test.go b/internal/reflect/get_set_test.go index 8cb4bb6..212044a 100644 --- a/internal/reflect/get_set_test.go +++ b/internal/reflect/get_set_test.go @@ -272,7 +272,7 @@ func TestSet(t *testing.T) { assert.NoError(t, reflect.Set(&p, "Name", "Jane", false)) assert.Equal(t, person{Name: "Jane"}, p) }) - t.Run("var a any = struct{}; a2 := &a; setter.set(&a2...", func(t *testing.T) { + t.Run("var a any = struct{}; a2 := &a; setter.Set(&a2...", func(t *testing.T) { t.Parallel() var p any = person{} @@ -280,7 +280,7 @@ func TestSet(t *testing.T) { assert.NoError(t, reflect.Set(&p2, "Name", "Jane", false)) assert.Equal(t, person{Name: "Jane"}, p) }) - t.Run("var a1 any = struct{}; var a2 any = &a1; var a3 any = &a2; ...; setter.set(&aN...", func(t *testing.T) { + t.Run("var a1 any = struct{}; var a2 any = &a1; var a3 any = &a2; ...; setter.Set(&aN...", func(t *testing.T) { t.Parallel() var p any = person{} diff --git a/internal/reflect/iterate.go b/internal/reflect/iterate.go index 892af6e..f2c2f58 100644 --- a/internal/reflect/iterate.go +++ b/internal/reflect/iterate.go @@ -133,6 +133,7 @@ func valueFromField(strct reflect.Value, i int) any { //nolint:ireturn return f.Interface() } +//nolint:revive func iterateStruct(reflectVal reflect.Value, callback FieldCallback, convert bool, convertToPtr bool) error { for i := 0; i < reflectVal.Type().NumField(); i++ { result := callback(reflectVal.Type().Field(i), valueFromField(reflectVal, i)) @@ -192,7 +193,12 @@ func iteratePtrStruct(reflectVal reflect.Value, callback FieldCallback, convert return nil } -func iteratePtrInterfaceStruct(reflectVal reflect.Value, callback FieldCallback, convert bool, convertToPtr bool) error { +func iteratePtrInterfaceStruct( + reflectVal reflect.Value, + callback FieldCallback, + convert bool, + convertToPtr bool, +) error { v := reflectVal.Elem() tmp := reflect.New(v.Elem().Type()) tmp.Elem().Set(v.Elem()) diff --git a/setter/setter.go b/setter/setter.go index f95184c..6d9c0b1 100644 --- a/setter/setter.go +++ b/setter/setter.go @@ -33,7 +33,7 @@ Unexported fields are supported. Name string } p := Person{} - _ = setter.set(&p, "Name", "Jane", false) + _ = setter.Set(&p, "Name", "Jane", false) fmt.Println(p) // {Jane} */ func Set(strct any, field string, val any, convert bool) error { From 7f1036d9e2bfb9f5d19ab6be3e140d3936520275 Mon Sep 17 00:00:00 2001 From: bkrukowski Date: Wed, 2 Oct 2024 03:46:21 +0400 Subject: [PATCH 15/15] feat(fields): read/set all the fields from the given struct refactor --- caller/internal/caller/call.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caller/internal/caller/call.go b/caller/internal/caller/call.go index 8ed5c78..73ba83b 100644 --- a/caller/internal/caller/call.go +++ b/caller/internal/caller/call.go @@ -152,7 +152,7 @@ func validateAndForceCallMethod( return nil, err } - // see [intReflect.set] + // see [intReflect.Set] for { switch { case chain.Prefixed(reflect.Ptr, reflect.Ptr):