From 516f7b0c96e231ea43606babe4c20060d6aafb28 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Wed, 19 Dec 2018 14:08:15 +0700 Subject: [PATCH 01/11] init --- article/mocks/Repository.go | 129 -------------------------- article/mocks/Usecase.go | 129 -------------------------- article/repository.go | 17 ---- article/usecase.go | 17 ---- article/usecase/article_ucase.go | 71 +++++++------- author/mocks/Repository.go | 34 ------- author/repository.go | 12 --- author/repository/mysql_repository.go | 35 +++---- domain/article.go | 36 +++++++ domain/author.go | 16 ++++ {models => domain}/errors.go | 2 +- models/article.go | 14 --- models/author.go | 8 -- 13 files changed, 99 insertions(+), 421 deletions(-) delete mode 100644 article/mocks/Repository.go delete mode 100644 article/mocks/Usecase.go delete mode 100644 article/repository.go delete mode 100644 article/usecase.go delete mode 100644 author/mocks/Repository.go delete mode 100644 author/repository.go create mode 100644 domain/article.go create mode 100644 domain/author.go rename {models => domain}/errors.go (95%) delete mode 100644 models/article.go delete mode 100644 models/author.go diff --git a/article/mocks/Repository.go b/article/mocks/Repository.go deleted file mode 100644 index 5541e8b..0000000 --- a/article/mocks/Repository.go +++ /dev/null @@ -1,129 +0,0 @@ -// Code generated by mockery v1.0.0 -package mocks - -import context "context" -import mock "github.com/stretchr/testify/mock" -import models "github.com/bxcodec/go-clean-arch/models" - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -// Delete provides a mock function with given fields: ctx, id -func (_m *Repository) Delete(ctx context.Context, id int64) error { - ret := _m.Called(ctx, id) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Fetch provides a mock function with given fields: ctx, cursor, num -func (_m *Repository) Fetch(ctx context.Context, cursor string, num int64) ([]*models.Article, string, error) { - ret := _m.Called(ctx, cursor, num) - - var r0 []*models.Article - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*models.Article); ok { - r0 = rf(ctx, cursor, num) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.Article) - } - } - - var r1 string - if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok { - r1 = rf(ctx, cursor, num) - } else { - r1 = ret.Get(1).(string) - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok { - r2 = rf(ctx, cursor, num) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetByID provides a mock function with given fields: ctx, id -func (_m *Repository) GetByID(ctx context.Context, id int64) (*models.Article, error) { - ret := _m.Called(ctx, id) - - var r0 *models.Article - if rf, ok := ret.Get(0).(func(context.Context, int64) *models.Article); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Article) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetByTitle provides a mock function with given fields: ctx, title -func (_m *Repository) GetByTitle(ctx context.Context, title string) (*models.Article, error) { - ret := _m.Called(ctx, title) - - var r0 *models.Article - if rf, ok := ret.Get(0).(func(context.Context, string) *models.Article); ok { - r0 = rf(ctx, title) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Article) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, title) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store provides a mock function with given fields: ctx, a -func (_m *Repository) Store(ctx context.Context, a *models.Article) error { - ret := _m.Called(ctx, a) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Article) error); ok { - r0 = rf(ctx, a) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Update provides a mock function with given fields: ctx, ar -func (_m *Repository) Update(ctx context.Context, ar *models.Article) error { - ret := _m.Called(ctx, ar) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Article) error); ok { - r0 = rf(ctx, ar) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/article/mocks/Usecase.go b/article/mocks/Usecase.go deleted file mode 100644 index 10f3acb..0000000 --- a/article/mocks/Usecase.go +++ /dev/null @@ -1,129 +0,0 @@ -// Code generated by mockery v1.0.0 -package mocks - -import context "context" -import mock "github.com/stretchr/testify/mock" -import models "github.com/bxcodec/go-clean-arch/models" - -// Usecase is an autogenerated mock type for the Usecase type -type Usecase struct { - mock.Mock -} - -// Delete provides a mock function with given fields: ctx, id -func (_m *Usecase) Delete(ctx context.Context, id int64) error { - ret := _m.Called(ctx, id) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Fetch provides a mock function with given fields: ctx, cursor, num -func (_m *Usecase) Fetch(ctx context.Context, cursor string, num int64) ([]*models.Article, string, error) { - ret := _m.Called(ctx, cursor, num) - - var r0 []*models.Article - if rf, ok := ret.Get(0).(func(context.Context, string, int64) []*models.Article); ok { - r0 = rf(ctx, cursor, num) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.Article) - } - } - - var r1 string - if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok { - r1 = rf(ctx, cursor, num) - } else { - r1 = ret.Get(1).(string) - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok { - r2 = rf(ctx, cursor, num) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetByID provides a mock function with given fields: ctx, id -func (_m *Usecase) GetByID(ctx context.Context, id int64) (*models.Article, error) { - ret := _m.Called(ctx, id) - - var r0 *models.Article - if rf, ok := ret.Get(0).(func(context.Context, int64) *models.Article); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Article) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetByTitle provides a mock function with given fields: ctx, title -func (_m *Usecase) GetByTitle(ctx context.Context, title string) (*models.Article, error) { - ret := _m.Called(ctx, title) - - var r0 *models.Article - if rf, ok := ret.Get(0).(func(context.Context, string) *models.Article); ok { - r0 = rf(ctx, title) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Article) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, title) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store provides a mock function with given fields: _a0, _a1 -func (_m *Usecase) Store(_a0 context.Context, _a1 *models.Article) error { - ret := _m.Called(_a0, _a1) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Article) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Update provides a mock function with given fields: ctx, ar -func (_m *Usecase) Update(ctx context.Context, ar *models.Article) error { - ret := _m.Called(ctx, ar) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.Article) error); ok { - r0 = rf(ctx, ar) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/article/repository.go b/article/repository.go deleted file mode 100644 index bbcc1f7..0000000 --- a/article/repository.go +++ /dev/null @@ -1,17 +0,0 @@ -package article - -import ( - "context" - - "github.com/bxcodec/go-clean-arch/models" -) - -// Repository represent the article's repository contract -type Repository interface { - Fetch(ctx context.Context, cursor string, num int64) (res []*models.Article, nextCursor string, err error) - GetByID(ctx context.Context, id int64) (*models.Article, error) - GetByTitle(ctx context.Context, title string) (*models.Article, error) - Update(ctx context.Context, ar *models.Article) error - Store(ctx context.Context, a *models.Article) error - Delete(ctx context.Context, id int64) error -} diff --git a/article/usecase.go b/article/usecase.go deleted file mode 100644 index 3afdaab..0000000 --- a/article/usecase.go +++ /dev/null @@ -1,17 +0,0 @@ -package article - -import ( - "context" - - "github.com/bxcodec/go-clean-arch/models" -) - -// Usecase represent the article's usecases -type Usecase interface { - Fetch(ctx context.Context, cursor string, num int64) ([]*models.Article, string, error) - GetByID(ctx context.Context, id int64) (*models.Article, error) - Update(ctx context.Context, ar *models.Article) error - GetByTitle(ctx context.Context, title string) (*models.Article, error) - Store(context.Context, *models.Article) error - Delete(ctx context.Context, id int64) error -} diff --git a/article/usecase/article_ucase.go b/article/usecase/article_ucase.go index 3534c6d..904d553 100644 --- a/article/usecase/article_ucase.go +++ b/article/usecase/article_ucase.go @@ -4,21 +4,19 @@ import ( "context" "time" - "github.com/bxcodec/go-clean-arch/models" + "github.com/bxcodec/go-clean-arch/domain" - "github.com/bxcodec/go-clean-arch/article" - "github.com/bxcodec/go-clean-arch/author" "golang.org/x/sync/errgroup" ) type articleUsecase struct { - articleRepo article.Repository - authorRepo author.Repository + articleRepo domain.ArticleRepository + authorRepo domain.AuthorRepository contextTimeout time.Duration } -// NewArticleUsecase will create new an articleUsecase object representation of article.Usecase interface -func NewArticleUsecase(a article.Repository, ar author.Repository, timeout time.Duration) article.Usecase { +// NewArticleUsecase will create new an articleUsecase object representation of domain.ArticleUsecase interface +func NewArticleUsecase(a domain.ArticleRepository, ar domain.AuthorRepository, timeout time.Duration) domain.ArticleUsecase { return &articleUsecase{ articleRepo: a, authorRepo: ar, @@ -31,19 +29,19 @@ func NewArticleUsecase(a article.Repository, ar author.Repository, timeout time. * Look how this works in this package explanation * in godoc: https://godoc.org/golang.org/x/sync/errgroup#ex-Group--Pipeline */ -func (a *articleUsecase) fillAuthorDetails(c context.Context, data []*models.Article) ([]*models.Article, error) { +func (a *articleUsecase) fillAuthorDetails(c context.Context, data []domain.Article) ([]domain.Article, error) { g, ctx := errgroup.WithContext(c) // Get the author's id - mapAuthors := map[int64]models.Author{} + mapAuthors := map[int64]domain.Author{} for _, article := range data { - mapAuthors[article.Author.ID] = models.Author{} + mapAuthors[article.Author.ID] = domain.Author{} } // Using goroutine to fetch the author's detail - chanAuthor := make(chan *models.Author) - for authorID, _ := range mapAuthors { + chanAuthor := make(chan domain.Author) + for authorID := range mapAuthors { authorID := authorID g.Go(func() error { res, err := a.authorRepo.GetByID(ctx, authorID) @@ -61,8 +59,8 @@ func (a *articleUsecase) fillAuthorDetails(c context.Context, data []*models.Art }() for author := range chanAuthor { - if author != nil { - mapAuthors[author.ID] = *author + if author != (domain.Author{}) { + mapAuthors[author.ID] = author } } @@ -79,7 +77,7 @@ func (a *articleUsecase) fillAuthorDetails(c context.Context, data []*models.Art return data, nil } -func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) ([]*models.Article, string, error) { +func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) ([]domain.Article, string, error) { if num == 0 { num = 10 } @@ -100,25 +98,25 @@ func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) ([]* return listArticle, nextCursor, nil } -func (a *articleUsecase) GetByID(c context.Context, id int64) (*models.Article, error) { +func (a *articleUsecase) GetByID(c context.Context, id int64) (res domain.Article, err error) { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() - res, err := a.articleRepo.GetByID(ctx, id) + res, err = a.articleRepo.GetByID(ctx, id) if err != nil { - return nil, err + return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { - return nil, err + return domain.Article{}, err } - res.Author = *resAuthor - return res, nil + res.Author = resAuthor + return } -func (a *articleUsecase) Update(c context.Context, ar *models.Article) error { +func (a *articleUsecase) Update(c context.Context, ar *domain.Article) error { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() @@ -127,38 +125,35 @@ func (a *articleUsecase) Update(c context.Context, ar *models.Article) error { return a.articleRepo.Update(ctx, ar) } -func (a *articleUsecase) GetByTitle(c context.Context, title string) (*models.Article, error) { +func (a *articleUsecase) GetByTitle(c context.Context, title string) (res domain.Article, err error) { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() - res, err := a.articleRepo.GetByTitle(ctx, title) + res, err = a.articleRepo.GetByTitle(ctx, title) if err != nil { - return nil, err + return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { - return nil, err + return domain.Article{}, err } - res.Author = *resAuthor + res.Author = resAuthor - return res, nil + return } -func (a *articleUsecase) Store(c context.Context, m *models.Article) error { +func (a *articleUsecase) Store(c context.Context, m *domain.Article) (err error) { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() existedArticle, _ := a.GetByTitle(ctx, m.Title) - if existedArticle != nil { - return models.ErrConflict + if existedArticle != (domain.Article{}) { + return domain.ErrConflict } - err := a.articleRepo.Store(ctx, m) - if err != nil { - return err - } - return nil + err = a.articleRepo.Store(ctx, m) + return } func (a *articleUsecase) Delete(c context.Context, id int64) error { @@ -168,8 +163,8 @@ func (a *articleUsecase) Delete(c context.Context, id int64) error { if err != nil { return err } - if existedArticle == nil { - return models.ErrNotFound + if existedArticle == (domain.Article{}) { + return domain.ErrNotFound } return a.articleRepo.Delete(ctx, id) } diff --git a/author/mocks/Repository.go b/author/mocks/Repository.go deleted file mode 100644 index fc599c1..0000000 --- a/author/mocks/Repository.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated by mockery v1.0.0 -package mocks - -import context "context" -import mock "github.com/stretchr/testify/mock" -import models "github.com/bxcodec/go-clean-arch/models" - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -// GetByID provides a mock function with given fields: ctx, id -func (_m *Repository) GetByID(ctx context.Context, id int64) (*models.Author, error) { - ret := _m.Called(ctx, id) - - var r0 *models.Author - if rf, ok := ret.Get(0).(func(context.Context, int64) *models.Author); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Author) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/author/repository.go b/author/repository.go deleted file mode 100644 index 98d3bf8..0000000 --- a/author/repository.go +++ /dev/null @@ -1,12 +0,0 @@ -package author - -import ( - "context" - - "github.com/bxcodec/go-clean-arch/models" -) - -// Repository represent the author's repository contract -type Repository interface { - GetByID(ctx context.Context, id int64) (*models.Author, error) -} diff --git a/author/repository/mysql_repository.go b/author/repository/mysql_repository.go index aca33e5..7b50f21 100644 --- a/author/repository/mysql_repository.go +++ b/author/repository/mysql_repository.go @@ -4,49 +4,40 @@ import ( "context" "database/sql" - "github.com/sirupsen/logrus" - - "github.com/bxcodec/go-clean-arch/author" - "github.com/bxcodec/go-clean-arch/models" + "github.com/bxcodec/go-clean-arch/domain" ) type mysqlAuthorRepo struct { DB *sql.DB } -// NewMysqlAuthorRepository will create an implementation of author.Repository -func NewMysqlAuthorRepository(db *sql.DB) author.Repository { +// NewMysqlAuthorRepository will create an implementation of domain.AuthorRepository +func NewMysqlAuthorRepository(db *sql.DB) domain.AuthorRepository { return &mysqlAuthorRepo{ DB: db, } } -func (m *mysqlAuthorRepo) getOne(ctx context.Context, query string, args ...interface{}) (*models.Author, error) { - +func (m *mysqlAuthorRepo) getOne(ctx context.Context, query string, args ...interface{}) (res domain.Author, err error) { stmt, err := m.DB.PrepareContext(ctx, query) if err != nil { - logrus.Error(err) - return nil, err + + return domain.Author{}, err } row := stmt.QueryRowContext(ctx, args...) - a := &models.Author{} + res = domain.Author{} err = row.Scan( - &a.ID, - &a.Name, - &a.CreatedAt, - &a.UpdatedAt, + &res.ID, + &res.Name, + &res.CreatedAt, + &res.UpdatedAt, ) - if err != nil { - logrus.Error(err) - return nil, err - } - - return a, nil + return } -func (m *mysqlAuthorRepo) GetByID(ctx context.Context, id int64) (*models.Author, error) { +func (m *mysqlAuthorRepo) GetByID(ctx context.Context, id int64) (domain.Author, error) { query := `SELECT id, name, created_at, updated_at FROM author WHERE id=?` return m.getOne(ctx, query, id) } diff --git a/domain/article.go b/domain/article.go new file mode 100644 index 0000000..abc2573 --- /dev/null +++ b/domain/article.go @@ -0,0 +1,36 @@ +package domain + +import ( + "context" + "time" +) + +// Article ... +type Article struct { + ID int64 `json:"id"` + Title string `json:"title" validate:"required"` + Content string `json:"content" validate:"required"` + Author Author `json:"author"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +// ArticleUsecase represent the article's usecases +type ArticleUsecase interface { + Fetch(ctx context.Context, cursor string, num int64) ([]Article, string, error) + GetByID(ctx context.Context, id int64) (Article, error) + Update(ctx context.Context, ar *Article) error + GetByTitle(ctx context.Context, title string) (Article, error) + Store(context.Context, *Article) error + Delete(ctx context.Context, id int64) error +} + +// ArticleRepository represent the article's repository contract +type ArticleRepository interface { + Fetch(ctx context.Context, cursor string, num int64) (res []Article, nextCursor string, err error) + GetByID(ctx context.Context, id int64) (Article, error) + GetByTitle(ctx context.Context, title string) (Article, error) + Update(ctx context.Context, ar *Article) error + Store(ctx context.Context, a *Article) error + Delete(ctx context.Context, id int64) error +} diff --git a/domain/author.go b/domain/author.go new file mode 100644 index 0000000..c3acd7d --- /dev/null +++ b/domain/author.go @@ -0,0 +1,16 @@ +package domain + +import "context" + +// Author ... +type Author struct { + ID int64 `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// AuthorRepository represent the author's repository contract +type AuthorRepository interface { + GetByID(ctx context.Context, id int64) (Author, error) +} diff --git a/models/errors.go b/domain/errors.go similarity index 95% rename from models/errors.go rename to domain/errors.go index 031740b..ea334af 100644 --- a/models/errors.go +++ b/domain/errors.go @@ -1,4 +1,4 @@ -package models +package domain import "errors" diff --git a/models/article.go b/models/article.go deleted file mode 100644 index e66eb36..0000000 --- a/models/article.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import ( - "time" -) - -type Article struct { - ID int64 `json:"id"` - Title string `json:"title" validate:"required"` - Content string `json:"content" validate:"required"` - Author Author `json:"author"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` -} diff --git a/models/author.go b/models/author.go deleted file mode 100644 index a4e57a0..0000000 --- a/models/author.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -type Author struct { - ID int64 `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} From adf30aa2db96e0321628a5390ec5998d3a02e394 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Wed, 19 Dec 2018 14:52:32 +0700 Subject: [PATCH 02/11] refactor: move everything to domain --- article/delivery/http/article_handler.go | 18 ++-- article/delivery/http/article_test.go | 32 +++--- article/repository/mysql_article.go | 52 +++++----- article/repository/mysqlarticle_test.go | 20 ++-- article/usecase/article_ucase_test.go | 71 +++++++------ domain/mocks/ArticleRepository.go | 125 +++++++++++++++++++++++ domain/mocks/ArticleUsecase.go | 125 +++++++++++++++++++++++ domain/mocks/AuthorRepository.go | 32 ++++++ main.go | 69 ------------- 9 files changed, 376 insertions(+), 168 deletions(-) create mode 100644 domain/mocks/ArticleRepository.go create mode 100644 domain/mocks/ArticleUsecase.go create mode 100644 domain/mocks/AuthorRepository.go delete mode 100644 main.go diff --git a/article/delivery/http/article_handler.go b/article/delivery/http/article_handler.go index 90b0e9d..dc956aa 100644 --- a/article/delivery/http/article_handler.go +++ b/article/delivery/http/article_handler.go @@ -7,9 +7,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/bxcodec/go-clean-arch/models" - - "github.com/bxcodec/go-clean-arch/article" + "github.com/bxcodec/go-clean-arch/domain" "github.com/labstack/echo" validator "gopkg.in/go-playground/validator.v9" @@ -22,10 +20,10 @@ type ResponseError struct { // HttpArticleHandler represent the httphandler for article type HttpArticleHandler struct { - AUsecase article.Usecase + AUsecase domain.ArticleUsecase } -func NewArticleHttpHandler(e *echo.Echo, us article.Usecase) { +func NewArticleHttpHandler(e *echo.Echo, us domain.ArticleUsecase) { handler := &HttpArticleHandler{ AUsecase: us, } @@ -72,7 +70,7 @@ func (a *HttpArticleHandler) GetByID(c echo.Context) error { return c.JSON(http.StatusOK, art) } -func isRequestValid(m *models.Article) (bool, error) { +func isRequestValid(m *domain.Article) (bool, error) { validate := validator.New() @@ -84,7 +82,7 @@ func isRequestValid(m *models.Article) (bool, error) { } func (a *HttpArticleHandler) Store(c echo.Context) error { - var article models.Article + var article domain.Article err := c.Bind(&article) if err != nil { return c.JSON(http.StatusUnprocessableEntity, err.Error()) @@ -129,11 +127,11 @@ func getStatusCode(err error) int { } 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 diff --git a/article/delivery/http/article_test.go b/article/delivery/http/article_test.go index 192bfd0..524edbb 100644 --- a/article/delivery/http/article_test.go +++ b/article/delivery/http/article_test.go @@ -10,8 +10,8 @@ import ( "time" 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" "github.com/labstack/echo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -20,12 +20,12 @@ import ( ) 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) @@ -49,10 +49,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("")) @@ -73,15 +73,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(int(num)), strings.NewReader("")) @@ -102,7 +102,7 @@ func TestGetByID(t *testing.T) { } func TestStore(t *testing.T) { - mockArticle := models.Article{ + mockArticle := domain.Article{ Title: "Title", Content: "Content", CreatedAt: time.Now(), @@ -111,12 +111,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))) @@ -137,11 +137,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) diff --git a/article/repository/mysql_article.go b/article/repository/mysql_article.go index c1f20c0..0fd7ede 100644 --- a/article/repository/mysql_article.go +++ b/article/repository/mysql_article.go @@ -9,8 +9,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/bxcodec/go-clean-arch/article" - "github.com/bxcodec/go-clean-arch/models" + "github.com/bxcodec/go-clean-arch/domain" ) const ( @@ -21,13 +20,13 @@ type mysqlArticleRepository struct { Conn *sql.DB } -// NewMysqlArticleRepository will create an object that represent the article.Repository interface -func NewMysqlArticleRepository(Conn *sql.DB) article.Repository { +// NewMysqlArticleRepository will create an object that represent the domain.ArticleRepository interface +func NewMysqlArticleRepository(Conn *sql.DB) domain.ArticleRepository { return &mysqlArticleRepository{Conn} } -func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]*models.Article, error) { +func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Article, error) { rows, err := m.Conn.QueryContext(ctx, query, args...) @@ -36,9 +35,9 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . return nil, err } defer rows.Close() - result := make([]*models.Article, 0) + result := make([]domain.Article, 0) for rows.Next() { - t := new(models.Article) + t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, @@ -53,7 +52,7 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . logrus.Error(err) return nil, err } - t.Author = models.Author{ + t.Author = domain.Author{ ID: authorID, } result = append(result, t) @@ -62,14 +61,14 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . return result, nil } -func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num int64) ([]*models.Article, string, error) { +func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE created_at > ? ORDER BY created_at LIMIT ? ` decodedCursor, err := DecodeCursor(cursor) if err != nil && cursor != "" { - return nil, "", models.ErrBadParamInput + return nil, "", domain.ErrBadParamInput } res, err := m.fetch(ctx, query, decodedCursor, num) if err != nil { @@ -82,44 +81,43 @@ func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num i return res, nextCursor, err } -func (m *mysqlArticleRepository) GetByID(ctx context.Context, id int64) (*models.Article, error) { +func (m *mysqlArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { - return nil, err + return domain.Article{}, err } - a := &models.Article{} + res = domain.Article{} if len(list) > 0 { - a = list[0] + res = list[0] } else { - return nil, models.ErrNotFound + return res, domain.ErrNotFound } - - return a, nil + return } -func (m *mysqlArticleRepository) GetByTitle(ctx context.Context, title string) (*models.Article, error) { +func (m *mysqlArticleRepository) GetByTitle(ctx context.Context, title string) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE title = ?` list, err := m.fetch(ctx, query, title) if err != nil { - return nil, err + return res, err } - a := &models.Article{} + res = domain.Article{} if len(list) > 0 { - a = list[0] + res = list[0] } else { - return nil, models.ErrNotFound + return res, domain.ErrNotFound } - return a, nil + return } -func (m *mysqlArticleRepository) Store(ctx context.Context, a *models.Article) error { +func (m *mysqlArticleRepository) Store(ctx context.Context, a *domain.Article) error { query := `INSERT article SET title=? , content=? , author_id=?, updated_at=? , created_at=?` stmt, err := m.Conn.PrepareContext(ctx, query) @@ -134,11 +132,11 @@ func (m *mysqlArticleRepository) Store(ctx context.Context, a *models.Article) e return err } - lastId, err := res.LastInsertId() + lastID, err := res.LastInsertId() if err != nil { return err } - a.ID = lastId + a.ID = lastID return nil } @@ -165,7 +163,7 @@ func (m *mysqlArticleRepository) Delete(ctx context.Context, id int64) error { return nil } -func (m *mysqlArticleRepository) Update(ctx context.Context, ar *models.Article) error { +func (m *mysqlArticleRepository) Update(ctx context.Context, ar *domain.Article) error { query := `UPDATE article set title=?, content=?, author_id=?, updated_at=? WHERE ID = ?` stmt, err := m.Conn.PrepareContext(ctx, query) diff --git a/article/repository/mysqlarticle_test.go b/article/repository/mysqlarticle_test.go index d879ff6..c2efa97 100644 --- a/article/repository/mysqlarticle_test.go +++ b/article/repository/mysqlarticle_test.go @@ -6,7 +6,7 @@ import ( "time" articleRepo "github.com/bxcodec/go-clean-arch/article/repository" - "github.com/bxcodec/go-clean-arch/models" + "github.com/bxcodec/go-clean-arch/domain" "github.com/stretchr/testify/assert" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" ) @@ -18,14 +18,14 @@ func TestFetch(t *testing.T) { } defer db.Close() - mockArticles := []models.Article{ - models.Article{ + mockArticles := []domain.Article{ + domain.Article{ ID: 1, Title: "title 1", Content: "content 1", - Author: models.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(), + Author: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(), }, - models.Article{ + domain.Article{ ID: 2, Title: "title 2", Content: "content 2", - Author: models.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(), + Author: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(), }, } @@ -69,12 +69,12 @@ func TestGetByID(t *testing.T) { func TestStore(t *testing.T) { now := time.Now() - ar := &models.Article{ + ar := &domain.Article{ Title: "Judul", Content: "Content", CreatedAt: now, UpdatedAt: now, - Author: models.Author{ + Author: domain.Author{ ID: 1, Name: "Iman Tumorang", }, @@ -137,13 +137,13 @@ func TestDelete(t *testing.T) { func TestUpdate(t *testing.T) { now := time.Now() - ar := &models.Article{ + ar := &domain.Article{ ID: 12, Title: "Judul", Content: "Content", CreatedAt: now, UpdatedAt: now, - Author: models.Author{ + Author: domain.Author{ ID: 1, Name: "Iman Tumorang", }, diff --git a/article/usecase/article_ucase_test.go b/article/usecase/article_ucase_test.go index 7985508..c7654a4 100644 --- a/article/usecase/article_ucase_test.go +++ b/article/usecase/article_ucase_test.go @@ -6,32 +6,31 @@ import ( "testing" "time" - "github.com/bxcodec/go-clean-arch/article/mocks" ucase "github.com/bxcodec/go-clean-arch/article/usecase" - _authorMock "github.com/bxcodec/go-clean-arch/author/mocks" - "github.com/bxcodec/go-clean-arch/models" + "github.com/bxcodec/go-clean-arch/domain" + "github.com/bxcodec/go-clean-arch/domain/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestFetch(t *testing.T) { - mockArticleRepo := new(mocks.Repository) - mockArticle := &models.Article{ + mockArticleRepo := new(mocks.ArticleRepository) + mockArticle := domain.Article{ Title: "Hello", Content: "Content", } - mockListArtilce := make([]*models.Article, 0) + mockListArtilce := make([]domain.Article, 0) mockListArtilce = append(mockListArtilce, mockArticle) t.Run("success", func(t *testing.T) { mockArticleRepo.On("Fetch", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("int64")).Return(mockListArtilce, "next-cursor", nil).Once() - mockAuthor := &models.Author{ + mockAuthor := domain.Author{ ID: 1, Name: "Iman Tumorang", } - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) num := int64(1) @@ -51,7 +50,7 @@ func TestFetch(t *testing.T) { mockArticleRepo.On("Fetch", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("int64")).Return(nil, "", errors.New("Unexpexted Error")).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) num := int64(1) cursor := "12" @@ -67,19 +66,19 @@ func TestFetch(t *testing.T) { } func TestGetByID(t *testing.T) { - mockArticleRepo := new(mocks.Repository) - mockArticle := models.Article{ + mockArticleRepo := new(mocks.ArticleRepository) + mockArticle := domain.Article{ Title: "Hello", Content: "Content", } - mockAuthor := &models.Author{ + mockAuthor := domain.Author{ ID: 1, Name: "Iman Tumorang", } t.Run("success", func(t *testing.T) { - mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(&mockArticle, nil).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockArticle, nil).Once() + mockAuthorrepo := new(mocks.AuthorRepository) mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) @@ -92,15 +91,15 @@ func TestGetByID(t *testing.T) { mockAuthorrepo.AssertExpectations(t) }) t.Run("error-failed", func(t *testing.T) { - mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, errors.New("Unexpected")).Once() + mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, errors.New("Unexpected")).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) a, err := u.GetByID(context.TODO(), mockArticle.ID) assert.Error(t, err) - assert.Nil(t, a) + assert.Equal(t, domain.Article{}, a) mockArticleRepo.AssertExpectations(t) mockAuthorrepo.AssertExpectations(t) @@ -109,8 +108,8 @@ func TestGetByID(t *testing.T) { } func TestStore(t *testing.T) { - mockArticleRepo := new(mocks.Repository) - mockArticle := models.Article{ + mockArticleRepo := new(mocks.ArticleRepository) + mockArticle := domain.Article{ Title: "Hello", Content: "Content", } @@ -118,10 +117,10 @@ func TestStore(t *testing.T) { t.Run("success", func(t *testing.T) { tempMockArticle := mockArticle tempMockArticle.ID = 0 - mockArticleRepo.On("GetByTitle", mock.Anything, mock.AnythingOfType("string")).Return(nil, models.ErrNotFound).Once() - mockArticleRepo.On("Store", mock.Anything, mock.AnythingOfType("*models.Article")).Return(nil).Once() + mockArticleRepo.On("GetByTitle", mock.Anything, mock.AnythingOfType("string")).Return(domain.Article{}, domain.ErrNotFound).Once() + mockArticleRepo.On("Store", mock.Anything, mock.AnythingOfType("*domain.Article")).Return(nil).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) err := u.Store(context.TODO(), &tempMockArticle) @@ -132,12 +131,12 @@ func TestStore(t *testing.T) { }) t.Run("existing-title", func(t *testing.T) { existingArticle := mockArticle - mockArticleRepo.On("GetByTitle", mock.Anything, mock.AnythingOfType("string")).Return(&existingArticle, nil).Once() - mockAuthor := &models.Author{ + mockArticleRepo.On("GetByTitle", mock.Anything, mock.AnythingOfType("string")).Return(existingArticle, nil).Once() + mockAuthor := domain.Author{ ID: 1, Name: "Iman Tumorang", } - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) @@ -152,18 +151,18 @@ func TestStore(t *testing.T) { } func TestDelete(t *testing.T) { - mockArticleRepo := new(mocks.Repository) - mockArticle := models.Article{ + mockArticleRepo := new(mocks.ArticleRepository) + mockArticle := domain.Article{ Title: "Hello", Content: "Content", } t.Run("success", func(t *testing.T) { - mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(&mockArticle, nil).Once() + mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockArticle, nil).Once() mockArticleRepo.On("Delete", mock.Anything, mock.AnythingOfType("int64")).Return(nil).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) err := u.Delete(context.TODO(), mockArticle.ID) @@ -173,9 +172,9 @@ func TestDelete(t *testing.T) { mockAuthorrepo.AssertExpectations(t) }) t.Run("article-is-not-exist", func(t *testing.T) { - mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, nil).Once() + mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, nil).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) err := u.Delete(context.TODO(), mockArticle.ID) @@ -185,9 +184,9 @@ func TestDelete(t *testing.T) { mockAuthorrepo.AssertExpectations(t) }) t.Run("error-happens-in-db", func(t *testing.T) { - mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, errors.New("Unexpected Error")).Once() + mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, errors.New("Unexpected Error")).Once() - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) err := u.Delete(context.TODO(), mockArticle.ID) @@ -200,8 +199,8 @@ func TestDelete(t *testing.T) { } func TestUpdate(t *testing.T) { - mockArticleRepo := new(mocks.Repository) - mockArticle := models.Article{ + mockArticleRepo := new(mocks.ArticleRepository) + mockArticle := domain.Article{ Title: "Hello", Content: "Content", ID: 23, @@ -210,7 +209,7 @@ func TestUpdate(t *testing.T) { t.Run("success", func(t *testing.T) { mockArticleRepo.On("Update", mock.Anything, &mockArticle).Once().Return(nil) - mockAuthorrepo := new(_authorMock.Repository) + mockAuthorrepo := new(mocks.AuthorRepository) u := ucase.NewArticleUsecase(mockArticleRepo, mockAuthorrepo, time.Second*2) err := u.Update(context.TODO(), &mockArticle) diff --git a/domain/mocks/ArticleRepository.go b/domain/mocks/ArticleRepository.go new file mode 100644 index 0000000..ad0b048 --- /dev/null +++ b/domain/mocks/ArticleRepository.go @@ -0,0 +1,125 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package mocks + +import context "context" +import domain "github.com/bxcodec/go-clean-arch/domain" +import mock "github.com/stretchr/testify/mock" + +// ArticleRepository is an autogenerated mock type for the ArticleRepository type +type ArticleRepository struct { + mock.Mock +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *ArticleRepository) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Fetch provides a mock function with given fields: ctx, cursor, num +func (_m *ArticleRepository) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) { + ret := _m.Called(ctx, cursor, num) + + var r0 []domain.Article + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []domain.Article); ok { + r0 = rf(ctx, cursor, num) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]domain.Article) + } + } + + var r1 string + if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok { + r1 = rf(ctx, cursor, num) + } else { + r1 = ret.Get(1).(string) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok { + r2 = rf(ctx, cursor, num) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *ArticleRepository) GetByID(ctx context.Context, id int64) (domain.Article, error) { + ret := _m.Called(ctx, id) + + var r0 domain.Article + if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(domain.Article) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByTitle provides a mock function with given fields: ctx, title +func (_m *ArticleRepository) GetByTitle(ctx context.Context, title string) (domain.Article, error) { + ret := _m.Called(ctx, title) + + var r0 domain.Article + if rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok { + r0 = rf(ctx, title) + } else { + r0 = ret.Get(0).(domain.Article) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, title) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store provides a mock function with given fields: ctx, a +func (_m *ArticleRepository) Store(ctx context.Context, a *domain.Article) error { + ret := _m.Called(ctx, a) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok { + r0 = rf(ctx, a) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, ar +func (_m *ArticleRepository) Update(ctx context.Context, ar *domain.Article) error { + ret := _m.Called(ctx, ar) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok { + r0 = rf(ctx, ar) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/domain/mocks/ArticleUsecase.go b/domain/mocks/ArticleUsecase.go new file mode 100644 index 0000000..e19379d --- /dev/null +++ b/domain/mocks/ArticleUsecase.go @@ -0,0 +1,125 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package mocks + +import context "context" +import domain "github.com/bxcodec/go-clean-arch/domain" +import mock "github.com/stretchr/testify/mock" + +// ArticleUsecase is an autogenerated mock type for the ArticleUsecase type +type ArticleUsecase struct { + mock.Mock +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *ArticleUsecase) Delete(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Fetch provides a mock function with given fields: ctx, cursor, num +func (_m *ArticleUsecase) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) { + ret := _m.Called(ctx, cursor, num) + + var r0 []domain.Article + if rf, ok := ret.Get(0).(func(context.Context, string, int64) []domain.Article); ok { + r0 = rf(ctx, cursor, num) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]domain.Article) + } + } + + var r1 string + if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok { + r1 = rf(ctx, cursor, num) + } else { + r1 = ret.Get(1).(string) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok { + r2 = rf(ctx, cursor, num) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *ArticleUsecase) GetByID(ctx context.Context, id int64) (domain.Article, error) { + ret := _m.Called(ctx, id) + + var r0 domain.Article + if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(domain.Article) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByTitle provides a mock function with given fields: ctx, title +func (_m *ArticleUsecase) GetByTitle(ctx context.Context, title string) (domain.Article, error) { + ret := _m.Called(ctx, title) + + var r0 domain.Article + if rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok { + r0 = rf(ctx, title) + } else { + r0 = ret.Get(0).(domain.Article) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, title) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store provides a mock function with given fields: _a0, _a1 +func (_m *ArticleUsecase) Store(_a0 context.Context, _a1 *domain.Article) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, ar +func (_m *ArticleUsecase) Update(ctx context.Context, ar *domain.Article) error { + ret := _m.Called(ctx, ar) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok { + r0 = rf(ctx, ar) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/domain/mocks/AuthorRepository.go b/domain/mocks/AuthorRepository.go new file mode 100644 index 0000000..6507628 --- /dev/null +++ b/domain/mocks/AuthorRepository.go @@ -0,0 +1,32 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package mocks + +import context "context" +import domain "github.com/bxcodec/go-clean-arch/domain" +import mock "github.com/stretchr/testify/mock" + +// AuthorRepository is an autogenerated mock type for the AuthorRepository type +type AuthorRepository struct { + mock.Mock +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *AuthorRepository) GetByID(ctx context.Context, id int64) (domain.Author, error) { + ret := _m.Called(ctx, id) + + var r0 domain.Author + if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Author); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(domain.Author) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/main.go b/main.go deleted file mode 100644 index 9c4627a..0000000 --- a/main.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - "net/url" - "os" - "time" - - _articleHttpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http" - _articleRepo "github.com/bxcodec/go-clean-arch/article/repository" - _articleUcase "github.com/bxcodec/go-clean-arch/article/usecase" - _authorRepo "github.com/bxcodec/go-clean-arch/author/repository" - "github.com/bxcodec/go-clean-arch/middleware" - _ "github.com/go-sql-driver/mysql" - "github.com/labstack/echo" - "github.com/spf13/viper" -) - -func init() { - viper.SetConfigFile(`config.json`) - err := viper.ReadInConfig() - - if err != nil { - panic(err) - } - - if viper.GetBool(`debug`) { - fmt.Println("Service RUN on DEBUG mode") - } - -} - -func main() { - - dbHost := viper.GetString(`database.host`) - dbPort := viper.GetString(`database.port`) - dbUser := viper.GetString(`database.user`) - dbPass := viper.GetString(`database.pass`) - dbName := viper.GetString(`database.name`) - connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName) - val := url.Values{} - val.Add("parseTime", "1") - 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) - } - err = dbConn.Ping() - if err != nil { - log.Fatal(err) - os.Exit(1) - } - - defer dbConn.Close() - e := echo.New() - middL := middleware.InitMiddleware() - e.Use(middL.CORS) - authorRepo := _authorRepo.NewMysqlAuthorRepository(dbConn) - ar := _articleRepo.NewMysqlArticleRepository(dbConn) - - timeoutContext := time.Duration(viper.GetInt("context.timeout")) * time.Second - au := _articleUcase.NewArticleUsecase(ar, authorRepo, timeoutContext) - _articleHttpDeliver.NewArticleHttpHandler(e, au) - - e.Start(viper.GetString("server.address")) -} From 0b4fdbc0c20367c2624b9790496c275c618a8db5 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Wed, 19 Dec 2018 14:52:48 +0700 Subject: [PATCH 03/11] add app --- app/cmd/http.go | 1 + app/cmd/root.go | 1 + app/main.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 app/cmd/http.go create mode 100644 app/cmd/root.go create mode 100644 app/main.go diff --git a/app/cmd/http.go b/app/cmd/http.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/app/cmd/http.go @@ -0,0 +1 @@ +package cmd diff --git a/app/cmd/root.go b/app/cmd/root.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/app/cmd/root.go @@ -0,0 +1 @@ +package cmd diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..9c4627a --- /dev/null +++ b/app/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/url" + "os" + "time" + + _articleHttpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http" + _articleRepo "github.com/bxcodec/go-clean-arch/article/repository" + _articleUcase "github.com/bxcodec/go-clean-arch/article/usecase" + _authorRepo "github.com/bxcodec/go-clean-arch/author/repository" + "github.com/bxcodec/go-clean-arch/middleware" + _ "github.com/go-sql-driver/mysql" + "github.com/labstack/echo" + "github.com/spf13/viper" +) + +func init() { + viper.SetConfigFile(`config.json`) + err := viper.ReadInConfig() + + if err != nil { + panic(err) + } + + if viper.GetBool(`debug`) { + fmt.Println("Service RUN on DEBUG mode") + } + +} + +func main() { + + dbHost := viper.GetString(`database.host`) + dbPort := viper.GetString(`database.port`) + dbUser := viper.GetString(`database.user`) + dbPass := viper.GetString(`database.pass`) + dbName := viper.GetString(`database.name`) + connection := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName) + val := url.Values{} + val.Add("parseTime", "1") + 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) + } + err = dbConn.Ping() + if err != nil { + log.Fatal(err) + os.Exit(1) + } + + defer dbConn.Close() + e := echo.New() + middL := middleware.InitMiddleware() + e.Use(middL.CORS) + authorRepo := _authorRepo.NewMysqlAuthorRepository(dbConn) + ar := _articleRepo.NewMysqlArticleRepository(dbConn) + + timeoutContext := time.Duration(viper.GetInt("context.timeout")) * time.Second + au := _articleUcase.NewArticleUsecase(ar, authorRepo, timeoutContext) + _articleHttpDeliver.NewArticleHttpHandler(e, au) + + e.Start(viper.GetString("server.address")) +} From d4aa550a601140dc5fc86ba49880e70c4cfbac17 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sun, 23 Dec 2018 14:12:27 +0700 Subject: [PATCH 04/11] remove unnecessar-things --- app/cmd/http.go | 1 - app/cmd/root.go | 1 - domain/mocks/ArticleRepository.go | 8 ++++---- 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 app/cmd/http.go delete mode 100644 app/cmd/root.go diff --git a/app/cmd/http.go b/app/cmd/http.go deleted file mode 100644 index 1d619dd..0000000 --- a/app/cmd/http.go +++ /dev/null @@ -1 +0,0 @@ -package cmd diff --git a/app/cmd/root.go b/app/cmd/root.go deleted file mode 100644 index 1d619dd..0000000 --- a/app/cmd/root.go +++ /dev/null @@ -1 +0,0 @@ -package cmd diff --git a/domain/mocks/ArticleRepository.go b/domain/mocks/ArticleRepository.go index ad0b048..95ba5a5 100644 --- a/domain/mocks/ArticleRepository.go +++ b/domain/mocks/ArticleRepository.go @@ -96,13 +96,13 @@ func (_m *ArticleRepository) GetByTitle(ctx context.Context, title string) (doma return r0, r1 } -// Store provides a mock function with given fields: ctx, a -func (_m *ArticleRepository) Store(ctx context.Context, a *domain.Article) error { - ret := _m.Called(ctx, a) +// Store provides a mock function with given fields: _a0, _a1 +func (_m *ArticleRepository) Store(_a0 context.Context, _a1 *domain.Article) error { + ret := _m.Called(_a0, _a1) var r0 error if rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok { - r0 = rf(ctx, a) + r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) } From e26f65f4e6bce9bd8e2bdbbda5b0e7ca56e2e736 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sun, 23 Dec 2018 14:17:38 +0700 Subject: [PATCH 05/11] fix makefile --- Dockerfile | 2 +- Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ae1109..22870b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ EXPOSE 9090 COPY --from=builder /go/src/github.com/bxcodec/go-clean-arch/engine /app -CMD /app/engine +CMD /app/engine \ No newline at end of file diff --git a/Makefile b/Makefile index 1e7e956..5021f1c 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,10 @@ vendor: @dep ensure -v engine: vendor - go build -o ${BINARY} + go build -o ${BINARY} app/*.go install: - go build -o ${BINARY} + go build -o ${BINARY} app/*.go unittest: go test -short $$(go list ./... | grep -v /vendor/) From 061c17de813b5a8449098cd25fb1044d3d6157b0 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Tue, 30 Apr 2019 14:04:28 +0700 Subject: [PATCH 06/11] ref: grouping the implementations in the same package --- app/main.go | 4 +-- article/repository/helper.go | 30 +++++++++++++++++ .../repository/{ => mysql}/mysql_article.go | 33 +++---------------- .../{ => mysql}/mysqlarticle_test.go | 19 ++++++----- .../{ => mysql}/mysql_repository.go | 2 +- author/repository/{ => mysql}/mysql_test.go | 4 +-- 6 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 article/repository/helper.go rename article/repository/{ => mysql}/mysql_article.go (83%) rename article/repository/{ => mysql}/mysqlarticle_test.go (90%) rename author/repository/{ => mysql}/mysql_repository.go (97%) rename author/repository/{ => mysql}/mysql_test.go (90%) diff --git a/app/main.go b/app/main.go index f118639..e8c82f8 100644 --- a/app/main.go +++ b/app/main.go @@ -13,9 +13,9 @@ import ( "github.com/spf13/viper" _articleHttpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http" - _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" "github.com/bxcodec/go-clean-arch/middleware" ) diff --git a/article/repository/helper.go b/article/repository/helper.go new file mode 100644 index 0000000..576cb6f --- /dev/null +++ b/article/repository/helper.go @@ -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)) +} diff --git a/article/repository/mysql_article.go b/article/repository/mysql/mysql_article.go similarity index 83% rename from article/repository/mysql_article.go rename to article/repository/mysql/mysql_article.go index 80fd791..3198e88 100644 --- a/article/repository/mysql_article.go +++ b/article/repository/mysql/mysql_article.go @@ -1,21 +1,16 @@ -package repository +package mysql import ( "context" "database/sql" - "encoding/base64" "fmt" - "time" "github.com/sirupsen/logrus" + "github.com/bxcodec/go-clean-arch/article/repository" "github.com/bxcodec/go-clean-arch/domain" ) -const ( - timeFormat = "2006-01-02T15:04:05.999Z07:00" // reduce precision from RFC3339Nano as date format -) - type mysqlArticleRepository struct { Conn *sql.DB } @@ -69,7 +64,7 @@ func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num i query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE created_at > ? ORDER BY created_at LIMIT ? ` - decodedCursor, err := DecodeCursor(cursor) + decodedCursor, err := repository.DecodeCursor(cursor) if err != nil && cursor != "" { return nil, "", domain.ErrBadParamInput } @@ -81,7 +76,7 @@ func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num i nextCursor := "" if len(res) == int(num) { - nextCursor = EncodeCursor(res[len(res)-1].CreatedAt) + nextCursor = repository.EncodeCursor(res[len(res)-1].CreatedAt) } return res, nextCursor, err @@ -190,23 +185,3 @@ func (m *mysqlArticleRepository) Update(ctx context.Context, ar *domain.Article) return nil } - -// 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)) -} diff --git a/article/repository/mysqlarticle_test.go b/article/repository/mysql/mysqlarticle_test.go similarity index 90% rename from article/repository/mysqlarticle_test.go rename to article/repository/mysql/mysqlarticle_test.go index ae8dd03..d5ebfa0 100644 --- a/article/repository/mysqlarticle_test.go +++ b/article/repository/mysql/mysqlarticle_test.go @@ -1,4 +1,4 @@ -package repository_test +package mysql_test import ( "context" @@ -9,7 +9,8 @@ import ( "github.com/stretchr/testify/require" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" - articleRepo "github.com/bxcodec/go-clean-arch/article/repository" + "github.com/bxcodec/go-clean-arch/article/repository" + articleMysqlRepo "github.com/bxcodec/go-clean-arch/article/repository/mysql" "github.com/bxcodec/go-clean-arch/domain" ) @@ -44,8 +45,8 @@ func TestFetch(t *testing.T) { query := "SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE created_at > \\? ORDER BY created_at LIMIT \\?" mock.ExpectQuery(query).WillReturnRows(rows) - a := articleRepo.NewMysqlArticleRepository(db) - cursor := articleRepo.EncodeCursor(mockArticles[1].CreatedAt) + a := articleMysqlRepo.NewMysqlArticleRepository(db) + cursor := repository.EncodeCursor(mockArticles[1].CreatedAt) num := int64(2) list, nextCursor, err := a.Fetch(context.TODO(), cursor, num) assert.NotEmpty(t, nextCursor) @@ -70,7 +71,7 @@ func TestGetByID(t *testing.T) { query := "SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = \\?" mock.ExpectQuery(query).WillReturnRows(rows) - a := articleRepo.NewMysqlArticleRepository(db) + a := articleMysqlRepo.NewMysqlArticleRepository(db) num := int64(5) anArticle, err := a.GetByID(context.TODO(), num) @@ -103,7 +104,7 @@ func TestStore(t *testing.T) { prep := mock.ExpectPrepare(query) prep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.CreatedAt, ar.UpdatedAt).WillReturnResult(sqlmock.NewResult(12, 1)) - a := articleRepo.NewMysqlArticleRepository(db) + a := articleMysqlRepo.NewMysqlArticleRepository(db) err = a.Store(context.TODO(), ar) assert.NoError(t, err) @@ -125,7 +126,7 @@ func TestGetByTitle(t *testing.T) { query := "SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE title = \\?" mock.ExpectQuery(query).WillReturnRows(rows) - a := articleRepo.NewMysqlArticleRepository(db) + a := articleMysqlRepo.NewMysqlArticleRepository(db) title := "title 1" anArticle, err := a.GetByTitle(context.TODO(), title) @@ -148,7 +149,7 @@ func TestDelete(t *testing.T) { prep := mock.ExpectPrepare(query) prep.ExpectExec().WithArgs(12).WillReturnResult(sqlmock.NewResult(12, 1)) - a := articleRepo.NewMysqlArticleRepository(db) + a := articleMysqlRepo.NewMysqlArticleRepository(db) num := int64(12) err = a.Delete(context.TODO(), num) @@ -183,7 +184,7 @@ func TestUpdate(t *testing.T) { prep := mock.ExpectPrepare(query) prep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID).WillReturnResult(sqlmock.NewResult(12, 1)) - a := articleRepo.NewMysqlArticleRepository(db) + a := articleMysqlRepo.NewMysqlArticleRepository(db) err = a.Update(context.TODO(), ar) assert.NoError(t, err) diff --git a/author/repository/mysql_repository.go b/author/repository/mysql/mysql_repository.go similarity index 97% rename from author/repository/mysql_repository.go rename to author/repository/mysql/mysql_repository.go index a23f2b6..3f21036 100644 --- a/author/repository/mysql_repository.go +++ b/author/repository/mysql/mysql_repository.go @@ -1,4 +1,4 @@ -package repository +package mysql import ( "context" diff --git a/author/repository/mysql_test.go b/author/repository/mysql/mysql_test.go similarity index 90% rename from author/repository/mysql_test.go rename to author/repository/mysql/mysql_test.go index ed4fe4c..0d18df7 100644 --- a/author/repository/mysql_test.go +++ b/author/repository/mysql/mysql_test.go @@ -1,4 +1,4 @@ -package repository_test +package mysql_test import ( "context" @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" - "github.com/bxcodec/go-clean-arch/author/repository" + repository "github.com/bxcodec/go-clean-arch/author/repository/mysql" ) func TestGetByID(t *testing.T) { From 810272afd2294b9669745fbae7f8831d4cc69a3d Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Tue, 30 Apr 2019 14:13:51 +0700 Subject: [PATCH 07/11] chore(code-style): change the code styles --- article/delivery/http/article_handler.go | 3 +- article/repository/mysql/mysql_article.go | 47 ++++++++++----------- article/usecase/article_ucase.go | 23 ++++------ author/repository/mysql/mysql_repository.go | 1 - 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/article/delivery/http/article_handler.go b/article/delivery/http/article_handler.go index b73b9ce..271f3f9 100644 --- a/article/delivery/http/article_handler.go +++ b/article/delivery/http/article_handler.go @@ -9,7 +9,6 @@ import ( "github.com/sirupsen/logrus" validator "gopkg.in/go-playground/validator.v9" - // "github.com/bxcodec/go-clean-arch/article" "github.com/bxcodec/go-clean-arch/domain" ) @@ -92,13 +91,13 @@ func (a *ArticleHandler) Store(c echo.Context) error { if ok, err := isRequestValid(&article); !ok { return c.JSON(http.StatusBadRequest, err.Error()) } + ctx := c.Request().Context() if ctx == nil { ctx = context.Background() } err = a.AUsecase.Store(ctx, &article) - if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } diff --git a/article/repository/mysql/mysql_article.go b/article/repository/mysql/mysql_article.go index 3198e88..4aa9294 100644 --- a/article/repository/mysql/mysql_article.go +++ b/article/repository/mysql/mysql_article.go @@ -20,7 +20,7 @@ func NewMysqlArticleRepository(Conn *sql.DB) domain.ArticleRepository { return &mysqlArticleRepository{Conn} } -func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) ([]domain.Article, error) { +func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) @@ -34,7 +34,7 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . } }() - result := make([]domain.Article, 0) + result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) @@ -60,7 +60,7 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . return result, nil } -func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) { +func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE created_at > ? ORDER BY created_at LIMIT ? ` @@ -69,17 +69,16 @@ func (m *mysqlArticleRepository) Fetch(ctx context.Context, cursor string, num i return nil, "", domain.ErrBadParamInput } - res, err := m.fetch(ctx, query, decodedCursor, num) + res, err = m.fetch(ctx, query, decodedCursor, num) if err != nil { return nil, "", err } - nextCursor := "" if len(res) == int(num) { nextCursor = repository.EncodeCursor(res[len(res)-1].CreatedAt) } - return res, nextCursor, err + return } func (m *mysqlArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at @@ -116,72 +115,70 @@ func (m *mysqlArticleRepository) GetByTitle(ctx context.Context, title string) ( return } -func (m *mysqlArticleRepository) Store(ctx context.Context, a *domain.Article) error { +func (m *mysqlArticleRepository) Store(ctx context.Context, a *domain.Article) (err error) { query := `INSERT article SET title=? , content=? , author_id=?, updated_at=? , created_at=?` stmt, err := m.Conn.PrepareContext(ctx, query) if err != nil { - return err + return } res, err := stmt.ExecContext(ctx, a.Title, a.Content, a.Author.ID, a.UpdatedAt, a.CreatedAt) if err != nil { - return err + return } lastID, err := res.LastInsertId() if err != nil { - return err + return } a.ID = lastID - return nil + return } -func (m *mysqlArticleRepository) Delete(ctx context.Context, id int64) error { +func (m *mysqlArticleRepository) Delete(ctx context.Context, id int64) (err error) { query := "DELETE FROM article WHERE id = ?" stmt, err := m.Conn.PrepareContext(ctx, query) if err != nil { - return err + return } res, err := stmt.ExecContext(ctx, id) if err != nil { - - return err + return } rowsAfected, err := res.RowsAffected() if err != nil { - return err + return } if rowsAfected != 1 { err = fmt.Errorf("Weird Behaviour. Total Affected: %d", rowsAfected) - return err + return } - return nil + return } -func (m *mysqlArticleRepository) Update(ctx context.Context, ar *domain.Article) error { +func (m *mysqlArticleRepository) Update(ctx context.Context, ar *domain.Article) (err error) { query := `UPDATE article set title=?, content=?, author_id=?, updated_at=? WHERE ID = ?` stmt, err := m.Conn.PrepareContext(ctx, query) if err != nil { - return nil + return } res, err := stmt.ExecContext(ctx, ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID) if err != nil { - return err + return } affect, err := res.RowsAffected() if err != nil { - return err + return } if affect != 1 { err = fmt.Errorf("Weird Behaviour. Total Affected: %d", affect) - - return err + return } - return nil + return } diff --git a/article/usecase/article_ucase.go b/article/usecase/article_ucase.go index 5d494c1..8217688 100644 --- a/article/usecase/article_ucase.go +++ b/article/usecase/article_ucase.go @@ -81,7 +81,7 @@ func (a *articleUsecase) fillAuthorDetails(c context.Context, data []domain.Arti return data, nil } -func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) ([]domain.Article, string, error) { +func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) { if num == 0 { num = 10 } @@ -89,21 +89,19 @@ func (a *articleUsecase) Fetch(c context.Context, cursor string, num int64) ([]d ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() - listArticle, nextCursor, err := a.articleRepo.Fetch(ctx, cursor, num) + res, nextCursor, err = a.articleRepo.Fetch(ctx, cursor, num) if err != nil { return nil, "", err } - listArticle, err = a.fillAuthorDetails(ctx, listArticle) + res, err = a.fillAuthorDetails(ctx, res) if err != nil { - return nil, "", err + nextCursor = "" } - - return listArticle, nextCursor, nil + return } func (a *articleUsecase) GetByID(c context.Context, id int64) (res domain.Article, err error) { - ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() @@ -120,8 +118,7 @@ func (a *articleUsecase) GetByID(c context.Context, id int64) (res domain.Articl return } -func (a *articleUsecase) Update(c context.Context, ar *domain.Article) error { - +func (a *articleUsecase) Update(c context.Context, ar *domain.Article) (err error) { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() @@ -130,7 +127,6 @@ func (a *articleUsecase) Update(c context.Context, ar *domain.Article) error { } func (a *articleUsecase) GetByTitle(c context.Context, title string) (res domain.Article, err error) { - ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() res, err = a.articleRepo.GetByTitle(ctx, title) @@ -142,13 +138,12 @@ func (a *articleUsecase) GetByTitle(c context.Context, title string) (res domain if err != nil { return domain.Article{}, err } - res.Author = resAuthor + res.Author = resAuthor return } func (a *articleUsecase) Store(c context.Context, m *domain.Article) (err error) { - ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() existedArticle, _ := a.GetByTitle(ctx, m.Title) @@ -160,12 +155,12 @@ func (a *articleUsecase) Store(c context.Context, m *domain.Article) (err error) return } -func (a *articleUsecase) Delete(c context.Context, id int64) error { +func (a *articleUsecase) Delete(c context.Context, id int64) (err error) { ctx, cancel := context.WithTimeout(c, a.contextTimeout) defer cancel() existedArticle, err := a.articleRepo.GetByID(ctx, id) if err != nil { - return err + return } if existedArticle == (domain.Article{}) { return domain.ErrNotFound diff --git a/author/repository/mysql/mysql_repository.go b/author/repository/mysql/mysql_repository.go index 3f21036..f590b14 100644 --- a/author/repository/mysql/mysql_repository.go +++ b/author/repository/mysql/mysql_repository.go @@ -21,7 +21,6 @@ func NewMysqlAuthorRepository(db *sql.DB) domain.AuthorRepository { func (m *mysqlAuthorRepo) getOne(ctx context.Context, query string, args ...interface{}) (res domain.Author, err error) { stmt, err := m.DB.PrepareContext(ctx, query) if err != nil { - return domain.Author{}, err } row := stmt.QueryRowContext(ctx, args...) From 8fb2c6167539a9b6e996c4361692459a2a48b634 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sat, 18 Apr 2020 12:32:51 +0700 Subject: [PATCH 08/11] chore: re-organize the middleware --- .golangci.yaml | 82 +++++++++++++++++++ Dockerfile | 2 +- Makefile | 10 +-- app/main.go | 16 ++-- article/delivery/http/article_handler.go | 28 +++---- .../delivery/http/middleware}/middleware.go | 2 +- .../http/middleware}/middleware_test.go | 2 +- article/repository/mysql/mysql_article.go | 10 +-- docker-compose.yaml | 6 +- go.sum | 1 - 10 files changed, 114 insertions(+), 45 deletions(-) create mode 100644 .golangci.yaml rename {middleware => article/delivery/http/middleware}/middleware.go (91%) rename {middleware => article/delivery/http/middleware}/middleware_test.go (89%) diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..d2d632c --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,82 @@ +linters-settings: + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + golint: + min-confidence: 0.8 + gocyclo: + min-complexity: 20 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 5 + misspell: + locale: US + lll: + line-length: 160 + # tab width in spaces. Default to 1. + tab-width: 1 + funlen: + lines: 120 + statements: 50 + +linters: + # please, do not use `enable-all`: it's deprecated and will be removed soon. + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint + disable-all: true + enable: + - deadcode + - errcheck + - funlen + - goconst + # - gocritic + - gocyclo + - golint + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - staticcheck + - structcheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + +run: + # default concurrency is a available CPU number + concurrency: 2 + + skip-dirs: + # - test/testdata_etc + skip-files: + # - .*_test.go + +issues: + exclude-rules: + - path: internal/(cache|renameio)/ + linters: + - lll + - gochecknoinits + - gocyclo + - funlen + - path: .*_test.go + linters: + - funlen + exclude-use-default: false + exclude: + - should have a package comment + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2a862a5..2c35bd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Builder -FROM golang:1.12.8-alpine3.10 as builder +FROM golang:1.14.2-alpine3.11 as builder RUN apk update && apk upgrade && \ apk --update add git make diff --git a/Makefile b/Makefile index 25cbe65..937d95f 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ docker: docker build -t go-clean-arch . run: - docker-compose up -d + docker-compose up --build -d stop: docker-compose down @@ -26,12 +26,6 @@ lint-prepare: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s latest lint: - ./bin/golangci-lint run \ - --exclude-use-default=false \ - --enable=golint \ - --enable=gocyclo \ - --enable=goconst \ - --enable=unconvert \ - ./... + ./bin/golangci-lint run ./... .PHONY: clean install unittest build docker run stop vendor lint-prepare lint \ No newline at end of file diff --git a/app/main.go b/app/main.go index 0e11a5c..a2d634f 100644 --- a/app/main.go +++ b/app/main.go @@ -5,18 +5,17 @@ import ( "fmt" "log" "net/url" - "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/labstack/echo" "github.com/spf13/viper" - _articleHttpDeliver "github.com/bxcodec/go-clean-arch/article/delivery/http" + _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/mysql" _articleUcase "github.com/bxcodec/go-clean-arch/article/usecase" _authorRepo "github.com/bxcodec/go-clean-arch/author/repository/mysql" - "github.com/bxcodec/go-clean-arch/middleware" ) func init() { @@ -27,7 +26,7 @@ func init() { } if viper.GetBool(`debug`) { - fmt.Println("Service RUN on DEBUG mode") + log.Println("Service RUN on DEBUG mode") } } @@ -43,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 { @@ -59,14 +59,14 @@ func main() { }() e := echo.New() - middL := middleware.InitMiddleware() + middL := _articleHttpDeliveryMiddleware.InitMiddleware() e.Use(middL.CORS) authorRepo := _authorRepo.NewMysqlAuthorRepository(dbConn) ar := _articleRepo.NewMysqlArticleRepository(dbConn) timeoutContext := time.Duration(viper.GetInt("context.timeout")) * time.Second au := _articleUcase.NewArticleUsecase(ar, authorRepo, timeoutContext) - _articleHttpDeliver.NewArticleHandler(e, au) + _articleHttpDelivery.NewArticleHandler(e, au) log.Fatal(e.Start(viper.GetString("server.address"))) } diff --git a/article/delivery/http/article_handler.go b/article/delivery/http/article_handler.go index 271f3f9..d54a396 100644 --- a/article/delivery/http/article_handler.go +++ b/article/delivery/http/article_handler.go @@ -1,7 +1,6 @@ package http import ( - "context" "net/http" "strconv" @@ -39,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) } @@ -60,14 +57,12 @@ func (a *ArticleHandler) GetByID(c echo.Context) 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) } @@ -81,26 +76,24 @@ func isRequestValid(m *domain.Article) (bool, error) { } // Store will store the article by given request body -func (a *ArticleHandler) Store(c echo.Context) error { +func (a *ArticleHandler) Store(c echo.Context) (err error) { var article domain.Article - err := c.Bind(&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() - } - err = a.AUsecase.Store(ctx, &article) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } + return c.JSON(http.StatusCreated, article) } @@ -110,11 +103,9 @@ func (a *ArticleHandler) Delete(c echo.Context) error { if err != nil { 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 { @@ -128,6 +119,7 @@ func getStatusCode(err error) int { if err == nil { return http.StatusOK } + logrus.Error(err) switch err { case domain.ErrInternalServerError: diff --git a/middleware/middleware.go b/article/delivery/http/middleware/middleware.go similarity index 91% rename from middleware/middleware.go rename to article/delivery/http/middleware/middleware.go index 0ddf5d8..2424de4 100644 --- a/middleware/middleware.go +++ b/article/delivery/http/middleware/middleware.go @@ -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{} } diff --git a/middleware/middleware_test.go b/article/delivery/http/middleware/middleware_test.go similarity index 89% rename from middleware/middleware_test.go rename to article/delivery/http/middleware/middleware_test.go index 2b2cd41..8104480 100644 --- a/middleware/middleware_test.go +++ b/article/delivery/http/middleware/middleware_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/bxcodec/go-clean-arch/middleware" + "github.com/bxcodec/go-clean-arch/article/delivery/http/middleware" ) func TestCORS(t *testing.T) { diff --git a/article/repository/mysql/mysql_article.go b/article/repository/mysql/mysql_article.go index 4aa9294..381223a 100644 --- a/article/repository/mysql/mysql_article.go +++ b/article/repository/mysql/mysql_article.go @@ -28,9 +28,9 @@ func (m *mysqlArticleRepository) fetch(ctx context.Context, query string, args . } defer func() { - err := rows.Close() - if err != nil { - logrus.Error(err) + errRow := rows.Close() + if errRow != nil { + logrus.Error(errRow) } }() @@ -153,7 +153,7 @@ func (m *mysqlArticleRepository) Delete(ctx context.Context, id int64) (err erro } if rowsAfected != 1 { - err = fmt.Errorf("Weird Behaviour. Total Affected: %d", rowsAfected) + err = fmt.Errorf("Weird Behavior. Total Affected: %d", rowsAfected) return } @@ -176,7 +176,7 @@ func (m *mysqlArticleRepository) Update(ctx context.Context, ar *domain.Article) return } if affect != 1 { - err = fmt.Errorf("Weird Behaviour. Total Affected: %d", affect) + err = fmt.Errorf("Weird Behavior. Total Affected: %d", affect) return } diff --git a/docker-compose.yaml b/docker-compose.yaml index e43ed36..b8481ee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,16 +1,18 @@ version: "2.3" services: web: - image: go-clean-arch + build: + context: . + dockerfile: Dockerfile container_name: article_management_api ports: - 9090:9090 depends_on: mysql: condition: service_healthy - volumes: - ./config.json:/app/config.json + mysql: image: mysql:5.7 container_name: go_clean_arch_mysql diff --git a/go.sum b/go.sum index 04c4f00..b77ed38 100644 --- a/go.sum +++ b/go.sum @@ -55,7 +55,6 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= From d9098d43d4095ce39a623641c1e748cc3e274b70 Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sat, 18 Apr 2020 12:59:46 +0700 Subject: [PATCH 09/11] chore: add readme explanation --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index afab441..7cc2f04 100644 --- a/README.md +++ b/README.md @@ -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) + Proposed on 2017, archived to v1 branch on 2018 + 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) + Proposed on 2018, archived to v2 branch on 2020 + Desc: Improvement from v1. The story can be read here: https://medium.com/hackernoon/trying-clean-architecture-on-golang-2-44d615bf8fdf + +- v3: master branch + Proposed on 2019, merged to master on 2020. + Desc: Introducing Domain package, the details can be seen on this PR #21 ## Description This is an example of implementation of Clean Architecture in Go (Golang) projects. @@ -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 From d0e7a6b337d76aede1e5a4a112b25c7cd046360e Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sat, 18 Apr 2020 13:03:56 +0700 Subject: [PATCH 10/11] chore: update Readme styling --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7cc2f04..9b108a8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # go-clean-arch ## Changelog -- v1: checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1) - Proposed on 2017, archived to v1 branch on 2018 +- **v1**: checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1)
+ Proposed on 2017, archived to v1 branch on 2018
Desc: Initial proposal by me. The story can be read here: https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047 -- v2: checkout to the [v2 branch](https://github.com/bxcodec/go-clean-arch/tree/v2) - Proposed on 2018, archived to v2 branch on 2020 +- **v2**: checkout to the [v2 branch](https://github.com/bxcodec/go-clean-arch/tree/v2)
+ Proposed on 2018, archived to v2 branch on 2020
Desc: Improvement from v1. The story can be read here: https://medium.com/hackernoon/trying-clean-architecture-on-golang-2-44d615bf8fdf -- v3: master branch - Proposed on 2019, merged to master on 2020. +- **v3**: master branch
+ Proposed on 2019, merged to master on 2020.
Desc: Introducing Domain package, the details can be seen on this PR #21 ## Description From efd921210486a50be2f7ca8f61c3e11d2bad75ba Mon Sep 17 00:00:00 2001 From: Iman Tumorang Date: Sat, 18 Apr 2020 13:05:01 +0700 Subject: [PATCH 11/11] chore: add URL on link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b108a8..048312d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - **v3**: master branch
Proposed on 2019, merged to master on 2020.
- Desc: Introducing Domain package, the details can be seen on this PR #21 + 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.