diff --git a/caller/internal/caller/call.go b/caller/internal/caller/call.go index 4468fa1..73ba83b 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/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..c02be75 --- /dev/null +++ b/fields/examples_test.go @@ -0,0 +1,272 @@ +// 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 Example_readJSON() { + 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..2101277 --- /dev/null +++ b/fields/iterate.go @@ -0,0 +1,197 @@ +// 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, set 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) intReflect.FieldCallbackResult { + // call getter + if cfg.getter != nil { + cfg.getter(append(path, f), value) + } + + var valueHasChanged bool + + value, valueHasChanged = trySetValue(f, value, cfg, path) + + if cfg.recursive && isStructOrNonNilStructPtr(f.Type, 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, &cpCfg, append(path, f)); err != nil { + finalErr = fmt.Errorf("%s: %w", f.Name, err) + + return intReflect.FieldCallbackResultStop() + } + } + + if valueHasChanged { + return intReflect.FieldCallbackResultSet(value) + } + + return intReflect.FieldCallbackResultDontSet() + } + + 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..18c98b8 --- /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 { //nolint:thelper + 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..b9955c9 --- /dev/null +++ b/internal/reflect/common.go @@ -0,0 +1,69 @@ +// 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" +) + +//nolint:revive +func ReducedValue(val reflect.Value) (reflect.Value, kindChain, error) { + chain, err := ValueToKindChain(val) + 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): + val = val.Elem() + chain = chain[1:] + + continue + case chain.Prefixed(reflect.Ptr, reflect.Interface, reflect.Ptr): + val = val.Elem().Elem() + chain = chain[2:] + + continue + } + + break + } + + return val, chain, nil +} + +//nolint:revive +func ReducedValueOf(val any) (reflect.Value, kindChain, error) { + return ReducedValue(reflect.ValueOf(val)) +} diff --git a/internal/reflect/get_set.go b/internal/reflect/get_set.go index ee5c9a8..69dca13 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 := ReducedValueOf(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..f2c2f58 --- /dev/null +++ b/internal/reflect/iterate.go @@ -0,0 +1,242 @@ +// 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 FieldCallbackResult struct { + value any + set bool + stop bool +} + +func FieldCallbackResultSet(value any) FieldCallbackResult { + return FieldCallbackResult{ + value: value, + set: true, + stop: false, + } +} + +func FieldCallbackResultDontSet() FieldCallbackResult { + return FieldCallbackResult{ + value: nil, + set: false, + stop: false, + } +} + +func FieldCallbackResultStop() FieldCallbackResult { + return FieldCallbackResult{ + value: nil, + set: false, + stop: true, + } +} + +type FieldCallback = func(_ reflect.StructField, value any) FieldCallbackResult + +// 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 := ReducedValueOf(strct) + if err != nil { + 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 + + case chain.equalTo(reflect.Ptr, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Elem().Type()).Interface()) + iterator = iteratePtrStruct + + case chain.equalTo(reflect.Ptr, reflect.Interface, reflect.Struct): + strType = fmt.Sprintf("%T", reflect.Zero(reflectVal.Type()).Interface()) + iterator = iteratePtrInterfaceStruct + + default: + if err := ptrToNilStructError(strct); err != nil { + return err + } + + return fmt.Errorf("expected struct or pointer to struct, %T given", strct) + } + + if err := iterator(reflectVal, callback, convert, convertToPtr); err != nil { + return err + } + + 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() +} + +//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)) + + if result.set { + return fmt.Errorf("pointer is required to set fields") + } + + if result.stop { + return nil + } + } + + return nil +} + +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.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) + 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) + } + + if result.stop { + return nil + } + } + + return nil +} + +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, + } + } + + result := callback(f, value) + + if result.stop { + stop = true + } + + if result.set { + set = true + } + + return result + } + + if err := IterateFields(tmp.Interface(), newCallback, convert, convertToPtr); err != nil { + return err + } + + if set { + v.Set(tmp.Elem()) + } + + return nil +} diff --git a/internal/reflect/iterate_test.go b/internal/reflect/iterate_test.go new file mode 100644 index 0000000..f385d71 --- /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) reflect.FieldCallbackResult { + if f.Name == "Name" { + return reflect.FieldCallbackResultSet("Jane") + } + + if f.Name == "age" { + return reflect.FieldCallbackResultSet(uint(30)) + } + + return reflect.FieldCallbackResultDontSet() + }, + convert: true, + convertToPtr: false, + expected: person{ + Name: "Jane", + age: 30, + }, + }, + { + strct: person{}, + callback: func(f stdReflect.StructField, value any) reflect.FieldCallbackResult { + if f.Name == "Name" { + return reflect.FieldCallbackResultSet("Jane") + } + + if f.Name == "age" { + return reflect.FieldCallbackResultSet(uint(30)) + } + + return reflect.FieldCallbackResultDontSet() + }, + 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) + }) + } + }) +}