Skip to content

Commit

Permalink
feat: Introduce domain package (bxcodec#21)
Browse files Browse the repository at this point in the history
* init

* refactor: move everything to domain

* add app

* remove unnecessar-things

* fix makefile

* ref: grouping the implementations in the same package

* chore(code-style): change the code styles

* chore: re-organize the middleware

* chore: add readme explanation

* chore: update Readme styling

* chore: add URL on link in readme
  • Loading branch information
bxcodec authored Apr 18, 2020
1 parent 0e2d18a commit d452858
Show file tree
Hide file tree
Showing 28 changed files with 370 additions and 418 deletions.
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,4 @@ exclude-rules:
- funlen
exclude-use-default: false
exclude:
- should have a package comment
- should have a package comment
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ EXPOSE 9090

COPY --from=builder /app/engine /app

CMD /app/engine
CMD /app/engine
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ test:
go test -v -cover -covermode=atomic ./...

engine:
go build -o ${BINARY} main.go
go build -o ${BINARY} app/*.go


unittest:
go test -short ./...
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# go-clean-arch

## Looking for the old code ?
If you are looking for the old code, you can checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1)
## Changelog
- **v1**: checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1) <br>
Proposed on 2017, archived to v1 branch on 2018 <br>
Desc: Initial proposal by me. The story can be read here: https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047

_Last Updated: May 12th 2018_
- **v2**: checkout to the [v2 branch](https://github.com/bxcodec/go-clean-arch/tree/v2) <br>
Proposed on 2018, archived to v2 branch on 2020 <br>
Desc: Improvement from v1. The story can be read here: https://medium.com/hackernoon/trying-clean-architecture-on-golang-2-44d615bf8fdf

- **v3**: master branch <br>
Proposed on 2019, merged to master on 2020. <br>
Desc: Introducing Domain package, the details can be seen on this PR [#21](https://github.com/bxcodec/go-clean-arch/pull/21)

## Description
This is an example of implementation of Clean Architecture in Go (Golang) projects.
Expand All @@ -27,7 +35,8 @@ This project has 4 Domain layer :

![golang clean architecture](https://github.com/bxcodec/go-clean-arch/raw/master/clean-arch.png)

The explanation about this project's structure can read from this medium's post : https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047
The original explanation about this project's structure can read from this medium's post : https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047
It may different already, but the concept still the same in application level, also you can see the change log from v1 to current version in Master.

### How To Run This Project
> Make Sure you have run the article.sql in your mysql
Expand Down
11 changes: 6 additions & 5 deletions main.go → app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (

_articleHttpDelivery "github.com/bxcodec/go-clean-arch/article/delivery/http"
_articleHttpDeliveryMiddleware "github.com/bxcodec/go-clean-arch/article/delivery/http/middleware"
_articleRepo "github.com/bxcodec/go-clean-arch/article/repository"
_articleRepo "github.com/bxcodec/go-clean-arch/article/repository/mysql"
_articleUcase "github.com/bxcodec/go-clean-arch/article/usecase"
_authorRepo "github.com/bxcodec/go-clean-arch/author/repository"
_authorRepo "github.com/bxcodec/go-clean-arch/author/repository/mysql"
)

func init() {
Expand All @@ -26,7 +26,7 @@ func init() {
}

if viper.GetBool(`debug`) {
fmt.Println("Service RUN on DEBUG mode")
log.Println("Service RUN on DEBUG mode")
}
}

Expand All @@ -42,8 +42,9 @@ func main() {
val.Add("loc", "Asia/Jakarta")
dsn := fmt.Sprintf("%s?%s", connection, val.Encode())
dbConn, err := sql.Open(`mysql`, dsn)
if err != nil && viper.GetBool("debug") {
fmt.Println(err)

if err != nil {
log.Fatal(err)
}
err = dbConn.Ping()
if err != nil {
Expand Down
51 changes: 21 additions & 30 deletions article/delivery/http/article_handler.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package http

import (
"context"
"net/http"
"strconv"

"github.com/labstack/echo"
"github.com/sirupsen/logrus"
validator "gopkg.in/go-playground/validator.v9"

"github.com/bxcodec/go-clean-arch/article"
"github.com/bxcodec/go-clean-arch/models"
"github.com/bxcodec/go-clean-arch/domain"
)

// ResponseError represent the reseponse error struct
Expand All @@ -20,11 +18,11 @@ type ResponseError struct {

// ArticleHandler represent the httphandler for article
type ArticleHandler struct {
AUsecase article.Usecase
AUsecase domain.ArticleUsecase
}

// NewArticleHandler will initialize the articles/ resources endpoint
func NewArticleHandler(e *echo.Echo, us article.Usecase) {
func NewArticleHandler(e *echo.Echo, us domain.ArticleUsecase) {
handler := &ArticleHandler{
AUsecase: us,
}
Expand All @@ -40,14 +38,12 @@ func (a *ArticleHandler) FetchArticle(c echo.Context) error {
num, _ := strconv.Atoi(numS)
cursor := c.QueryParam("cursor")
ctx := c.Request().Context()
if ctx == nil {
ctx = context.Background()
}
listAr, nextCursor, err := a.AUsecase.Fetch(ctx, cursor, int64(num))

listAr, nextCursor, err := a.AUsecase.Fetch(ctx, cursor, int64(num))
if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}

c.Response().Header().Set(`X-Cursor`, nextCursor)
return c.JSON(http.StatusOK, listAr)
}
Expand All @@ -56,23 +52,21 @@ func (a *ArticleHandler) FetchArticle(c echo.Context) error {
func (a *ArticleHandler) GetByID(c echo.Context) error {
idP, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusNotFound, models.ErrNotFound.Error())
return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())
}

id := int64(idP)
ctx := c.Request().Context()
if ctx == nil {
ctx = context.Background()
}

art, err := a.AUsecase.GetByID(ctx, id)
if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}

return c.JSON(http.StatusOK, art)
}

func isRequestValid(m *models.Article) (bool, error) {
func isRequestValid(m *domain.Article) (bool, error) {
validate := validator.New()
err := validate.Struct(m)
if err != nil {
Expand All @@ -82,40 +76,36 @@ func isRequestValid(m *models.Article) (bool, error) {
}

// Store will store the article by given request body
func (a *ArticleHandler) Store(c echo.Context) error {
var article models.Article
err := c.Bind(&article)
func (a *ArticleHandler) Store(c echo.Context) (err error) {
var article domain.Article
err = c.Bind(&article)
if err != nil {
return c.JSON(http.StatusUnprocessableEntity, err.Error())
}

if ok, err := isRequestValid(&article); !ok {
var ok bool
if ok, err = isRequestValid(&article); !ok {
return c.JSON(http.StatusBadRequest, err.Error())
}
ctx := c.Request().Context()
if ctx == nil {
ctx = context.Background()
}

ctx := c.Request().Context()
err = a.AUsecase.Store(ctx, &article)

if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}

return c.JSON(http.StatusCreated, article)
}

// Delete will delete article by given param
func (a *ArticleHandler) Delete(c echo.Context) error {
idP, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusNotFound, models.ErrNotFound.Error())
return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())
}

id := int64(idP)
ctx := c.Request().Context()
if ctx == nil {
ctx = context.Background()
}

err = a.AUsecase.Delete(ctx, id)
if err != nil {
Expand All @@ -129,13 +119,14 @@ func getStatusCode(err error) int {
if err == nil {
return http.StatusOK
}

logrus.Error(err)
switch err {
case models.ErrInternalServerError:
case domain.ErrInternalServerError:
return http.StatusInternalServerError
case models.ErrNotFound:
case domain.ErrNotFound:
return http.StatusNotFound
case models.ErrConflict:
case domain.ErrConflict:
return http.StatusConflict
default:
return http.StatusInternalServerError
Expand Down
32 changes: 16 additions & 16 deletions article/delivery/http/article_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ import (
"github.com/stretchr/testify/require"

articleHttp "github.com/bxcodec/go-clean-arch/article/delivery/http"
"github.com/bxcodec/go-clean-arch/article/mocks"
"github.com/bxcodec/go-clean-arch/models"
"github.com/bxcodec/go-clean-arch/domain"
"github.com/bxcodec/go-clean-arch/domain/mocks"
)

func TestFetch(t *testing.T) {
var mockArticle models.Article
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)
mockUCase := new(mocks.Usecase)
mockListArticle := make([]*models.Article, 0)
mockListArticle = append(mockListArticle, &mockArticle)
mockUCase := new(mocks.ArticleUsecase)
mockListArticle := make([]domain.Article, 0)
mockListArticle = append(mockListArticle, mockArticle)
num := 1
cursor := "2"
mockUCase.On("Fetch", mock.Anything, cursor, int64(num)).Return(mockListArticle, "10", nil)
Expand All @@ -50,10 +50,10 @@ func TestFetch(t *testing.T) {
}

func TestFetchError(t *testing.T) {
mockUCase := new(mocks.Usecase)
mockUCase := new(mocks.ArticleUsecase)
num := 1
cursor := "2"
mockUCase.On("Fetch", mock.Anything, cursor, int64(num)).Return(nil, "", models.ErrInternalServerError)
mockUCase.On("Fetch", mock.Anything, cursor, int64(num)).Return(nil, "", domain.ErrInternalServerError)

e := echo.New()
req, err := http.NewRequest(echo.GET, "/article?num=1&cursor="+cursor, strings.NewReader(""))
Expand All @@ -74,15 +74,15 @@ func TestFetchError(t *testing.T) {
}

func TestGetByID(t *testing.T) {
var mockArticle models.Article
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)

mockUCase := new(mocks.Usecase)
mockUCase := new(mocks.ArticleUsecase)

num := int(mockArticle.ID)

mockUCase.On("GetByID", mock.Anything, int64(num)).Return(&mockArticle, nil)
mockUCase.On("GetByID", mock.Anything, int64(num)).Return(mockArticle, nil)

e := echo.New()
req, err := http.NewRequest(echo.GET, "/article/"+strconv.Itoa(num), strings.NewReader(""))
Expand All @@ -104,7 +104,7 @@ func TestGetByID(t *testing.T) {
}

func TestStore(t *testing.T) {
mockArticle := models.Article{
mockArticle := domain.Article{
Title: "Title",
Content: "Content",
CreatedAt: time.Now(),
Expand All @@ -113,12 +113,12 @@ func TestStore(t *testing.T) {

tempMockArticle := mockArticle
tempMockArticle.ID = 0
mockUCase := new(mocks.Usecase)
mockUCase := new(mocks.ArticleUsecase)

j, err := json.Marshal(tempMockArticle)
assert.NoError(t, err)

mockUCase.On("Store", mock.Anything, mock.AnythingOfType("*models.Article")).Return(nil)
mockUCase.On("Store", mock.Anything, mock.AnythingOfType("*domain.Article")).Return(nil)

e := echo.New()
req, err := http.NewRequest(echo.POST, "/article", strings.NewReader(string(j)))
Expand All @@ -140,11 +140,11 @@ func TestStore(t *testing.T) {
}

func TestDelete(t *testing.T) {
var mockArticle models.Article
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)

mockUCase := new(mocks.Usecase)
mockUCase := new(mocks.ArticleUsecase)

num := int(mockArticle.ID)

Expand Down
2 changes: 1 addition & 1 deletion article/delivery/http/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func (m *GoMiddleware) CORS(next echo.HandlerFunc) echo.HandlerFunc {
}
}

// InitMiddleware intialize the middleware
// InitMiddleware initialize the middleware
func InitMiddleware() *GoMiddleware {
return &GoMiddleware{}
}
17 changes: 0 additions & 17 deletions article/repository.go

This file was deleted.

30 changes: 30 additions & 0 deletions article/repository/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package repository

import (
"encoding/base64"
"time"
)

const (
timeFormat = "2006-01-02T15:04:05.999Z07:00" // reduce precision from RFC3339Nano as date format
)

// DecodeCursor will decode cursor from user for mysql
func DecodeCursor(encodedTime string) (time.Time, error) {
byt, err := base64.StdEncoding.DecodeString(encodedTime)
if err != nil {
return time.Time{}, err
}

timeString := string(byt)
t, err := time.Parse(timeFormat, timeString)

return t, err
}

// EncodeCursor will encode cursor from mysql to user
func EncodeCursor(t time.Time) string {
timeString := t.Format(timeFormat)

return base64.StdEncoding.EncodeToString([]byte(timeString))
}
Loading

0 comments on commit d452858

Please sign in to comment.