diff --git a/Makefile b/Makefile index 5c79baf..cada2a2 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ export GOPATH ?= $(shell $(GO) env GOPATH) export GOBIN ?= $(GOPATH)/bin # Setup go-make version to use desired build and config scripts. -GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.103 +GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.104 INSTALL_FLAGS ?= -mod=readonly -buildvcs=auto # Request targets from go-make targets target. TARGETS := $(shell command -v $(GOBIN)/go-make >/dev/null || \ diff --git a/VERSION b/VERSION index 44517d5..fe04e7f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.19 +0.0.20 diff --git a/test/reflect.go b/test/reflect.go index dee1215..3030899 100644 --- a/test/reflect.go +++ b/test/reflect.go @@ -5,66 +5,105 @@ import ( "unsafe" ) -// Error creates an accessor/build for the given error to access and modify its -// unexported fields by field name. -func Error(err error) *Accessor[error] { - return NewAccessor[error](err) +// Builder is a generic interface that allows you to access and modify +// unexported fields of a (pointer) struct by field name. +type Builder[T any] interface { + // Set sets the value of the field with the given name. If the name is empty, + // and of the same type the stored target instance is replaced by the given + // value. + Set(name string, value any) Builder[T] + // Get returns the value of the field with the given name. If the name is + // empty, the stored target instance is returned. + Get(name string) any + // Build returns the created/modified target instance of the builder. + Build() T } -// Accessor allows you to access and modify unexported fields of a struct. -type Accessor[T any] struct { - target T +// Builder allows you to access and modify unexported fields of a struct. +type builder[T any] struct { + target any wrapped bool } -// NewAccessor creates a generic accessor/builder for a given target struct. -// If the target is a pointer to a struct (template), the instance is stored -// and modified. If the target is a struct, a pointer to a new instance of is -// created, since a struct cannot be modified by reflection. -func NewAccessor[T any](target T) *Accessor[T] { +// NewBuilder creates a generic builder for a target struct type. The builder +// allows you to access and modify unexported fields of the struct by field +// name. +func NewBuilder[T any]() Builder[T] { + var target T + return NewAccessor[T](target) +} + +// NewAccessor creates a generic builder/accessor for a given target struct. +// The builder allows you to access and modify unexported fields of the struct +// by field name. +// +// If the target is a pointer to a struct (template), the pointer is stored +// and the instance is modified directly. If the target is a struct, it is +// ignored and a new pointer struct is created for modification, since a struct +// cannot be modified directly by reflection. +func NewAccessor[T any](target T) Builder[T] { value := reflect.ValueOf(target) - if value.Kind() == reflect.Ptr && value.Elem().Kind() == reflect.Struct { - return &Accessor[T]{ - target: value.Interface().(T), + + if value.Kind() == reflect.Ptr { + // Create a new instance if the pointer is nil. + if value.Elem().Kind() == reflect.Invalid { + target = reflect.New(value.Type().Elem()).Interface().(T) + value = reflect.ValueOf(target) } - } else if value.Kind() == reflect.Struct { - target = reflect.New(value.Type()).Interface().(T) - return &Accessor[T]{ - target: target, + if value.Elem().Kind() == reflect.Struct { + return &builder[T]{ + target: target, + } + } + } else if value.Kind() == reflect.Struct { + // Create a new pointer instance for modification. + value = reflect.New(value.Type()) + return &builder[T]{ + target: value.Interface(), wrapped: true, } } + panic("target must be a struct or pointer to struct") } // Set sets the value of the field with the given name. If the name is empty, // and of the same type the stored target instance is replaced by the given // value. -func (a *Accessor[T]) Set(name string, value any) *Accessor[T] { +func (b *builder[T]) Set(name string, value any) Builder[T] { if name != "" { - field := reflect.ValueOf(a.target).Elem().FieldByName(name) + field := reflect.ValueOf(b.target).Elem().FieldByName(name) // #nosec G103,G115 // This is a safe use of unsafe.Pointer. reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). Elem().Set(reflect.ValueOf(value)) - } else if reflect.TypeOf(a.target) == reflect.TypeOf(value) { - a.target = value.(T) + } else if reflect.TypeOf(b.target) == reflect.TypeOf(value) { + b.target = value } else { - panic("target must of compatible struct pointer type") + panic("target must be a compatible struct pointer") } - return a + return b } // Get returns the value of the field with the given name. If the name is // empty, the stored target instance is returned. -func (a *Accessor[T]) Get(name string) any { +func (b *builder[T]) Get(name string) any { if name != "" { - field := reflect.ValueOf(a.target).Elem().FieldByName(name) + field := reflect.ValueOf(b.target).Elem().FieldByName(name) // #nosec G103,G115 // This is a safe use of unsafe.Pointer. return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())). Elem().Interface() - } else if a.wrapped { - return reflect.ValueOf(a.target).Elem().Interface() + } else if b.wrapped { + return reflect.ValueOf(b.target).Elem().Interface() + } + return b.target +} + +// Build returns the created/modified target instance of the builder/accessor. +func (b *builder[T]) Build() T { + if b.wrapped { + return reflect.ValueOf(b.target).Elem().Interface().(T) + } else { + return b.target.(T) } - return a.target } diff --git a/test/reflect_test.go b/test/reflect_test.go index f669633..148cdc0 100644 --- a/test/reflect_test.go +++ b/test/reflect_test.go @@ -1,10 +1,10 @@ package test_test import ( - "errors" "testing" "github.com/stretchr/testify/assert" + "github.com/tkrop/go-testing/mock" "github.com/tkrop/go-testing/test" ) @@ -14,96 +14,194 @@ type Struct struct{ s string } func NewStruct(s string) Struct { return Struct{s: s} } func NewPtrStruct(s string) *Struct { return &Struct{s: s} } -type testAccessorParam struct { - target any - setup func(*test.Accessor[any]) +type testBuilderStructParam struct { + target Struct + setup func(test.Builder[Struct]) expect mock.SetupFunc - check func(test.Test, *test.Accessor[any]) + check func(test.Test, test.Builder[Struct]) } -var testAccessorParams = map[string]testAccessorParam{ - "test invalid type": { - target: int(1), - expect: test.Panic("target must be a struct or pointer to struct"), - }, - +var testBuilderStructParams = map[string]testBuilderStructParam{ "test struct get is empty - no copy possible": { target: NewStruct("test get"), - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "", a.Get("s")) - assert.Equal(t, NewStruct(""), a.Get("")) + check: func(t test.Test, b test.Builder[Struct]) { + assert.Equal(t, "", b.Get("s")) + assert.Equal(t, NewStruct(""), b.Get("")) + assert.Equal(t, NewStruct(""), b.Build()) }, }, "test struct set": { target: NewStruct("test set"), - setup: func(a *test.Accessor[any]) { - a.Set("s", "test set first"). + setup: func(b test.Builder[Struct]) { + b.Set("s", "test set first"). Set("s", "test set final") }, - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "test set final", a.Get("s")) - assert.Equal(t, NewStruct("test set final"), a.Get("")) + check: func(t test.Test, b test.Builder[Struct]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewStruct("test set final"), b.Get("")) + assert.Equal(t, NewStruct("test set final"), b.Build()) }, }, "test struct reset no pointer": { target: NewStruct("test reset"), - setup: func(a *test.Accessor[any]) { - a.Set("s", "test reset first"). + setup: func(b test.Builder[Struct]) { + b.Set("s", "test reset first"). Set("", NewStruct("test reset final")) }, - expect: test.Panic("target must of compatible struct pointer type"), + expect: test.Panic("target must be a compatible struct pointer"), }, "test struct reset pointer": { target: NewStruct("test reset"), - setup: func(a *test.Accessor[any]) { - a.Set("s", "test reset first"). + setup: func(b test.Builder[Struct]) { + b.Set("s", "test reset first"). + Set("", NewPtrStruct("test reset final")) + }, + check: func(t test.Test, b test.Builder[Struct]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewStruct("test reset final"), b.Get("")) + assert.Equal(t, NewStruct("test reset final"), b.Build()) + }, + }, +} + +func TestBuilderStruct(t *testing.T) { + // test.New[testBuilderStructParam](t, testBuilderStructParams["test struct reset pointer"]). + test.Map(t, testBuilderStructParams). + Run(func(t test.Test, param testBuilderStructParam) { + // Given + mock.NewMocks(t).Expect(param.expect) + accessor := test.NewAccessor(param.target) + + // When + if param.setup != nil { + param.setup(accessor) + } + + // The + param.check(t, accessor) + }) +} + +type testBuilderPtrStructParam struct { + target *Struct + setup func(test.Builder[*Struct]) + expect mock.SetupFunc + check func(test.Test, test.Builder[*Struct]) +} + +var testBuilderPtrStructParams = map[string]testBuilderPtrStructParam{ + "test nil any get": { + target: nil, + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "", b.Get("s")) + assert.Equal(t, NewPtrStruct(""), b.Get("")) + assert.Equal(t, NewPtrStruct(""), b.Build()) + }, + }, + + "test nil any set": { + target: nil, + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test set first"). + Set("s", "test set final") + }, + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test set final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test set final"), b.Build()) + }, + }, + + "test nil any reset": { + target: nil, + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test reset first"). + Set("", NewPtrStruct("test reset final")) + }, + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Build()) + }, + }, + + "test nil struct get": { + target: new(Struct), + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "", b.Get("s")) + assert.Equal(t, NewPtrStruct(""), b.Get("")) + assert.Equal(t, NewPtrStruct(""), b.Build()) + }, + }, + + "test nil struct set": { + target: new(Struct), + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test set first"). + Set("s", "test set final") + }, + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test set final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test set final"), b.Build()) + }, + }, + + "test nil struct reset": { + target: new(Struct), + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test reset first"). Set("", NewPtrStruct("test reset final")) }, - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "test reset final", a.Get("s")) - assert.Equal(t, NewStruct("test reset final"), a.Get("")) + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Build()) }, }, "test ptr get": { target: NewPtrStruct("test get"), - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "test get", a.Get("s")) - assert.Equal(t, NewPtrStruct("test get"), a.Get("")) + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test get", b.Get("s")) + assert.Equal(t, NewPtrStruct("test get"), b.Get("")) + assert.Equal(t, NewPtrStruct("test get"), b.Build()) }, }, "test ptr set": { target: NewPtrStruct("test set"), - setup: func(a *test.Accessor[any]) { - a.Set("s", "test set first"). + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test set first"). Set("s", "test set final") }, - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "test set final", a.Get("s")) - assert.Equal(t, NewPtrStruct("test set final"), a.Get("")) + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test set final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test set final"), b.Build()) }, }, "test ptr reset": { target: NewPtrStruct("test reset"), - setup: func(a *test.Accessor[any]) { - a.Set("s", "test reset first"). + setup: func(b test.Builder[*Struct]) { + b.Set("s", "test reset first"). Set("", NewPtrStruct("test reset final")) }, - check: func(t test.Test, a *test.Accessor[any]) { - assert.Equal(t, "test reset final", a.Get("s")) - assert.Equal(t, NewPtrStruct("test reset final"), a.Get("")) + check: func(t test.Test, b test.Builder[*Struct]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Build()) }, }, } -func TestAccessor(t *testing.T) { - test.Map(t, testAccessorParams). - Run(func(t test.Test, param testAccessorParam) { +func TestBuilderPtrStruct(t *testing.T) { + test.Map(t, testBuilderPtrStructParams). + Run(func(t test.Test, param testBuilderPtrStructParam) { // Given mock.NewMocks(t).Expect(param.expect) accessor := test.NewAccessor(param.target) @@ -113,56 +211,145 @@ func TestAccessor(t *testing.T) { param.setup(accessor) } - // The + // Then param.check(t, accessor) }) } -type testErrorParam struct { - error error - setup func(*test.Accessor[error]) - check func(test.Test, *test.Accessor[error]) +type testBuilderAnyParam struct { + target any + setup func(test.Builder[any]) + expect mock.SetupFunc + check func(test.Test, test.Builder[any]) } -var testErrorParams = map[string]testErrorParam{ - "test get": { - error: errors.New("test get"), - check: func(t test.Test, a *test.Accessor[error]) { - assert.Equal(t, "test get", a.Get("s")) - assert.Equal(t, errors.New("test get"), a.Get("")) +var testBuilderAnyParams = map[string]testBuilderAnyParam{ + "test invalid type": { + target: nil, // nil is type any. + expect: test.Panic("target must be a struct or pointer to struct"), + }, + + "test struct get is empty - no copy possible": { + target: NewStruct("test get"), + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "", b.Get("s")) + assert.Equal(t, NewStruct(""), b.Get("")) + assert.Equal(t, NewStruct(""), b.Build()) + }, + }, + + "test struct set": { + target: NewStruct("test set"), + setup: func(b test.Builder[any]) { + b.Set("s", "test set first"). + Set("s", "test set final") + }, + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewStruct("test set final"), b.Get("")) + assert.Equal(t, NewStruct("test set final"), b.Build()) + }, + }, + + "test struct reset no pointer": { + target: NewStruct("test reset"), + setup: func(b test.Builder[any]) { + b.Set("s", "test reset first"). + Set("", NewStruct("test reset final")) + }, + expect: test.Panic("target must be a compatible struct pointer"), + }, + + "test struct reset pointer": { + target: NewStruct("test reset"), + setup: func(b test.Builder[any]) { + b.Set("s", "test reset first"). + Set("", NewPtrStruct("test reset final")) + }, + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewStruct("test reset final"), b.Get("")) + assert.Equal(t, NewStruct("test reset final"), b.Build()) + }, + }, + + "test ptr get": { + target: NewPtrStruct("test get"), + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test get", b.Get("s")) + assert.Equal(t, NewPtrStruct("test get"), b.Get("")) + assert.Equal(t, NewPtrStruct("test get"), b.Build()) + }, + }, + + "test ptr set": { + target: NewPtrStruct("test set"), + setup: func(b test.Builder[any]) { + b.Set("s", "test set first"). + Set("s", "test set final") + }, + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test set final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test set final"), b.Build()) + }, + }, + + "test ptr reset": { + target: NewPtrStruct("test reset"), + setup: func(b test.Builder[any]) { + b.Set("s", "test reset first"). + Set("", NewPtrStruct("test reset final")) + }, + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Build()) + }, + }, + + "test nil get": { + target: new(Struct), + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "", b.Get("s")) + assert.Equal(t, NewPtrStruct(""), b.Get("")) + assert.Equal(t, NewPtrStruct(""), b.Build()) }, }, - "test set": { - error: errors.New("test set"), - setup: func(a *test.Accessor[error]) { - a.Set("s", "test set first"). + "test nil set": { + target: new(Struct), + setup: func(b test.Builder[any]) { + b.Set("s", "test set first"). Set("s", "test set final") }, - check: func(t test.Test, a *test.Accessor[error]) { - assert.Equal(t, "test set final", a.Get("s")) - assert.Equal(t, errors.New("test set final"), a.Get("")) + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test set final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test set final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test set final"), b.Build()) }, }, - "test reset": { - error: errors.New("test set"), - setup: func(a *test.Accessor[error]) { - a.Set("s", "test set first"). - Set("", errors.New("test set final")) + "test nil reset": { + target: new(Struct), + setup: func(b test.Builder[any]) { + b.Set("s", "test reset first"). + Set("", NewPtrStruct("test reset final")) }, - check: func(t test.Test, a *test.Accessor[error]) { - assert.Equal(t, "test set final", a.Get("s")) - assert.Equal(t, errors.New("test set final"), a.Get("")) + check: func(t test.Test, b test.Builder[any]) { + assert.Equal(t, "test reset final", b.Get("s")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Get("")) + assert.Equal(t, NewPtrStruct("test reset final"), b.Build()) }, }, } -func TestError(t *testing.T) { - test.Map(t, testErrorParams). - Run(func(t test.Test, param testErrorParam) { +func TestBuilderAny(t *testing.T) { + test.Map(t, testBuilderAnyParams). + Run(func(t test.Test, param testBuilderAnyParam) { // Given - accessor := test.Error(param.error) + mock.NewMocks(t).Expect(param.expect) + accessor := test.NewAccessor(param.target) // When if param.setup != nil { @@ -173,3 +360,29 @@ func TestError(t *testing.T) { param.check(t, accessor) }) } + +func TestNewBuilderStruct(t *testing.T) { + // Given + builder := test.NewBuilder[Struct]() + + // When + builder.Set("s", "test set") + + // Then + assert.Equal(t, "test set", builder.Get("s")) + assert.Equal(t, NewStruct("test set"), builder.Get("")) + assert.Equal(t, NewStruct("test set"), builder.Build()) +} + +func TestNewBuilderPtrStruct(t *testing.T) { + // Given + builder := test.NewBuilder[*Struct]() + + // When + builder.Set("s", "test set") + + // Then + assert.Equal(t, "test set", builder.Get("s")) + assert.Equal(t, NewPtrStruct("test set"), builder.Get("")) + assert.Equal(t, NewPtrStruct("test set"), builder.Build()) +}