From 54fed8bc20029180e2ebf5d4e6bc3c59d9c3b945 Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Tue, 12 Oct 2021 13:55:53 +0700 Subject: [PATCH] Add generic interactor constructor (#8) --- .github/workflows/tip.yml | 33 ++++++++++++++++++++++++++ Makefile | 2 +- README.md | 30 +++++++++++++++++++++++ generic_go1.18.go | 48 +++++++++++++++++++++++++++++++++++++ generic_go1.18_test.go | 50 +++++++++++++++++++++++++++++++++++++++ go.mod | 10 ++++++-- go.sum | 4 ++-- interactor.go | 13 ++++++---- 8 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/tip.yml create mode 100644 generic_go1.18.go create mode 100644 generic_go1.18_test.go diff --git a/.github/workflows/tip.yml b/.github/workflows/tip.yml new file mode 100644 index 0000000..400f238 --- /dev/null +++ b/.github/workflows/tip.yml @@ -0,0 +1,33 @@ +name: tip +on: + push: + branches: + - master + - main + pull_request: +env: + GO111MODULE: "on" + RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing. +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Go cache + uses: actions/cache@v2 + with: + # In order: + # * Module download cache + # * Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-cache-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-cache + - name: Test + uses: docker://aleksi/golang-tip:master + with: + entrypoint: /bin/sh + args: -c "go version;make test-unit" diff --git a/Makefile b/Makefile index 46bfa47..8f0589b 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ ifeq ($(DEVGO_PATH),) DEVGO_PATH := $(shell GO111MODULE=on $(GO) list ${modVendor} -f '{{.Dir}}' -m github.com/bool64/dev) ifeq ($(DEVGO_PATH),) $(info Module github.com/bool64/dev not found, downloading.) - DEVGO_PATH := $(shell export GO111MODULE=on && $(GO) mod tidy && $(GO) list -f '{{.Dir}}' -m github.com/bool64/dev) + DEVGO_PATH := $(shell export GO111MODULE=on && $(GO) get github.com/bool64/dev && $(GO) list -f '{{.Dir}}' -m github.com/bool64/dev) endif endif diff --git a/README.md b/README.md index e4c8e3a..9009bbd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ This abstraction is intended for use with automated transport layer, for example ## Usage +### Input/Output Definitions + ```go // Configure use case interactor in application layer. type myInput struct { @@ -36,6 +38,10 @@ type myOutput struct { Value2 string `json:"value2"` } +``` +### Classic API + +```go u := usecase.NewIOI(new(myInput), new(myOutput), func(ctx context.Context, input, output interface{}) error { var ( in = input.(*myInput) @@ -53,6 +59,30 @@ u := usecase.NewIOI(new(myInput), new(myOutput), func(ctx context.Context, input return nil }) +``` + +### Generic API with type parameters + +With `go1.18` and later (or [`gotip`](https://pkg.go.dev/golang.org/dl/gotip)) you can use simplified generic API instead +of classic API based on `interface{}`. + +```go +u := usecase.NewInteractor(func(ctx context.Context, input myInput, output *myOutput) error { + if in.Param1%2 != 0 { + return status.InvalidArgument + } + + // Do something to set output based on input. + out.Value1 = in.Param1 + in.Param1 + out.Value2 = in.Param2 + in.Param2 + + return nil +}) +``` + +### Further Configuration And Usage + +```go // Additional properties can be configured for purposes of automated documentation. u.SetTitle("Doubler") u.SetDescription("Doubler doubles parameter values.") diff --git a/generic_go1.18.go b/generic_go1.18.go new file mode 100644 index 0000000..0e62dcb --- /dev/null +++ b/generic_go1.18.go @@ -0,0 +1,48 @@ +//go:build go1.18 +// +build go1.18 + +package usecase + +import ( + "context" + "fmt" +) + +type IOInteractorOf[i, o interface{}] struct { + IOInteractor + + InteractFunc func(ctx context.Context, input i, output *o) error +} + +func (ioi IOInteractorOf[i, o]) Invoke(ctx context.Context, input i, output *o) error { + return ioi.InteractFunc(ctx, input, output) +} + +// NewInteractor creates generic use case interactor with input and output ports. +// +// It pre-fills name and title with caller function. +// Input is passed by value, while output is passed by pointer to be mutable. +func NewInteractor[i, o any](interact func(ctx context.Context, input i, output *o) error) IOInteractorOf[i, o] { + u := IOInteractorOf[i, o]{} + u.Input = *new(i) + u.Output = new(o) + u.InteractFunc = interact + u.Interactor = Interact(func(ctx context.Context, input, output interface{}) error { + inp, ok := input.(i) + if !ok { + return fmt.Errorf("invalid input type received: %T, expected: %T", input, u.Input) + } + + out, ok := output.(*o) + if !ok { + return fmt.Errorf("invalid output type received: %T, expected: %T", output, u.Output) + } + + return interact(ctx, inp, out) + }) + + u.name, u.title = callerFunc() + u.name = filterName(u.name) + + return u +} diff --git a/generic_go1.18_test.go b/generic_go1.18_test.go new file mode 100644 index 0000000..926334b --- /dev/null +++ b/generic_go1.18_test.go @@ -0,0 +1,50 @@ +//go:build go1.18 +// +build go1.18 + +package usecase_test + +import ( + "context" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/swaggest/usecase" +) + +func TestNewIOI_classic(t *testing.T) { + u := usecase.NewIOI(*new(int), new(string), func(ctx context.Context, input, output interface{}) error { + in := input.(int) + out := output.(*string) + + *out = strconv.Itoa(in) + + return nil + }) + + ctx := context.Background() + + var out string + assert.NoError(t, u.Interact(ctx, 123, &out)) + assert.Equal(t, "123", out) +} + +func TestNewInteractor(t *testing.T) { + u := usecase.NewInteractor(func(ctx context.Context, input int, output *string) error { + *output = strconv.Itoa(input) + + return nil + }) + + u.SetDescription("Foo.") + + ctx := context.Background() + + var out string + assert.NoError(t, u.Interact(ctx, 123, &out)) + assert.Equal(t, "123", out) + + out = "" + assert.NoError(t, u.Invoke(ctx, 123, &out)) + assert.Equal(t, "123", out) +} diff --git a/go.mod b/go.mod index cd51034..9429b54 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,14 @@ module github.com/swaggest/usecase -go 1.12 +go 1.18 require ( - github.com/bool64/dev v0.1.41 + github.com/bool64/dev v0.1.42 github.com/stretchr/testify v1.4.0 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect +) diff --git a/go.sum b/go.sum index 8c09673..dd4417e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/bool64/dev v0.1.41 h1:L554LCQZc3d7mtcdPUgDbSrCVbr48/30zgu0VuC/FTA= -github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= +github.com/bool64/dev v0.1.42 h1:Ps0IvNNf/v1MlIXt8Q5YKcKjYsIVLY/fb/5BmA7gepg= +github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/interactor.go b/interactor.go index a30ab97..23a8898 100644 --- a/interactor.go +++ b/interactor.go @@ -184,10 +184,7 @@ func NewIOI(input, output interface{}, interact Interact) IOInteractor { u.Interactor = interact u.name, u.title = callerFunc() - - u.name = strings.TrimPrefix(u.name, "internal/") - u.name = strings.TrimPrefix(u.name, "usecase.") - u.name = strings.TrimPrefix(u.name, "./main.") + u.name = filterName(u.name) return u } @@ -199,6 +196,14 @@ var titleReplacer = strings.NewReplacer( ")", "", ) +func filterName(name string) string { + name = strings.TrimPrefix(name, "internal/") + name = strings.TrimPrefix(name, "usecase.") + name = strings.TrimPrefix(name, "./main.") + + return name +} + // callerFunc returns trimmed path and name of parent function. func callerFunc() (string, string) { skipFrames := 2