diff --git a/.golangci.yml b/.golangci.yml index b3f1630..299c985 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,12 @@ issues: - path: _test\.go linters: - exhaustruct + - path: caller/constraints_test\.go + linters: + - lll + - path: caller/caller_test\.go + linters: + - lll - path: _test\.go text: "var-declaration: should drop .* from declaration of .*; it is the zero value" - path: _test\.go diff --git a/caller/README.md b/caller/README.md new file mode 100644 index 0000000..0bc2ddc --- /dev/null +++ b/caller/README.md @@ -0,0 +1,15 @@ +# Caller + +This package provides functions that allow calling other functions with unknown arguments. +The `convertArgs` argument instructs the caller whether you allow for converting the type. + +```go +sum := func(a, b int) int { + return a + b +} + +returns, _ := caller.Call(sum, []any{2, 3}, false) +fmt.Println(returns) // [5] +``` + +See [examples](examples_test.go). diff --git a/caller/any.go b/caller/any.go new file mode 100644 index 0000000..ad741ac --- /dev/null +++ b/caller/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 caller + +type any = interface{} //nolint diff --git a/caller/any_test.go b/caller/any_test.go new file mode 100644 index 0000000..dfd5576 --- /dev/null +++ b/caller/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 caller_test + +type any = interface{} //nolint diff --git a/caller/caller.go b/caller/caller.go new file mode 100644 index 0000000..4b672a7 --- /dev/null +++ b/caller/caller.go @@ -0,0 +1,307 @@ +// 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 caller + +import ( + "fmt" + "reflect" + + "github.com/gontainer/grouperror" + "github.com/gontainer/reflectpro/caller/internal/caller" +) + +// Call calls the given function with the given arguments. +// It returns values returned by the function in a slice. +// If the third argument equals true, it converts types whenever it is possible. +// +//nolint:wrapcheck +func Call(fn any, args []any, convertArgs bool) (_ []any, err error) { + defer func() { + if err != nil { + err = grouperror.Prefix(fmt.Sprintf("cannot call %T: ", fn), err) + } + }() + + v, err := caller.Func(fn) + if err != nil { + return nil, err + } + + return caller.CallFunc(v, args, convertArgs) +} + +const ( + providerInternalErrPrefix = "cannot call provider %T: " + providerMethodInternalErrPrefix = "cannot call provider (%T).%+q: " + providerExternalErrPrefix = "provider returned error: " +) + +//nolint:wrapcheck +func callProvider( //nolint:ireturn + getFn func() (reflect.Value, error), + args []any, + convertArgs bool, + internalErrPrefix func() string, +) (_ any, err error) { + executedProvider := false + defer func() { + if !executedProvider && err != nil { + err = grouperror.Prefix(internalErrPrefix(), err) + } + }() + + fn, err := getFn() + if err != nil { + return nil, err + } + + if err := caller.ValidatorProvider.Validate(fn); err != nil { + return nil, err + } + + results, err := caller.CallFunc(fn, args, convertArgs) + if err != nil { + return nil, err + } + + executedProvider = true + + r := results[0] + + var e error + + if len(results) > 1 { + // do not panic when results[1] == nil + e, _ = results[1].(error) + } + + if e != nil { + e = grouperror.Prefix(providerExternalErrPrefix, newProviderError(e)) + } + + return r, e +} + +/* +CallProvider works similar to [Call] with the difference it requires a provider as the first argument. +Provider is a function which returns 1 or 2 values. +The second return value which is optional must be a type of error. +See [ProviderError]. + + p := func() (any, error) { + db, err := sql.Open("mysql", "user:password@/dbname") + if err != nil { + return nil, err + } + + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + return db, nil + } + + db, err := caller.CallProvider(p, nil, false) +*/ +//nolint:wrapcheck +func CallProvider(provider any, args []any, convertArgs bool) (any, error) { //nolint:ireturn + return callProvider( + func() (reflect.Value, error) { + return caller.Func(provider) + }, + args, + convertArgs, + func() string { + return fmt.Sprintf(providerInternalErrPrefix, provider) + }, + ) +} + +/* +CallProviderMethod works similar to [CallProvider], but the provider must be a method on the given object. + + db, _ := sql.Open("mysql", "user:password@/dbname") + tx, err := caller.CallProviderMethod(db, "Begin", nil, false) +*/ +//nolint:wrapcheck +func CallProviderMethod(object any, method string, args []any, convertArgs bool) (any, error) { //nolint:ireturn + return callProvider( + func() (reflect.Value, error) { + return caller.Method(object, method) + }, + args, + convertArgs, + func() string { + return fmt.Sprintf(providerMethodInternalErrPrefix, object, method) + }, + ) +} + +// ForceCallProviderMethod is an extended version of [CallProviderMethod]. +// See [ForceCallMethod]. +// +//nolint:wrapcheck +func ForceCallProviderMethod(object any, method string, args []any, convertArgs bool) (any, error) { //nolint:ireturn + results, err := caller.ValidateAndForceCallMethod(object, method, args, convertArgs, caller.ValidatorProvider) + if err != nil { + return nil, grouperror.Prefix(fmt.Sprintf(providerMethodInternalErrPrefix, object, method), err) + } + + r := results[0] + + var e error + + if len(results) > 1 { + // do not panic when results[1] == nil + e, _ = results[1].(error) + } + + if e != nil { + e = grouperror.Prefix(providerExternalErrPrefix, newProviderError(e)) + } + + return r, e +} + +/* +CallMethod works similar to [Call] with the difference it calls the method by the name over the given receiver. + + type Person struct { + Name string + } + + func (p *Person) SetName(n string) { + p.Name = n + } + + func main() { + p := &Person{} + _, _ = caller.CallMethod(p, "SetName", []any{"Mary"}, false) + fmt.Println(p.name) + // Output: Mary + } +*/ +//nolint:wrapcheck +func CallMethod(object any, method string, args []any, convertArgs bool) (_ []any, err error) { + defer func() { + if err != nil { + err = grouperror.Prefix(fmt.Sprintf("cannot call method (%T).%+q: ", object, method), err) + } + }() + + fn, err := caller.Method(object, method) + if err != nil { + return nil, err + } + + return caller.CallFunc(fn, args, convertArgs) +} + +/* +ForceCallMethod is an extended version of [CallMethod]. + +The following code cannot work: + + var p any = person{} + caller.CallMethod(&p, "SetName", []any{"Jane"}, false) + +because `&p` returns a pointer to an interface, not to the `person` type. +The same problem occurs without using that package: + + var tmp any = person{} + p := &tmp.(person) + // compiler returns: + // invalid operation: cannot take address of tmp.(person) (comma, ok expression of type person). + +[ForceCallMethod] solves that problem by copying the value and creating a pointer to it using the [reflect] package, +but that solution is slightly slower. In contrast to [CallMethod], it requires a pointer always. +*/ +//nolint:wrapcheck +func ForceCallMethod(object any, method string, args []any, convertArgs bool) (_ []any, err error) { + defer func() { + if err != nil { + err = grouperror.Prefix(fmt.Sprintf("cannot call method (%T).%+q: ", object, method), err) + } + }() + + return caller.ValidateAndForceCallMethod(object, method, args, convertArgs, caller.DontValidate) +} + +/* +CallWither works similar to [CallMethod] with the difference the method must be a wither. + + type Person struct { + name string + } + + func (p Person) WithName(n string) Person { + p.Name = n + return p + } + + func main() { + p := Person{} + p2, _ := caller.CallWither(p, "WithName", "Mary") + fmt.Printf("%+v", p2) // {name:Mary} + } +*/ +//nolint:wrapcheck +func CallWither(object any, wither string, args []any, convertArgs bool) (_ any, err error) { //nolint:ireturn + defer func() { + if err != nil { + err = grouperror.Prefix(fmt.Sprintf("cannot call wither (%T).%+q: ", object, wither), err) + } + }() + + fn, err := caller.Method(object, wither) + if err != nil { + return nil, err + } + + if err := caller.ValidatorWither.Validate(fn); err != nil { + return nil, err + } + + r, err := caller.CallFunc(fn, args, convertArgs) + if err != nil { + return nil, err + } + + return r[0], nil +} + +// ForceCallWither calls the given wither (see [CallWither]) using the same approach as [ForceCallMethod]. +// +//nolint:wrapcheck +func ForceCallWither(object any, wither string, args []any, convertArgs bool) (_ any, err error) { //nolint:ireturn + defer func() { + if err != nil { + err = grouperror.Prefix(fmt.Sprintf("cannot call wither (%T).%+q: ", object, wither), err) + } + }() + + r, err := caller.ValidateAndForceCallMethod(object, wither, args, convertArgs, caller.ValidatorWither) + if err != nil { + return nil, err + } + + return r[0], nil +} diff --git a/caller/caller_test.go b/caller/caller_test.go new file mode 100644 index 0000000..6e633f6 --- /dev/null +++ b/caller/caller_test.go @@ -0,0 +1,849 @@ +// 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 caller_test + +import ( + "errors" + "fmt" + "strings" + "testing" + + errAssert "github.com/gontainer/grouperror/assert" + "github.com/gontainer/reflectpro/caller" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCall(t *testing.T) { + t.Parallel() + + t.Run("Given method", func(t *testing.T) { + t.Parallel() + + p := person{} + assert.Equal(t, "", p.name) + _, err := caller.Call(p.setName, []any{"Mary"}, false) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "Mary", p.name) + }) + + t.Run("Given invalid functions", func(t *testing.T) { + t.Parallel() + + scenarios := []struct { + fn any + }{ + {fn: 5}, + {fn: false}, + {fn: (*error)(nil)}, + {fn: struct{}{}}, + } + const expectedRegexp = "\\Acannot call .*: expected func, .* given\\z" + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + _, err := caller.Call(s.fn, nil, false) + assert.Error(t, err) + assert.Regexp(t, expectedRegexp, err) + }) + } + }) + + t.Run("Given invalid argument", func(t *testing.T) { + t.Parallel() + + const msg = "cannot call func([]int): arg0: cannot convert struct {} to []int" + callee := func([]int) {} + params := []any{ + struct{}{}, + } + + _, err := caller.Call(callee, params, true) + assert.EqualError(t, err, msg) + }) + + t.Run("Given invalid arguments", func(t *testing.T) { + t.Parallel() + + callee := func([]int, *int) {} + params := []any{ + struct{}{}, + "*int", + } + + _, err := caller.Call(callee, params, true) + + expected := []string{ + "cannot call func([]int, *int): arg0: cannot convert struct {} to []int", + "cannot call func([]int, *int): arg1: cannot convert string to *int", + } + errAssert.EqualErrorGroup(t, err, expected) + }) + + t.Run("Given too many arguments", func(t *testing.T) { + t.Parallel() + + const msg = "too many input arguments" + scenarios := []struct { + fn any + args []any + }{ + { + fn: strings.Join, + args: []any{"1", "2", "3"}, + }, + { + fn: func() {}, + args: []any{1}, + }, + } + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + _, err := caller.Call(s.fn, s.args, true) + assert.ErrorContains(t, err, msg) + }) + } + }) + + t.Run("Given too few arguments", func(t *testing.T) { + const msg = "not enough input arguments" + scenarios := []struct { + fn any + args []any + }{ + { + fn: strings.Join, + args: []any{}, + }, + { + fn: func(a int) {}, + args: []any{}, + }, + } + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + _, err := caller.Call(s.fn, s.args, true) + assert.ErrorContains(t, err, msg) + }) + } + }) + + t.Run("Given scenarios", func(t *testing.T) { + scenarios := []struct { + fn any + args []any + expected []any + }{ + { + fn: func(a, b int) int { + return a + b + }, + args: []any{uint(1), uint(2)}, + expected: []any{int(3)}, + }, + { + fn: func(a, b uint) uint { + return a + b + }, + args: []any{int(7), int(3)}, + expected: []any{uint(10)}, + }, + { + fn: func(vals ...uint) (result uint) { + for _, v := range vals { + result += v + } + + return + }, + args: []any{int(1), int(2), int(3)}, + expected: []any{uint(6)}, + }, + } + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + r, err := caller.Call(s.fn, s.args, true) + assert.NoError(t, err) + assert.Equal(t, s.expected, r) + }) + } + }) + + t.Run("Convert parameters", func(t *testing.T) { + scenarios := map[string]struct { + fn any + input any + output any + error string + }{ + "[]any to []type": { + fn: func(v []int) []int { + return v + }, + input: []any{1, 2, 3}, + output: []int{1, 2, 3}, + }, + "[]struct{}{} to []type": { + fn: func([]int) {}, + input: []struct{}{}, + error: `cannot call func([]int): arg0: cannot convert []struct {} to []int: cannot convert struct {} to int`, + }, + "nil to any": { + fn: func(v any) any { + return v + }, + input: nil, + output: (any)(nil), + }, + } + + for n, tmp := range scenarios { + s := tmp + t.Run(n, func(t *testing.T) { + t.Parallel() + + r, err := caller.Call(s.fn, []any{s.input}, true) + if s.error != "" { + assert.EqualError(t, err, s.error) + assert.Nil(t, r) + + return + } + + assert.NoError(t, err) + assert.Equal(t, r[0], s.output) + }) + } + }) +} + +func TestCallProvider(t *testing.T) { + t.Parallel() + + t.Run("Given scenarios", func(t *testing.T) { + t.Parallel() + + scenarios := []struct { + provider any + params []any + expected any + }{ + { + provider: func() any { + return nil + }, + expected: nil, + }, + { + provider: func(vals ...int) (int, error) { + result := 0 + for _, v := range vals { + result += v + } + + return result, nil + }, + params: []any{10, 100, 200}, + expected: 310, + }, + } + + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + r, err := caller.CallProvider(s.provider, s.params, false) + assert.NoError(t, err) + assert.Equal(t, s.expected, r) + }) + } + }) + + t.Run("Provider error", func(t *testing.T) { + t.Parallel() + + type myError struct { + error + } + p := func() (any, error) { + return nil, &myError{errors.New("my error")} + } + _, err := caller.CallProvider(p, nil, false) + require.EqualError(t, err, "provider returned error: my error") + + var providerErr *caller.ProviderError + require.True(t, errors.As(err, &providerErr)) + assert.EqualError(t, providerErr, "my error") + + var myErr *myError + require.True(t, errors.As(err, &myErr)) + assert.EqualError(t, myErr, "my error") + }) + + t.Run("Given errors", func(t *testing.T) { + t.Parallel() + + scenarios := []struct { + provider any + params []any + err string + }{ + { + provider: func() {}, + err: "cannot call provider func(): provider must return 1 or 2 values, given function returns 0 values", + }, + { + provider: func() (any, any, any) { + return nil, nil, nil + }, + err: "cannot call provider func() (interface {}, interface {}, interface {}): provider must return 1 or 2 values, given function returns 3 values", + }, + { + provider: func() (any, any) { + return nil, nil + }, + err: "cannot call provider func() (interface {}, interface {}): second value returned by provider must implement error interface, interface {} given", + }, + { + provider: func() (any, int) { + return nil, 0 + }, + err: "cannot call provider func() (interface {}, int): second value returned by provider must implement error interface, int given", + }, + { + provider: func() (any, Person) { + return nil, Person{} + }, + err: "cannot call provider func() (interface {}, caller_test.Person): second value returned by provider must implement error interface, caller_test.Person given", + }, + { + provider: func() (any, error) { + return nil, errors.New("test error") + }, + err: "provider returned error: test error", + }, + { + provider: func() any { + return nil + }, + params: []any{1, 2, 3}, + err: "cannot call provider func() interface {}: too many input arguments", + }, + } + + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + r, err := caller.CallProvider(s.provider, s.params, false) + assert.Nil(t, r) + assert.EqualError(t, err, s.err) + }) + } + }) + + t.Run("Given invalid provider", func(t *testing.T) { + t.Parallel() + + _, err := caller.CallProvider(5, nil, false) + assert.EqualError(t, err, "cannot call provider int: expected func, int given") + }) + + t.Run("Given provider panics", func(t *testing.T) { + t.Parallel() + + defer func() { + assert.Equal(t, "panic!", recover()) + }() + + _, _ = caller.CallProvider( + func() any { + panic("panic!") + }, + nil, + false, + ) + }) +} + +type mockProvider struct { + fn func() any + fnWithError func() (any, error) +} + +func (m *mockProvider) Provider() any { //nolint:ireturn + return m.fn() +} + +func (m *mockProvider) ProviderWithError() (any, error) { //nolint:ireturn + return m.fnWithError() +} + +func (m *mockProvider) NotProvider() (any, any) { //nolint:ireturn + return nil, nil +} + +func TestCallProviderMethod(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + p := mockProvider{ + fn: func() any { + return "value #1" + }, + } + r, err := caller.CallProviderMethod(&p, "Provider", nil, false) + assert.NoError(t, err) + assert.Equal(t, "value #1", r) + }) + t.Run("#2", func(t *testing.T) { + t.Parallel() + + p := mockProvider{ + fnWithError: func() (any, error) { + return "value #2", nil + }, + } + r, err := caller.CallProviderMethod(&p, "ProviderWithError", nil, false) + assert.NoError(t, err) + assert.Equal(t, "value #2", r) + }) + }) + t.Run("Errors", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + r, err := caller.CallProviderMethod(nil, "MyProvider", nil, false) + assert.Nil(t, r) + assert.EqualError(t, err, `cannot call provider ()."MyProvider": invalid method receiver: `) + }) + t.Run("#2", func(t *testing.T) { + t.Parallel() + + p := mockProvider{ + fnWithError: func() (any, error) { + return "error value", errors.New("my error") + }, + } + r, err := caller.CallProviderMethod(&p, "ProviderWithError", nil, false) + assert.Equal(t, "error value", r) + assert.EqualError(t, err, "provider returned error: my error") + }) + t.Run("#3", func(t *testing.T) { + t.Parallel() + + r, err := caller.CallProviderMethod(&mockProvider{}, "NotProvider", nil, false) + assert.Nil(t, r) + assert.EqualError(t, err, `cannot call provider (*caller_test.mockProvider)."NotProvider": second value returned by provider must implement error interface, interface {} given`) + }) + }) +} + +func TestForceCallProviderMethod(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + t.Run(`With "Force" prefix`, func(t *testing.T) { + t.Parallel() + + var p any = mockProvider{ + fn: func() any { + return "my value" + }, + } + // p is not a pointer, Provider requires pointer receiver, but this function can handle that + r, err := caller.ForceCallProviderMethod(&p, "Provider", nil, false) + assert.Equal(t, "my value", r) + assert.NoError(t, err) + }) + + t.Run(`Without "Force" prefix`, func(t *testing.T) { + t.Parallel() + + var p any = mockProvider{ + fnWithError: func() (any, error) { + return "error value", errors.New("my error") + }, + } + // oops... p is not a pointer, ProviderWithError requires pointer receiver + r, err := caller.CallProviderMethod(&p, "ProviderWithError", nil, false) + assert.Nil(t, r) + assert.EqualError(t, err, `cannot call provider (*interface {})."ProviderWithError": invalid func (*interface {})."ProviderWithError"`) + }) + }) + }) + t.Run("Errors", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + r, err := caller.ForceCallProviderMethod(nil, "MyProvider", nil, false) + assert.Nil(t, r) + assert.EqualError(t, err, `cannot call provider ()."MyProvider": expected ptr, given`) + }) + t.Run("#2", func(t *testing.T) { + t.Parallel() + + p := mockProvider{ + fnWithError: func() (any, error) { + return "my error value", errors.New("my error in provider") + }, + } + r, err := caller.ForceCallProviderMethod(&p, "ProviderWithError", nil, false) + assert.Equal(t, "my error value", r) + assert.EqualError(t, err, "provider returned error: my error in provider") + var providerErr *caller.ProviderError + require.True(t, errors.As(err, &providerErr)) + assert.EqualError(t, providerErr, "my error in provider") + }) + t.Run("#3", func(t *testing.T) { + t.Parallel() + + var p any = mockProvider{} + r, err := caller.ForceCallProviderMethod(&p, "NotProvider", nil, false) + assert.Nil(t, r) + assert.EqualError(t, err, `cannot call provider (*interface {})."NotProvider": second value returned by provider must implement error interface, interface {} given`) + }) + }) +} + +func TestCallMethod(t *testing.T) { + t.Parallel() + + t.Run("Pointer loop", func(t *testing.T) { + t.Parallel() + + var a any + a = &a + r, err := caller.CallMethod(a, "method", nil, false) + assert.EqualError(t, err, `cannot call method (*interface {})."method": unexpected pointer loop`) + assert.Nil(t, r) + }) +} + +func TestCallWither(t *testing.T) { + t.Parallel() + + t.Run("Given scenarios", func(t *testing.T) { + t.Parallel() + + var emptyPerson any = person{} + + scenarios := []struct { + object any + wither string + params []any + output any + }{ + { + object: make(ints, 0), + wither: "Append", + params: []any{5}, + output: ints{5}, + }, + { + object: person{name: "Mary"}, + wither: "WithName", + params: []any{"Jane"}, + output: person{name: "Jane"}, + }, + { + object: &person{name: "Mary"}, + wither: "WithName", + params: []any{"Jane"}, + output: person{name: "Jane"}, + }, + { + object: emptyPerson, + wither: "WithName", + params: []any{"Kaladin"}, + output: person{name: "Kaladin"}, + }, + { + object: &emptyPerson, + wither: "WithName", + params: []any{"Shallan"}, + output: person{name: "Shallan"}, + }, + } + + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + result, err := caller.CallWither(s.object, s.wither, s.params, false) + assert.NoError(t, err) + assert.Equal(t, s.output, result) + }) + } + }) + + t.Run("Given errors", func(t *testing.T) { + t.Parallel() + + scenarios := []struct { + object any + wither string + params []any + error string + }{ + { + object: person{}, + wither: "withName", + params: nil, + error: `cannot call wither (caller_test.person)."withName": invalid func (caller_test.person)."withName"`, + }, + { + object: person{}, + wither: "Clone", + params: nil, + error: `cannot call wither (caller_test.person)."Clone": wither must return 1 value, given function returns 2 values`, + }, + { + object: person{}, + wither: "WithName", + params: nil, + error: `cannot call wither (caller_test.person)."WithName": not enough input arguments`, + }, + } + + for i, tmp := range scenarios { + s := tmp + t.Run(fmt.Sprintf("Scenario #%d", i), func(t *testing.T) { + t.Parallel() + + o, err := caller.CallWither(s.object, s.wither, s.params, false) + assert.Nil(t, o) + assert.EqualError(t, err, s.error) + }) + } + }) + + t.Run("Pointer loop", func(t *testing.T) { + t.Parallel() + + var a any + a = &a + r, err := caller.CallWither(a, "method", nil, false) + assert.EqualError(t, err, `cannot call wither (*interface {})."method": unexpected pointer loop`) + assert.Nil(t, r) + }) + + t.Run("Nil pointer receiver", func(t *testing.T) { + t.Parallel() + + var p *person + r, err := caller.CallWither(p, "Empty", nil, false) + assert.NoError(t, err) + assert.Nil(t, p) + assert.Equal(t, person{}, r) + }) +} + +type ints []int + +func (i ints) Append(v int) ints { + return append(i, v) +} + +type person struct { + name string + age uint +} + +func (p person) Clone() (person, error) { + return p, nil +} + +func (p person) WithName(n string) person { + return person{name: n} +} + +func (p person) withName(n string) person { //nolint:unused + return person{name: n} +} + +func (p *person) setName(n string) { + p.name = n +} + +func (p *person) SetName(n string) { + p.name = n +} + +func (p *person) Empty() person { + return person{} +} + +type nums []int + +func (n *nums) Append(v int) { + *n = append(*n, v) +} + +func TestForceCallMethod(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + var p any = person{age: 28} // make sure pre-initiated values won't disappear + var p2 any = &p + var p3 = &p2 + _, err := caller.ForceCallMethod(&p3, "SetName", []any{"Jane"}, false) + assert.NoError(t, err) + assert.Equal(t, person{age: 28, name: "Jane"}, p) + }) + t.Run("OK #2", func(t *testing.T) { + t.Parallel() + + var n any = nums{} + for i := 5; i < 8; i++ { + r, err := caller.ForceCallMethod(&n, "Append", []any{i}, false) + assert.NoError(t, err) + assert.Nil(t, r) + } + assert.Equal(t, nums{5, 6, 7}, n.(nums)) //nolint:forcetypeassert + }) + t.Run("OK #3 (nil pointer receiver)", func(t *testing.T) { + t.Parallel() + + var p1 *person + var p2 any = p1 + r, err := caller.ForceCallMethod(&p2, "Empty", nil, false) + assert.NoError(t, err) + assert.Nil(t, p1) + assert.Equal(t, person{}, r[0]) + }) + }) + t.Run("Errors", func(t *testing.T) { + t.Parallel() + + t.Run("#1", func(t *testing.T) { + t.Parallel() + + var a *int + _, err := caller.ForceCallMethod(a, "SomeMethod", nil, false) + assert.EqualError(t, err, `cannot call method (*int)."SomeMethod": invalid func (*int)."SomeMethod"`) + }) + t.Run("Method panics", func(t *testing.T) { + t.Parallel() + + defer func() { + assert.Equal( + t, + "runtime error: invalid memory address or nil pointer dereference", + fmt.Sprintf("%s", recover()), + ) + }() + + var p *person + _, _ = caller.ForceCallMethod(&p, "SetName", []any{"Jane"}, false) + }) + }) +} + +type Pet struct { + Name string + Type string +} + +func (p *Pet) WithName(n string) *Pet { + r := *p + r.Name = n + + return &r +} + +func (p *Pet) WithType(t string) *Pet { + r := *p + r.Type = t + + return &r +} + +func (p *Pet) NameType() (name, type_ string) { //nolint // var-naming: don't use underscores in Go names + return p.Name, p.Type +} + +func TestForceCallWither(t *testing.T) { + t.Parallel() + + t.Run("OK #1", func(t *testing.T) { + t.Parallel() + + var p any = Pet{} + r, err := caller.ForceCallWither(&p, "WithName", []any{"Laika"}, false) + assert.NoError(t, err) + r, err = caller.ForceCallWither(&r, "WithType", []any{"dog"}, false) + assert.NoError(t, err) + assert.Equal(t, Pet{Name: "Laika", Type: "dog"}, *r.(*Pet)) //nolint:forcetypeassert + }) + t.Run("OK #2 (nil pointer receiver)", func(t *testing.T) { + t.Parallel() + + var p1 *person + var p2 any = p1 + r, err := caller.ForceCallWither(&p2, "Empty", nil, false) + assert.NoError(t, err) + assert.Nil(t, p1) + assert.Equal(t, person{}, r) + }) + t.Run("Error", func(t *testing.T) { + t.Parallel() + + var p any = Pet{} + r, err := caller.ForceCallWither(&p, "NameType", nil, false) + assert.EqualError(t, err, `cannot call wither (*interface {})."NameType": wither must return 1 value, given function returns 2 values`) + assert.Nil(t, r) + }) +} diff --git a/caller/constraints_test.go b/caller/constraints_test.go new file mode 100644 index 0000000..b0dd1a6 --- /dev/null +++ b/caller/constraints_test.go @@ -0,0 +1,230 @@ +// 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 caller_test + +import ( + "testing" + + "github.com/gontainer/reflectpro/caller" + "github.com/stretchr/testify/assert" +) + +type book struct { + title string +} + +func (b *book) SetTitle(t string) { + b.title = t +} + +func (b *book) setTitle(t string) { //nolint:unused + b.title = t +} + +func (b book) WithTitle(t string) book { + b.title = t + + return b +} + +func TestConstraint(t *testing.T) { + t.Parallel() + + const ( + harryPotterTitle = "Harry Potter" + ) + + var ( + harryPotter = book{title: harryPotterTitle} + emptyBook = book{} + ) + + // https://github.com/golang/go/wiki/MethodSets#interfaces + + // Method with a pointer receiver requires explicit definition of the pointer: + // v := &book{}; CallMethod(v, ... + // var v any = &book{}; CallMethod(v, ... + // v := book{}; CallMethod(&v, ... + // + // Creating variable as a value will not work: + // v := book{}; CallMethod(v, ... + // var v interface{} = book{}; CallMethod(&v, ... + t.Run("Call a method", func(t *testing.T) { + t.Parallel() + + t.Run("Pointer receiver", func(t *testing.T) { + t.Parallel() + + t.Run("Given errors", func(t *testing.T) { + t.Parallel() + + t.Run("v := book{}; CallMethod(v, ...", func(t *testing.T) { + t.Parallel() + + b := book{} + r, err := caller.CallMethod(b, "SetTitle", []any{harryPotterTitle}, false) + assert.EqualError(t, err, `cannot call method (caller_test.book)."SetTitle": invalid func (caller_test.book)."SetTitle"`) + assert.Nil(t, r) + assert.Zero(t, b) + }) + t.Run("var v any = book{}; CallMethod(&v, ...", func(t *testing.T) { + t.Parallel() + + t.Run("CallMethod", func(t *testing.T) { + t.Parallel() + + var b any = book{} + r, err := caller.CallMethod(&b, "SetTitle", []any{harryPotterTitle}, false) + assert.EqualError(t, err, `cannot call method (*interface {})."SetTitle": invalid func (*interface {})."SetTitle"`) + assert.Nil(t, r) + assert.Equal(t, emptyBook, b) + }) + t.Run("ForceCallMethod", func(t *testing.T) { + t.Parallel() + + var b any = book{} + r, err := caller.ForceCallMethod(&b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Nil(t, r) + assert.Equal(t, harryPotter, b) + }) + }) + }) + t.Run("Given scenarios", func(t *testing.T) { + t.Parallel() + + t.Run("v := book{}; CallMethod(&v, ...", func(t *testing.T) { + t.Parallel() + + b := book{} + r, err := caller.CallMethod(&b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, harryPotter, b) + }) + t.Run("v := &book{}; CallMethod(&v, ...", func(t *testing.T) { + t.Parallel() + + b := &book{} + r, err := caller.CallMethod(&b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, &harryPotter, b) + }) + t.Run("v := &book{}; CallMethod(v, ...", func(t *testing.T) { + t.Parallel() + + b := &book{} + r, err := caller.CallMethod(b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, &harryPotter, b) + }) + t.Run("var v any = &book{}; CallMethod(v, ...", func(t *testing.T) { + t.Parallel() + + var b any = &book{} + r, err := caller.CallMethod(b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, &harryPotter, b) + }) + t.Run("var v any = &book{}; CallMethod(&v, ...", func(t *testing.T) { + t.Parallel() + + var b any = &book{} + r, err := caller.CallMethod(&b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, &harryPotter, b) + }) + t.Run("var v interface{ SetTitle(string) } = &book{}; CallMethod(v, ...", func(t *testing.T) { + t.Parallel() + + var b interface{ SetTitle(string) } = &book{} //nolint:inamedparam + r, err := caller.CallMethod(b, "SetTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Len(t, r, 0) + assert.Equal(t, &harryPotter, b) + }) + }) + }) + // Methods with a value receiver do not have any constraints + t.Run("Value receiver", func(t *testing.T) { + t.Parallel() + + t.Run("b := book{}", func(t *testing.T) { + t.Parallel() + + b := book{} + r, err := caller.CallWither(b, "WithTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Equal(t, harryPotter, r) + assert.Zero(t, b) + }) + t.Run("b := &book{}", func(t *testing.T) { + t.Parallel() + + b := &book{} + r, err := caller.CallWither(b, "WithTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Equal(t, harryPotter, r) + assert.Equal(t, &emptyBook, b) + }) + t.Run("var b any = book{}", func(t *testing.T) { + t.Parallel() + + var b any = book{} + r, err := caller.CallWither(b, "WithTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Equal(t, harryPotter, r) + assert.Equal(t, emptyBook, b) + }) + t.Run("var b any = &book{}", func(t *testing.T) { + t.Parallel() + + var b any = &book{} + r, err := caller.CallWither(b, "WithTitle", []any{harryPotterTitle}, false) + assert.NoError(t, err) + assert.Equal(t, harryPotter, r) + assert.Equal(t, &emptyBook, b) + }) + }) + t.Run("Unexported method", func(t *testing.T) { + t.Parallel() + + t.Run("CallMethod", func(t *testing.T) { + t.Parallel() + + b := book{} + _, err := caller.CallMethod(&b, "setTitle", []any{harryPotter}, false) + assert.EqualError(t, err, `cannot call method (*caller_test.book)."setTitle": invalid func (*caller_test.book)."setTitle"`) + }) + t.Run("ForceCallMethod", func(t *testing.T) { + t.Parallel() + + b := book{} + _, err := caller.ForceCallMethod(&b, "setTitle", []any{harryPotter}, false) + assert.EqualError(t, err, `cannot call method (*caller_test.book)."setTitle": invalid func (*caller_test.book)."setTitle"`) + }) + }) + }) +} diff --git a/caller/doc.go b/caller/doc.go new file mode 100644 index 0000000..acb9f6c --- /dev/null +++ b/caller/doc.go @@ -0,0 +1,84 @@ +// 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 caller provides functions that allow calling other functions with unknown arguments. + +# Example + + sum := func(a, b int) int { + return a + b + } + + returns, _ := caller.Call(sum, []any{2, 3}, false) + fmt.Println(returns) // [5] + +# Provider + +It is a function that returns 1 or 2 values. The first value is the desired output of the provider. +The optional second value may contain information about a potential error. + +Provider that does not return any error: + + func NewHttpClient(timeout time.Duration) *http.Client { + return &http.Client{ + Timeout: timeout, + } + } + + // httpClient, _ := caller.CallProvider(NewHttpClient, time.Minute) + +Provider that may return an error: + + func NewDB(username string, password string) (any, error) { + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/test", username, password)) + if err != nil { + return nil, err + } + + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + + return db, nil + } + + // db, err := caller.CallProvider(NewDB, []any{"root", "root"}, false) + // if err != nil { + // panic(err) + // } + +# Wither + +It is a method that returns one value always: + + type Person struct { + Name string + } + + func (p Person) WithName(n string) Person { // it is a wither + p.Name = n + return p + } + + // p, _ := caller.CallWither(caller.Person{}, "WithName", []any{"Jane"}, false) + // fmt.Println(p) // {Jane} +*/ +package caller diff --git a/caller/errors.go b/caller/errors.go new file mode 100644 index 0000000..76d8117 --- /dev/null +++ b/caller/errors.go @@ -0,0 +1,62 @@ +// 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 caller + +import ( + "github.com/gontainer/grouperror" +) + +/* +ProviderError wraps errors returned by providers in [CallProvider]. + + type myError struct { + error + } + + p := func() (any, error) { + return nil, &myError{errors.New("my error")} + } + + _, err := caller.CallProvider(p, nil, false) + if err != nil { + var providerErr *caller.ProviderError + if errors.As(err, &providerErr) { + fmt.Println("provider returned error:", providerErr) + } else { + fmt.Println("provider wasn't invoked:", err) + } + } +*/ +type ProviderError struct { + error +} + +func newProviderError(err error) *ProviderError { + return &ProviderError{error: err} +} + +func (e *ProviderError) Collection() []error { + return grouperror.Collection(e.error) +} + +func (e *ProviderError) Unwrap() error { + return e.error +} diff --git a/caller/errors_test.go b/caller/errors_test.go new file mode 100644 index 0000000..03fd844 --- /dev/null +++ b/caller/errors_test.go @@ -0,0 +1,40 @@ +// 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 caller //nolint:testpackage + +import ( + "errors" + "testing" + + "github.com/gontainer/grouperror" + errAssert "github.com/gontainer/grouperror/assert" +) + +func TestProviderError_Collection(t *testing.T) { + t.Parallel() + + err := newProviderError(grouperror.Prefix("prefix: ", errors.New("error 1"), errors.New("error 2"))) + expected := []string{ + `prefix: error 1`, + `prefix: error 2`, + } + errAssert.EqualErrorGroup(t, err, expected) +} diff --git a/caller/examples_test.go b/caller/examples_test.go new file mode 100644 index 0000000..14488db --- /dev/null +++ b/caller/examples_test.go @@ -0,0 +1,104 @@ +// 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 caller_test + +import ( + "fmt" + + "github.com/gontainer/reflectpro/caller" +) + +type Person struct { + name string +} + +func NewPerson(name string) *Person { + return &Person{name: name} +} + +func (p *Person) SetName(n string) { + p.name = n +} + +func (p Person) WithName(n string) Person { + p.name = n + + return p +} + +func ExampleCall_ok() { + p := &Person{} + _, _ = caller.Call(p.SetName, []any{"Mary"}, false) + fmt.Println(p.name) + // Output: Mary +} + +func ExampleCall_returnValue() { + fn := func(a int, b int) int { + return a * b + } + r, _ := caller.Call(fn, []any{3, 2}, false) + fmt.Println(r[0]) + // Output: 6 +} + +func ExampleCall_error() { + fn := func(a int, b int) int { + return a * b + } + _, err := caller.Call(fn, []any{"2", "2"}, true) + + fmt.Println(err) + // Output: + // cannot call func(int, int) int: arg0: cannot convert string to int + // cannot call func(int, int) int: arg1: cannot convert string to int +} + +func ExampleCall_error2() { + fn := func(a int, b int) int { + return a * b + } + _, err := caller.Call(fn, []any{"2", "2"}, false) + fmt.Println(err) + // Output: + // cannot call func(int, int) int: arg0: value of type string is not assignable to type int + // cannot call func(int, int) int: arg1: value of type string is not assignable to type int +} + +func ExampleCallProvider() { + p, _ := caller.CallProvider(NewPerson, []any{"Mary"}, false) + fmt.Printf("%+v", p) + // Output: &{name:Mary} +} + +func ExampleCallMethod() { + p := &Person{} + _, _ = caller.CallMethod(p, "SetName", []any{"Mary"}, false) + fmt.Println(p.name) + // Output: Mary +} + +func ExampleCallWither() { + p := Person{} + p2, _ := caller.CallWither(p, "WithName", []any{"Mary"}, false) + fmt.Printf("%+v", p2) + // Output: {name:Mary} +} diff --git a/caller/internal/caller/any.go b/caller/internal/caller/any.go new file mode 100644 index 0000000..ad741ac --- /dev/null +++ b/caller/internal/caller/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 caller + +type any = interface{} //nolint diff --git a/caller/internal/caller/call.go b/caller/internal/caller/call.go new file mode 100644 index 0000000..eec7a84 --- /dev/null +++ b/caller/internal/caller/call.go @@ -0,0 +1,186 @@ +// 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 caller + +import ( + "errors" + "fmt" + "reflect" + + "github.com/gontainer/grouperror" + intReflect "github.com/gontainer/reflectpro/internal/reflect" +) + +type reflectType struct { + reflect.Type +} + +// In works almost same as reflect.Type.In, +// but it returns t.In(t.NumIn() - 1).Elem() for t.isVariadic() && i >= t.NumIn(). +func (t reflectType) In(i int) reflect.Type { + last := t.NumIn() - 1 + if i > last { + i = last + } + + r := t.Type.In(i) + + if t.IsVariadic() && i == last { + r = r.Elem() + } + + return r +} + +// ValidateAndCallFunc validates and calls the given func. +// +// fn.Kind() MUST BE equal to [reflect.Func]. +func ValidateAndCallFunc(fn reflect.Value, args []any, convertArgs bool, v FuncValidator) ([]any, error) { + if v != nil { + if err := v.Validate(fn); err != nil { + return nil, err //nolint:wrapcheck + } + } + + return CallFunc(fn, args, convertArgs) +} + +// CallFunc calls the given func. +// +// fn.Kind() MUST BE equal to [reflect.Func]. +func CallFunc(fn reflect.Value, args []any, convertArgs bool) ([]any, error) { + fnType := reflectType{fn.Type()} + + if len(args) > fnType.NumIn() && !fnType.IsVariadic() { + return nil, errors.New("too many input arguments") + } + + minParams := fnType.NumIn() + if fnType.IsVariadic() { + minParams-- + } + + if len(args) < minParams { + return nil, errors.New("not enough input arguments") + } + + var ( + argsVals = make([]reflect.Value, len(args)) + errs = make([]error, 0, len(args)) + ) + + for i, p := range args { + var ( + convertTo = fnType.In(i) + err error + ) + + argsVals[i], err = intReflect.ValueOf(p, convertTo, convertArgs) + + if err != nil { + errs = append(errs, grouperror.Prefix(fmt.Sprintf("arg%d: ", i), err)) + } + } + + if len(errs) > 0 { + return nil, grouperror.Join(errs...) //nolint:wrapcheck + } + + var result []any + + if fn.Type().NumOut() > 0 { + result = make([]any, fn.Type().NumOut()) + } + + for i, v := range fn.Call(argsVals) { + result[i] = v.Interface() + } + + return result, nil +} + +//nolint:wrapcheck,cyclop +func ValidateAndForceCallMethod( + object any, + method string, + args []any, + convertArgs bool, + v FuncValidator, +) ( + []any, + error, +) { + val := reflect.ValueOf(object) + if val.Kind() != reflect.Ptr { + return nil, fmt.Errorf("expected %s, %T given", reflect.Ptr.String(), object) + } + + chain, err := intReflect.ValueToKindChain(val) + if err != nil { + return nil, err + } + + // see [intReflect.Set] + 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 + } + + if len(chain) == 2 && chain.Prefixed(reflect.Ptr) { + fn, err := MethodByName(val, method) + if err != nil { + return nil, err + } + + return ValidateAndCallFunc(fn, args, convertArgs, v) + } + + if len(chain) == 3 && chain.Prefixed(reflect.Ptr, reflect.Interface) { + cp := reflect.New(val.Elem().Elem().Type()) + cp.Elem().Set(val.Elem().Elem()) + + fn, err := MethodByName(cp, method) + if err != nil { + return nil, err + } + + res, err := ValidateAndCallFunc(fn, args, convertArgs, v) + if err == nil { + val.Elem().Set(cp.Elem()) + } + + return res, err + } + + panic("ValidateAndForceCallMethod: unexpected error") // this should be unreachable +} diff --git a/caller/internal/caller/funcs.go b/caller/internal/caller/funcs.go new file mode 100644 index 0000000..453fa5d --- /dev/null +++ b/caller/internal/caller/funcs.go @@ -0,0 +1,79 @@ +// 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 caller + +import ( + "fmt" + "reflect" + + intReflect "github.com/gontainer/reflectpro/internal/reflect" +) + +func Func(fn any) (reflect.Value, error) { + v := reflect.ValueOf(fn) + if !v.IsValid() { + return reflect.Value{}, fmt.Errorf("invalid func: %T", fn) + } + + if v.Kind() != reflect.Func { + return reflect.Value{}, fmt.Errorf("expected %s, %T given", reflect.Func.String(), fn) + } + + return v, nil +} + +const ( + invalidMethodErr = "invalid func (%T).%+q" +) + +func Method(object any, method string) (reflect.Value, error) { + obj := reflect.ValueOf(object) + if !obj.IsValid() { + return reflect.Value{}, fmt.Errorf("invalid method receiver: %T", object) + } + + fn := obj.MethodByName(method) + + _, err := intReflect.ValueToKindChain(obj) + if err != nil { + return reflect.Value{}, err //nolint:wrapcheck + } + + for !fn.IsValid() && (obj.Kind() == reflect.Ptr || obj.Kind() == reflect.Interface) { + obj = obj.Elem() + fn = obj.MethodByName(method) + } + + if !fn.IsValid() { + return reflect.Value{}, fmt.Errorf(invalidMethodErr, object, method) + } + + return fn, nil +} + +func MethodByName(val reflect.Value, method string) (reflect.Value, error) { + fn := val.MethodByName(method) + if !fn.IsValid() { + return reflect.Value{}, fmt.Errorf(invalidMethodErr, val.Interface(), method) + } + + return fn, nil +} diff --git a/caller/internal/caller/validators.go b/caller/internal/caller/validators.go new file mode 100644 index 0000000..1e65382 --- /dev/null +++ b/caller/internal/caller/validators.go @@ -0,0 +1,80 @@ +// 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 caller + +import ( + "fmt" + "reflect" +) + +//nolint:gochecknoglobals +var ( + DontValidate FuncValidator = nil //nolint + ValidatorWither = ChainValidator{validateWither} + ValidatorProvider = ChainValidator{validateProvider} +) + +//nolint:gochecknoglobals +var ( + errorInterface = reflect.TypeOf((*error)(nil)).Elem() +) + +type FuncValidator interface { + Validate(reflect.Value) error //nolint:inamedparam +} + +type ChainValidator []func(reflect.Value) error + +func (f ChainValidator) Validate(fn reflect.Value) error { + for _, v := range f { + if err := v(fn); err != nil { + return err + } + } + + return nil +} + +func validateWither(fn reflect.Value) error { + if fn.Type().NumOut() != 1 { + return fmt.Errorf("wither must return 1 value, given function returns %d values", fn.Type().NumOut()) + } + + return nil +} + +func validateProvider(fn reflect.Value) error { + if fn.Type().NumOut() == 0 || fn.Type().NumOut() > 2 { + return fmt.Errorf( + "provider must return 1 or 2 values, given function returns %d values", + fn.Type().NumOut(), + ) + } + + if fn.Type().NumOut() == 2 && !fn.Type().Out(1).Implements(errorInterface) { + return fmt.Errorf( + "second value returned by provider must implement error interface, %s given", + fn.Type().Out(1).String(), + ) + } + + return nil +} diff --git a/copier/README.md b/copier/README.md index 2d218e3..e6c7b4e 100644 --- a/copier/README.md +++ b/copier/README.md @@ -35,7 +35,7 @@ package main import ( "fmt" - "github.com/gontainer/gontainer-helpers/v3/copier" + "github.com/gontainer/reflectpro/copier" ) type Person struct {