3. Use Cases
Following the BookingFly
use case (.torpedo/use_cases/booking_fly.yaml
), Torpedo will generate the skeleton code where the custom logic should be placed.
The use case dir struct is not mandatory, but it is strongly suggested as:
| booking-fly/
|_ .torpedo
|_ dependency
|_ domain
|_ ...
|_ use_cases
|_ onboarding
|_ inputs
|_ http // dir to add your controllers logic and DTOs.
|_ outputs
|_ mongodb // dir to put all your repository logic related to your use case.
|_ testing
|_ mocks // dir to add your custom JSON mocks files or others.
|
|_ torpedo_use_case_base.go // autogenerated struct with defined entities dependencies.
|_ use_case.go // UseCase struct where your use case code must be writen
|_ use_case_test.go // use case tests
|_ ...
|
After get the use case code struct ready to use we should define its method or methods.
Domain Service
If we need to export our Domain Service
the use case should be bound into the Service
struct.
Remember that Domain Service
is implemented following the Facade pattern.
For further documentation please read Domain Service section
Let's start writing the use case
Adding use case method
Based on our Booking Fly App example, the auto generated BookingFly
use case code looks like:
booking-fly/domain/use_cases/booking_fly/use_case.go
| // Package booking_fly Fly reservation use case
package booking_fly
import (
"github.com/darksubmarine/torpedo-lib-go/context"
"github.com/darksubmarine/torpedo-lib-go/log"
"github.com/darksubmarine/booking-fly/domain/entities/user"
"github.com/darksubmarine/booking-fly/domain/entities/trip"
)
// UseCase struct that implements the use case business logic.
type UseCase struct {
*UseCaseBase
/* Put here your custom use case attributes */
}
// NewUseCase creates a new instance.
func NewUseCase(logger log.ILogger, userSrv user.IService, tripSrv trip.IService) *UseCase {
return &UseCase{
UseCaseBase: NewUseCaseBase(logger,
userSrv, tripSrv,
)}
}
/*
Write here all the methods to cover your use case logic!
func (uc *UseCase) YourAwesomeUseCase() error { return nil }
*/
|
Remember that our case is
Name: BookingFly
Description
Given a frequent flyer user should be able to do a booking fly from our well known fly routes, selecting the
departure airport and the arrival airport, also setting up the from-to fly dates. If the booking is successful, so the
system should calculate the user awards and upgrade it following the rules below:
Rule 1:
IF the user.Plan is GOLD
AND the user accumulated miles (current user miles + trip.Miles) are greater than 1500
AND the trip.Miles are greater than 2000
THEN
user.Miles = current user miles + trip.Miles + 1000
ELSE
user.Miles = current user miles + trip.Miles + 200
Rule 2:
IF the user.Plan is SILVER
AND the user accumulated miles (current user miles + trip.Miles) are greater than 8000
THEN
user.Miles = current user miles + trip.Miles + 500
user.Plan = GOLD
Rule 3:
IF the user.Plan is BRONZE
AND the user accumulated miles (current user miles + trip.Miles) are greater than 4000
THEN
user.Miles = current user miles + trip.Miles + 300
user.Plan = SILVER
lets adding a method named DoBooking
to our use case:
| // ErrUserNotFound user not found with given Id
var ErrUserNotFound = errors.New("user not found with given Id") //(1)!
// DoBooking method that performs a trip reservation updating user plan and miles.
func (uc *UseCase) DoBooking(tripModel *trip.TripEntity) (*trip.TripEntity, error) {
//1. creates the trip data
tripCreated, err := uc.tripSrv.Create(context.NoOpDataMap, tripModel)
if err != nil {
uc.logger.Error("something was wrong at booking creation", "error", err)
return nil, err
}
//2. fetch the user linked to the trip
userModel, err := uc.userSrv.Read(context.NoOpDataMap, tripCreated.UserId())
if err != nil {
uc.logger.Error("something was wrong getting the booking user", "error", err)
return nil, ErrUserNotFound
}
//3. prepare trip award information
tripMiles := tripCreated.Miles()
accumulatedMiles := userModel.Miles() + tripMiles
awardMiles := accumulatedMiles
userPlan := userModel.Plan()
switch userPlan {
case "GOLD":
if accumulatedMiles > 15000 && tripMiles > 2000 {
awardMiles += 1000
} else {
awardMiles += 200
}
case "SILVER":
if accumulatedMiles > 8000 {
awardMiles += 500
userPlan = "GOLD"
}
case "BRONZE":
if accumulatedMiles > 4000 {
awardMiles += 300
userPlan = "SILVER"
}
}
userModel.SetMiles(awardMiles)
userModel.SetPlan(userPlan)
//4. save user with updated award information based on the previous rules.
if _, err := uc.userSrv.Update(context.NoOpDataMap, userModel); err != nil {
uc.logger.Error("something was wrong updating the user", "error", err)
return nil, err
}
return tripCreated, nil
}
|
- This error and others useful to use case error handling can be placed within a file named
errors.go
.
For illustrative purpose has been added as part of the use_case.go
file.
Import errors package
After adding the method DoBooking check if the IDE added the errors
package to imports section. If not add it by your own.
| import (
"errors"
"github.com/darksubmarine/torpedo-lib-go/context"
"github.com/darksubmarine/torpedo-lib-go/log"
"github.com/darksubmarine/booking-fly/domain/entities/user"
"github.com/darksubmarine/booking-fly/domain/entities/trip"
)
|
Adding use case endpoint to REST API
It is time to expose our use case via the REST API. In order to do it we need to create two files:
- dto.go which is the input data model that we will be reading from the endpoint body
- controller.go where the input logic will be placed, http function in this case.
So, let's create those files:
domain/use_cases/booking_fly/inputs/http/dto.go
| package http
// BookingFlyDTO use case DTO to book a fly
type BookingFlyDTO struct {
Departure_ *string `json:"departure,omitempty"`
Arrival_ *string `json:"arrival,omitempty"`
Miles_ *int64 `json:"miles,omitempty"`
From_ *int64 `json:"from,omitempty"`
To_ *int64 `json:"to,omitempty"`
UserId_ *string `json:"userId,omitempty"`
} //@name useCases.BookingFlyDTO
|
domain/use_cases/booking_fly/inputs/http/controller.go
| package http
import (
"errors"
"fmt"
"github.com/darksubmarine/booking-fly/domain/entities/trip"
tripHTTP "github.com/darksubmarine/booking-fly/domain/entities/trip/inputs/http/gin"
"github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly"
"github.com/darksubmarine/torpedo-lib-go/api"
"github.com/darksubmarine/torpedo-lib-go/entity"
"github.com/darksubmarine/torpedo-lib-go/log"
"github.com/gin-gonic/gin"
"net/http"
)
// Controller struct that handles HTTP Requests for the use case
type Controller struct {
logger log.ILogger
ucBookingFly *booking_fly.UseCase //(1)!
}
// NewController controller constructor function
func NewController(useCase *booking_fly.UseCase, logger log.ILogger) *Controller {
return &Controller{ucBookingFly: useCase, logger: logger}
}
// BookingFlyEndpoint HTTP handler function that calls the use case DoBooking method.
// @Summary Books a fly
// @Schemes http https
// @Description Books a fly given a user and the trip information
// @Tags UseCases
// @Accept json
// @Produce json
// @Param trip body BookingFlyDTO true "The user fly trip reservation"
// @Success 200 {object} trip.FullDTO
// @Failure 400 {object} api.Error
// @Failure 500 {object} api.Error
// @Router /booking [post]
func (c *Controller) BookingFlyEndpoint(ctx *gin.Context) {
var dto BookingFlyDTO
if err := ctx.ShouldBindJSON(&dto); err != nil {
ctx.JSON(http.StatusBadRequest, api.ErrorBindingJSON(err))
return
}
tripModel := trip.New()
if err := entity.From(&dto, tripModel); err != nil {
ctx.JSON(http.StatusBadRequest, api.ErrorBuildingEntityFromDTO(err))
return
}
tripCreated, err := c.ucBookingFly.DoBooking(tripModel) //(2)!
if err != nil {
if errors.Is(err, booking_fly.ErrUserNotFound) { //(3)!
ctx.JSON(http.StatusBadRequest, api.NewError("4007", err))
} else {
ctx.JSON(http.StatusInternalServerError, api.ErrorEntityCreation(err))
}
return
}
var tripDTO = tripHTTP.NewFullDTO()
if err := entity.To(tripCreated, tripDTO); err != nil {
ctx.JSON(http.StatusBadRequest, api.NewError("4006", fmt.Errorf("error copying model to DTO: %w", err)))
return
}
ctx.JSON(http.StatusOK, tripDTO)
}
|
- Reference to the use case business logic.
- Calls to use case logic.
- Error handling based on use case error.
Now with the previous files created we can register it as part of the REST API within its own dependency module
located in dependency/use_case_booking_fly.go
.
dependency/use_case_booking_fly.go
| package dependency
import (
"github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly"
BookingFlyHTTP "github.com/darksubmarine/booking-fly/domain/use_cases/booking_fly/inputs/http" //(1)!
"github.com/gin-gonic/gin" //(2)!
"github.com/darksubmarine/booking-fly/domain/entities/trip"
"github.com/darksubmarine/booking-fly/domain/entities/user"
"github.com/darksubmarine/torpedo-lib-go/app"
"github.com/darksubmarine/torpedo-lib-go/log"
)
type UseCaseBookingFlyProvider struct {
app.BaseProvider
// useCaseBookingFly provides an booking_fly.UseCase instance.
useCaseBookingFly *booking_fly.UseCase `torpedo.di:"provide"`
// logger instance provided by LoggerProvider.
logger log.ILogger `torpedo.di:"bind"`
// userSrv instance of user service.
userSrv user.IService `torpedo.di:"bind"`
// tripSrv instance of trip service.
tripSrv trip.IService `torpedo.di:"bind"`
// Uncomment following lines if your use case contains http input.
// api router group to add endpoints under /api prefix
apiV1 *gin.RouterGroup `torpedo.di:"bind,name=APIv1"` //(3)!
}
func NewUseCaseBookingFlyProvider() *UseCaseBookingFlyProvider {
return &UseCaseBookingFlyProvider{}
}
// Provide provides the use case instance.
func (p *UseCaseBookingFlyProvider) Provide(c app.IContainer) error {
p.useCaseBookingFly = booking_fly.NewUseCase(p.logger, p.userSrv, p.tripSrv)
p.apiV1.POST("/booking",
BookingFlyHTTP.NewController(p.useCaseBookingFly, p.logger).BookingFlyEndpoint) //(4)!
return nil
}
|
- Import the use case http package.
- Import the Gin Gonic dependency.
- Bind with the REST API instance.
- Adds the use case endpoint.