Skip to main content

Use Cases

Primitives

To build the application layer, Arrower offers a set of primitives:

PrimitiveDescription
Requestcan produce side effects and return data
Commandproduces side effects, e.g. mutate state
Querydoes not produce side effects and returns data
Jobproduces side effects

A Use Case can be generated with the CLI to save boilerplate. It creates two files in the application layer: the handler and a corresponding test, both ready for the actual logic to be implemented.

shared/application/hello-world.usecase.go
package application

import (
"context"
"errors"

"github.com/go-arrower/arrower/app"
)

var ErrHelloWorldFailed = errors.New("hello world failed")

func NewHelloWorldRequestHandler() app.Request[HelloWorldRequest, HelloWorldResponse] {
return &helloWorldRequestHandler{}
}

type helloWorldRequestHandler struct{}

type (
HelloWorldRequest struct{}
HelloWorldResponse struct{}
)

func (h *helloWorldRequestHandler) H(ctx context.Context, req HelloWorldRequest) (HelloWorldResponse, error) {
return HelloWorldResponse{}, nil
}
shared/application/hello-world.usecase_test.go
package application_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

"github.com/go-arrower/skeleton/shared/application"
)

func TestHelloWorldRequestHandler_H(t *testing.T) {
t.Parallel()

t.Run("success case", func(t *testing.T) {
t.Parallel()

handler := application.NewHelloWorldRequestHandler()

res, err := handler.H(context.Background(), application.HelloWorldRequest{})
assert.NoError(t, err)
assert.Empty(t, res)
})
}

Code Generation

$ arrower generate request helloWorld
$ arrower generate command helloWorld
$ arrower generate query helloWorld
$ arrower generate job helloWorld

# arrower detects the primitive when appended to the desired use case.
$ arrower generate uc helloWorldRequest

Or generate for a specific context:

$ arrower generate request <contextName> helloWorld

Instrumentation

The primitives make it straightforward to write middleware. Arrower ships with decorators for:

  • Tracing
  • Metrics
  • Logging
  • Validation
  • Transactions

They can be called with:

handler := app.NewLoggedRequest(
logger,
NewHelloWorldRequestHandler(),
)

To fully instrument a Use Case, rely on the convenience helper, which applies tracing, metrics, and logging at once:

handler := app.NewInstrumentedRequest(
di.TraceProvider, di.MeterProvider, di.Logger,
NewHelloWorldRequestHandler(),
),

Testing

For testing code with a Use Case dependency (e.g. controller), a bunch of helpers are ready for use.

Take a look at this test and how the app.TestRequestHandler is used to quickly assert on the specific input coming to the Use Case or returning data:

// ...

t.Run("successful request", func(t *testing.T) {
t.Parallel()

handler := app.NewValidatedRequest(validator.New(), app.TestRequestHandler(func(ctx context.Context, _ structWithValidationTags) (response, error) {
assert.True(t, app.PassedValidation(ctx))
return response{}, nil
}))

res, err := handler.H(context.Background(), passingValidationValue)
assert.NoError(t, err)
assert.Empty(t, res)
})

// ...

For many tests this level of control is not required. There are also helpers that always succeed app.TestSuccessRequestHandler or always fail app.TestFailureRequestHandler:

// ...

t.Run("success", func(t *testing.T) {
t.Parallel()

req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := echoRouter.NewContext(req, rec)

handler := web.NewHelloController(application.App{
SayHello: app.TestSuccessRequestHandler[application.SayHelloRequest, application.SayHelloResponse](),
})

assert.NoError(t, handler.SayHello()(c))
assert.Equal(t, http.StatusOK, rec.Code)
})

// ...