Skip to content

Commit

Permalink
Sqlite database, graceful shutdown, http server, readme file
Browse files Browse the repository at this point in the history
  • Loading branch information
kiaplayer committed Jul 10, 2024
1 parent d731b4e commit c5f1ac2
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SERVICE_ADDR=:3000
SQLITE_DB_FILE=sqlite.db
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/.*
!/.env
!/.gitignore
/*.db
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Clean architecture example

Simple service for orders management using сlean architecture principles.

Sqlite database is used as storage.

Default service configuration is loaded from `.env` file, but you can override any parameters from ENV.

## How to run

1) Run db migrations:
```
$ go install -tags 'sqlite3 sqlite' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
$ migrate -database "sqlite3://sqlite.db" -path db/migrations up
```

2) Start service (on port 3000 by default):
```
$ go run cmd/main.go
```

## How to use
```
$ curl --location 'localhost:3000/sale-order' \
--header 'Content-Type: application/json' \
--data '{"customer_id": 1, "products": [{"product_id":1, "quantity": 1}]}'
$ curl --location --request GET 'localhost:3000/sale-order?id=1'
```
63 changes: 58 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package main

import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"

"github.com/joho/godotenv"
_ "github.com/mattn/go-sqlite3"

"github.com/kiaplayer/clean-architecture-example/internal/adapters/repositories/sale_order"
saleorderservice "github.com/kiaplayer/clean-architecture-example/internal/domain/service/sale_order"
Expand All @@ -14,17 +22,62 @@ import (
)

func main() {
conn := &sql.DB{}
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file: %s", err)
}

dbConn, err := sql.Open("sqlite3", os.Getenv("SQLITE_DB_FILE"))
if err != nil {
log.Fatal(err)
}
defer func(dbConn *sql.DB) {
closeErr := dbConn.Close()
if closeErr != nil {
log.Fatal(closeErr)
}
}(dbConn)

transactor := db.NewTransactor(conn)
transactor := db.NewTransactor(dbConn)
timeGenerator := generators.NewTimeGenerator()
numberGenerator := generators.NewNumberGenerator()
saleOrderRepo := sale_order.NewRepository(conn)
saleOrderRepo := sale_order.NewRepository(dbConn)
saleOrderService := saleorderservice.NewService(saleOrderRepo)

create_sale_order.NewHandler(
createSaleOrderHandler := create_sale_order.NewHandler(
createsaleorderusecase.NewUseCase(timeGenerator, numberGenerator, saleOrderService),
transactor,
)
get_sale_order.NewHandler(getsaleorderusecase.NewUseCase(saleOrderService))
getSaleOrderHandler := get_sale_order.NewHandler(getsaleorderusecase.NewUseCase(saleOrderService))

srvMux := http.NewServeMux()
srvMux.HandleFunc("POST /sale-order", createSaleOrderHandler.Handle)
srvMux.HandleFunc("GET /sale-order", getSaleOrderHandler.Handle)

srv := http.Server{
Addr: os.Getenv("SERVICE_ADDR"),
Handler: srvMux,
}

idleConnsClosed := make(chan struct{})
go func() {
sigint := make(chan os.Signal, 1)
signal.Notify(sigint, os.Interrupt)
<-sigint

log.Println("Service shutting down...")

if err := srv.Shutdown(context.Background()); err != nil {
log.Printf("HTTP server Shutdown: %v", err)
}
close(idleConnsClosed)
}()

log.Printf("Service started at: %s", srv.Addr)

if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server ListenAndServe: %v", err)
}

<-idleConnsClosed
}
1 change: 1 addition & 0 deletions db/migrations/000001_create_sale_orders.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS sale_order;
7 changes: 7 additions & 0 deletions db/migrations/000001_create_sale_orders.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS sale_order
(
id INTEGER PRIMARY KEY,
number TEXT UNIQUE NOT NULL,
date TIMESTAMP WITH TIME ZONE NOT NULL,
status INTEGER NOT NULL DEFAULT 0
);
13 changes: 9 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
module github.com/kiaplayer/clean-architecture-example

go 1.21
go 1.22

require (
github.com/DATA-DOG/go-sqlmock v1.5.1
github.com/golang/mock v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/stretchr/testify v1.8.4
)

require (
github.com/DATA-DOG/go-sqlmock v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand Down Expand Up @@ -32,6 +36,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3 changes: 3 additions & 0 deletions internal/adapters/repositories/sale_order/sale_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"database/sql"
"fmt"
"slices"
"time"

"github.com/kiaplayer/clean-architecture-example/internal/domain/entity/document"
"github.com/kiaplayer/clean-architecture-example/pkg/helpers"
Expand Down Expand Up @@ -45,6 +46,8 @@ func (r *Repository) CreateOrder(ctx context.Context, order *document.SaleOrder)
}
order.ID = uint64(lastID)

time.Sleep(10 * time.Second)

return order, nil
}

Expand Down
17 changes: 11 additions & 6 deletions internal/adapters/repositories/sale_order/sale_order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,12 @@ func TestGetByID_Success(t *testing.T) {

func TestGetByID_QueryError(t *testing.T) {
// arrange
ctrl := gomock.NewController(t)
ctx := context.Background()

db, mock, _ := sqlmock.New()
queryExecutorMock := mocks.NewMockQueryExecutor(ctrl)

repository := NewRepository(db)
repository := NewRepository(queryExecutorMock)

saleOrder := &document.SaleOrder{
Document: document.Document{
Expand All @@ -192,10 +193,14 @@ func TestGetByID_QueryError(t *testing.T) {

queryError := errors.New("query error")

mock.
ExpectQuery("SELECT id, date, number, status FROM sale_order WHERE id = ?").
WithArgs(saleOrder.ID).
WillReturnError(queryError)
queryExecutorMock.
EXPECT().
QueryContext(
ctx,
"SELECT id, date, number, status FROM sale_order WHERE id = ?",
saleOrder.ID,
).
Return(nil, queryError)

// act
actualSaleOrder, getErr := repository.GetByID(ctx, saleOrder.ID)
Expand Down
4 changes: 2 additions & 2 deletions internal/handlers/create_sale_order/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (h *Handler) validateAndPrepare(request *http.Request) (*document.SaleOrder
return dto.SaleOrderDtoToSaleOrder(saleOrderDTO), nil
}

func (h *Handler) Handle(ctx context.Context, writer http.ResponseWriter, request *http.Request) {
func (h *Handler) Handle(writer http.ResponseWriter, request *http.Request) {
err := h.checkAccess(request)
if err != nil {
writer.WriteHeader(http.StatusForbidden)
Expand All @@ -70,7 +70,7 @@ func (h *Handler) Handle(ctx context.Context, writer http.ResponseWriter, reques
return
}

saleOrderUpdated, err := h.transactor.RunInTx(ctx, func(ctx context.Context) (any, error) {
saleOrderUpdated, err := h.transactor.RunInTx(request.Context(), func(ctx context.Context) (any, error) {
return h.useCase.Handle(ctx, saleOrder)
})
if err != nil {
Expand Down
16 changes: 6 additions & 10 deletions internal/handlers/create_sale_order/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestHandle_Success(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodPost, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand All @@ -72,7 +72,6 @@ func TestHandle_Success(t *testing.T) {
func TestHandle_checkAccessError(t *testing.T) {
// arrange
ctrl := gomock.NewController(t)
ctx := context.Background()

useCaseMock := mocks.NewMockuseCase(ctrl)
transactorMock := mocks.NewMocktransactor(ctrl)
Expand All @@ -83,7 +82,7 @@ func TestHandle_checkAccessError(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodDelete, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand All @@ -93,7 +92,6 @@ func TestHandle_checkAccessError(t *testing.T) {
func TestHandle_validateError_emptyRequest(t *testing.T) {
// arrange
ctrl := gomock.NewController(t)
ctx := context.Background()

useCaseMock := mocks.NewMockuseCase(ctrl)
transactorMock := mocks.NewMocktransactor(ctrl)
Expand All @@ -104,7 +102,7 @@ func TestHandle_validateError_emptyRequest(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodPost, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand All @@ -114,7 +112,6 @@ func TestHandle_validateError_emptyRequest(t *testing.T) {
func TestHandle_validateError_zeroProductID(t *testing.T) {
// arrange
ctrl := gomock.NewController(t)
ctx := context.Background()

useCaseMock := mocks.NewMockuseCase(ctrl)
transactorMock := mocks.NewMocktransactor(ctrl)
Expand All @@ -125,7 +122,7 @@ func TestHandle_validateError_zeroProductID(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodPost, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand All @@ -135,7 +132,6 @@ func TestHandle_validateError_zeroProductID(t *testing.T) {
func TestHandle_validateError_invalidJSON(t *testing.T) {
// arrange
ctrl := gomock.NewController(t)
ctx := context.Background()

useCaseMock := mocks.NewMockuseCase(ctrl)
transactorMock := mocks.NewMocktransactor(ctrl)
Expand All @@ -146,7 +142,7 @@ func TestHandle_validateError_invalidJSON(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodPost, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand Down Expand Up @@ -199,7 +195,7 @@ func TestHandle_UseCaseError(t *testing.T) {
request, requestErr := http.NewRequest(http.MethodPost, "", bodyReader)

// act
handler.Handle(ctx, response, request)
handler.Handle(response, request)

// assert
assert.NoError(t, requestErr)
Expand Down
9 changes: 7 additions & 2 deletions internal/handlers/get_sale_order/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func NewHandler(u useCase) *Handler {
}
}

func (h *Handler) Handle(ctx context.Context, writer http.ResponseWriter, request *http.Request) {
func (h *Handler) Handle(writer http.ResponseWriter, request *http.Request) {
err := h.checkAccess(request)
if err != nil {
writer.WriteHeader(http.StatusForbidden)
Expand All @@ -43,11 +43,16 @@ func (h *Handler) Handle(ctx context.Context, writer http.ResponseWriter, reques
return
}

saleOrder, err := h.useCase.Handle(ctx, saleOrderID)
saleOrder, err := h.useCase.Handle(request.Context(), saleOrderID)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
if saleOrder == nil {
writer.WriteHeader(http.StatusNotFound)
_, _ = writer.Write([]byte(fmt.Sprint("sale order not found")))
return
}

_, _ = writer.Write([]byte(fmt.Sprintf("SaleOrder ID = %d", saleOrder.ID)))

Expand Down
Loading

0 comments on commit c5f1ac2

Please sign in to comment.