Skip to content

Commit

Permalink
Add generic interactor constructor (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Oct 12, 2021
1 parent fe22fff commit 54fed8b
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 9 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/tip.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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.")
Expand Down
48 changes: 48 additions & 0 deletions generic_go1.18.go
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions generic_go1.18_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 8 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
13 changes: 9 additions & 4 deletions interactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down

0 comments on commit 54fed8b

Please sign in to comment.