Repository: bxcodec/go-clean-arch
Branch: master
Commit: e06c6d0cb370
Files: 40
Total size: 88.2 KB
Directory structure:
gitextract_qu10t1uh/
├── .air.toml
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ └── gotest.yml
├── .gitignore
├── .golangci.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app/
│ └── main.go
├── article/
│ ├── mocks/
│ │ ├── ArticleRepository.go
│ │ └── AuthorRepository.go
│ ├── service.go
│ └── service_test.go
├── article.sql
├── compose.yaml
├── domain/
│ ├── article.go
│ ├── author.go
│ └── errors.go
├── example.env
├── go.mod
├── go.sum
├── internal/
│ ├── README.md
│ ├── repository/
│ │ ├── helper.go
│ │ └── mysql/
│ │ ├── article.go
│ │ ├── article_test.go
│ │ ├── author.go
│ │ └── author_test.go
│ ├── rest/
│ │ ├── article.go
│ │ ├── article_test.go
│ │ ├── middleware/
│ │ │ ├── cors.go
│ │ │ ├── cors_test.go
│ │ │ └── timeout.go
│ │ └── mocks/
│ │ └── ArticleService.go
│ └── workers/
│ ├── .gitkeep
│ └── README.md
└── misc/
└── make/
├── help.Makefile
└── tools.Makefile
================================================
FILE CONTENTS
================================================
================================================
FILE: .air.toml
================================================
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root
root = "."
tmp_dir = "tmp_app"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp_app/app/engine ./app/."
# Binary file yields from `cmd`.
bin = "tmp_app/app"
# Customize binary.
full_bin = "./tmp_app/app/engine"
# This log file places in your tmp_dir.
log = "air_errors.log"
# Watch these filename extensions.
include_ext = ["go", "yaml", "toml"]
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Ignore these filename extensions or directories.
exclude_dir = ["tmp_app", "tmp"]
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
[log]
# Show log time
time = true
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
================================================
FILE: .dockerignore
================================================
engine
*.out
================================================
FILE: .github/FUNDING.yml
================================================
github: [bxcodec]
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/gotest.yml
================================================
name: Go Test
on:
push:
branches: ["main", "chore/upgrade-linter"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Linter
run: make lint
- name: Test
run: make tests
================================================
FILE: .gitignore
================================================
vendor/
article_clean
_*
*.test
.DS_Store
engine
bin/
*.out
tmp_app/
.env
================================================
FILE: .golangci.yaml
================================================
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
gocyclo:
min-complexity: 50
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US
revive:
confidence: 0.8
lll:
line-length: 160
# tab width in spaces. Default to 1.
tab-width: 1
funlen:
lines: 150
statements: 80
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:
- errcheck
- funlen
- goconst
- gocyclo
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- revive
- staticcheck
- typecheck
- unconvert
- unparam
- unused
# don't enable:
# - gochecknoglobals
# - gocognit
# - godox
# - maligned
# - prealloc
run:
skip-dirs:
# - test/testdata_etc
skip-files:
- ".*_test\\.go$"
issues:
exclude-rules:
#
================================================
FILE: Dockerfile
================================================
# Builder
FROM golang:1.20.7-alpine3.17 as builder
RUN apk update && apk upgrade && \
apk --update add git make bash build-base
WORKDIR /app
COPY . .
RUN make build
# Distribution
FROM alpine:latest
RUN apk update && apk upgrade && \
apk --update --no-cache add tzdata && \
mkdir /app
WORKDIR /app
EXPOSE 9090
COPY --from=builder /app/engine /app/
CMD /app/engine
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Iman Tumorang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Database
MYSQL_USER ?= user
MYSQL_PASSWORD ?= password
MYSQL_ADDRESS ?= 127.0.0.1:3306
MYSQL_DATABASE ?= article
# Exporting bin folder to the path for makefile
export PATH := $(PWD)/bin:$(PATH)
# Default Shell
export SHELL := bash
# Type of OS: Linux or Darwin.
export OSTYPE := $(shell uname -s | tr A-Z a-z)
export ARCH := $(shell uname -m)
# --- Tooling & Variables ----------------------------------------------------------------
include ./misc/make/tools.Makefile
include ./misc/make/help.Makefile
# ~~~ Development Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
up: dev-env dev-air ## Startup / Spinup Docker Compose and air
down: docker-stop ## Stop Docker
destroy: docker-teardown clean ## Teardown (removes volumes, tmp files, etc...)
install-deps: migrate air gotestsum tparse mockery ## Install Development Dependencies (localy).
deps: $(MIGRATE) $(AIR) $(GOTESTSUM) $(TPARSE) $(MOCKERY) $(GOLANGCI) ## Checks for Global Development Dependencies.
deps:
@echo "Required Tools Are Available"
dev-env: ## Bootstrap Environment (with a Docker-Compose help).
@ docker-compose up -d --build mysql
dev-env-test: dev-env ## Run application (within a Docker-Compose help)
@ $(MAKE) image-build
docker-compose up web
dev-air: $(AIR) ## Starts AIR ( Continuous Development app).
air
docker-stop:
@ docker-compose down
docker-teardown:
@ docker-compose down --remove-orphans -v
# ~~~ Code Actions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
lint: $(GOLANGCI) ## Runs golangci-lint with predefined configuration
@echo "Applying linter"
golangci-lint version
golangci-lint run -c .golangci.yaml ./...
# -trimpath - will remove the filepathes from the reports, good to same money on network trafic,
# focus on bug reports, and find issues fast.
# - race - adds a racedetector, in case of racecondition, you can catch report with sentry.
# https://golang.org/doc/articles/race_detector.html
#
# todo(butuzov): add additional flags to compiler to have an `version` flag.
build: ## Builds binary
@ printf "Building aplication... "
@ go build \
-trimpath \
-o engine \
./app/
@ echo "done"
build-race: ## Builds binary (with -race flag)
@ printf "Building aplication with race flag... "
@ go build \
-trimpath \
-race \
-o engine \
./app/
@ echo "done"
go-generate: $(MOCKERY) ## Runs go generte ./...
go generate ./...
TESTS_ARGS := --format testname --jsonfile gotestsum.json.out
TESTS_ARGS += --max-fails 2
TESTS_ARGS += -- ./...
TESTS_ARGS += -test.parallel 2
TESTS_ARGS += -test.count 1
TESTS_ARGS += -test.failfast
TESTS_ARGS += -test.coverprofile coverage.out
TESTS_ARGS += -test.timeout 5s
TESTS_ARGS += -race
tests: $(GOTESTSUM)
@ gotestsum $(TESTS_ARGS) -short
tests-complete: tests $(TPARSE) ## Run Tests & parse details
@cat gotestsum.json.out | $(TPARSE) -all -notests
# ~~~ Docker Build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ONESHELL:
image-build:
@ echo "Docker Build"
@ DOCKER_BUILDKIT=0 docker build \
--file Dockerfile \
--tag go-clean-arch \
.
# Commenting this as this not relevant for the project, we load the DB data from the SQL file.
# please refer this when introducing the database schema migrations.
# # ~~~ Database Migrations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# MYSQL_DSN := "mysql://$(MYSQL_USER):$(MYSQL_PASSWORD)@tcp($(MYSQL_ADDRESS))/$(MYSQL_DATABASE)"
# migrate-up: $(MIGRATE) ## Apply all (or N up) migrations.
# @ read -p "How many migration you wants to perform (default value: [all]): " N; \
# migrate -database $(MYSQL_DSN) -path=misc/migrations up ${NN}
# .PHONY: migrate-down
# migrate-down: $(MIGRATE) ## Apply all (or N down) migrations.
# @ read -p "How many migration you wants to perform (default value: [all]): " N; \
# migrate -database $(MYSQL_DSN) -path=misc/migrations down ${NN}
# .PHONY: migrate-drop
# migrate-drop: $(MIGRATE) ## Drop everything inside the database.
# migrate -database $(MYSQL_DSN) -path=misc/migrations drop
# .PHONY: migrate-create
# migrate-create: $(MIGRATE) ## Create a set of up/down migrations with a specified name.
# @ read -p "Please provide name for the migration: " Name; \
# migrate create -ext sql -dir misc/migrations $${Name}
# ~~~ Cleans ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
clean: clean-artifacts clean-docker
clean-artifacts: ## Removes Artifacts (*.out)
@printf "Cleanning artifacts... "
@rm -f *.out
@echo "done."
clean-docker: ## Removes dangling docker images
@ docker image prune -f
================================================
FILE: README.md
================================================
# go-clean-arch
## 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
- **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/@imantumorang/trying-clean-architecture-on-golang-2-44d615bf8fdf
- **v3**: checkout to the [v3 branch](https://github.com/bxcodec/go-clean-arch/tree/v3) <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)
- **v4**: master branch
Proposed on 2024, merged to master on 2024. <br>
Desc:
- Declare Interfaces to the consuming side,
- Introduce `internal` package
- Introduce `Service-focused` package.
Details can be seen in this PR [#88](https://github.com/bxcodec/go-clean-arch/pull/88).<br>
> ### Author's Note
>
> You may notice it diverges from the structures seen in previous versions. I encourage you to explore the branches for each version to select the structure that appeals to you the most. In my recent projects, the code structure has progressed to version 4. However, I do not strictly advocate for one version over another. You may encounter alternative examples on the internet that align more closely with your preferences. Rest assured, the foundational concept will remain consistent or at least bear resemblance. The differences are primarily in the arrangement of directories or the integration of advanced tools directly into the setup.
## Description
This is an example of implementation of Clean Architecture in Go (Golang) projects.
Rule of Clean Architecture by Uncle Bob
- Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
- Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
- Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
- Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
- Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.
More at https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
This project has 4 Domain layer :
- Models Layer
- Repository Layer
- Usecase Layer
- Delivery Layer
#### The diagram:

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 be 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
Since the project is already use Go Module, I recommend to put the source code in any folder but GOPATH.
#### Run the Testing
```bash
$ make tests
```
#### Run the Applications
Here is the steps to run it with `docker-compose`
```bash
#move to directory
$ cd workspace
# Clone into your workspace
$ git clone https://github.com/bxcodec/go-clean-arch.git
#move to project
$ cd go-clean-arch
# copy the example.env to .env
$ cp example.env .env
# Run the application
$ make up
# The hot reload will running
# Execute the call in another terminal
$ curl localhost:9090/articles
```
### Tools Used:
In this project, I use some tools listed below. But you can use any similar library that have the same purposes. But, well, different library will have different implementation type. Just be creative and use anything that you really need.
- All libraries listed in [`go.mod`](https://github.com/bxcodec/go-clean-arch/blob/master/go.mod)
- ["github.com/vektra/mockery".](https://github.com/vektra/mockery) To Generate Mocks for testing needs.
================================================
FILE: app/main.go
================================================
package main
import (
"database/sql"
"fmt"
"log"
"net/url"
"os"
"strconv"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/labstack/echo/v4"
mysqlRepo "github.com/bxcodec/go-clean-arch/internal/repository/mysql"
"github.com/bxcodec/go-clean-arch/article"
"github.com/bxcodec/go-clean-arch/internal/rest"
"github.com/bxcodec/go-clean-arch/internal/rest/middleware"
"github.com/joho/godotenv"
)
const (
defaultTimeout = 30
defaultAddress = ":9090"
)
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
func main() {
//prepare database
dbHost := os.Getenv("DATABASE_HOST")
dbPort := os.Getenv("DATABASE_PORT")
dbUser := os.Getenv("DATABASE_USER")
dbPass := os.Getenv("DATABASE_PASS")
dbName := os.Getenv("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 {
log.Fatal("failed to open connection to database", err)
}
err = dbConn.Ping()
if err != nil {
log.Fatal("failed to ping database ", err)
}
defer func() {
err := dbConn.Close()
if err != nil {
log.Fatal("got error when closing the DB connection", err)
}
}()
// prepare echo
e := echo.New()
e.Use(middleware.CORS)
timeoutStr := os.Getenv("CONTEXT_TIMEOUT")
timeout, err := strconv.Atoi(timeoutStr)
if err != nil {
log.Println("failed to parse timeout, using default timeout")
timeout = defaultTimeout
}
timeoutContext := time.Duration(timeout) * time.Second
e.Use(middleware.SetRequestContextWithTimeout(timeoutContext))
// Prepare Repository
authorRepo := mysqlRepo.NewAuthorRepository(dbConn)
articleRepo := mysqlRepo.NewArticleRepository(dbConn)
// Build service Layer
svc := article.NewService(articleRepo, authorRepo)
rest.NewArticleHandler(e, svc)
// Start Server
address := os.Getenv("SERVER_ADDRESS")
if address == "" {
address = defaultAddress
}
log.Fatal(e.Start(address)) //nolint
}
================================================
FILE: article/mocks/ArticleRepository.go
================================================
// Code generated by mockery v2.42.0. DO NOT EDIT.
package mocks
import (
context "context"
domain "github.com/bxcodec/go-clean-arch/domain"
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)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
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)
if len(ret) == 0 {
panic("no return value specified for Fetch")
}
var r0 []domain.Article
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]domain.Article, string, error)); ok {
return rf(ctx, cursor, num)
}
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)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok {
r1 = rf(ctx, cursor, num)
} else {
r1 = ret.Get(1).(string)
}
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)
if len(ret) == 0 {
panic("no return value specified for GetByID")
}
var r0 domain.Article
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Article, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(domain.Article)
}
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)
if len(ret) == 0 {
panic("no return value specified for GetByTitle")
}
var r0 domain.Article
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Article, error)); ok {
return rf(ctx, title)
}
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok {
r0 = rf(ctx, title)
} else {
r0 = ret.Get(0).(domain.Article)
}
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)
if len(ret) == 0 {
panic("no return value specified for Store")
}
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)
if len(ret) == 0 {
panic("no return value specified for Update")
}
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
}
// NewArticleRepository creates a new instance of ArticleRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewArticleRepository(t interface {
mock.TestingT
Cleanup(func())
}) *ArticleRepository {
mock := &ArticleRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: article/mocks/AuthorRepository.go
================================================
// Code generated by mockery v2.42.0. DO NOT EDIT.
package mocks
import (
context "context"
domain "github.com/bxcodec/go-clean-arch/domain"
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)
if len(ret) == 0 {
panic("no return value specified for GetByID")
}
var r0 domain.Author
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Author, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Author); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(domain.Author)
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewAuthorRepository creates a new instance of AuthorRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAuthorRepository(t interface {
mock.TestingT
Cleanup(func())
}) *AuthorRepository {
mock := &AuthorRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: article/service.go
================================================
package article
import (
"context"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/bxcodec/go-clean-arch/domain"
)
// ArticleRepository represent the article's repository contract
//
//go:generate mockery --name ArticleRepository
type ArticleRepository interface {
Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error)
GetByID(ctx context.Context, id int64) (domain.Article, error)
GetByTitle(ctx context.Context, title string) (domain.Article, error)
Update(ctx context.Context, ar *domain.Article) error
Store(ctx context.Context, a *domain.Article) error
Delete(ctx context.Context, id int64) error
}
// AuthorRepository represent the author's repository contract
//
//go:generate mockery --name AuthorRepository
type AuthorRepository interface {
GetByID(ctx context.Context, id int64) (domain.Author, error)
}
type Service struct {
articleRepo ArticleRepository
authorRepo AuthorRepository
}
// NewService will create a new article service object
func NewService(a ArticleRepository, ar AuthorRepository) *Service {
return &Service{
articleRepo: a,
authorRepo: ar,
}
}
/*
* In this function below, I'm using errgroup with the pipeline pattern
* Look how this works in this package explanation
* in godoc: https://godoc.org/golang.org/x/sync/errgroup#ex-Group--Pipeline
*/
func (a *Service) fillAuthorDetails(ctx context.Context, data []domain.Article) ([]domain.Article, error) {
g, ctx := errgroup.WithContext(ctx)
// Get the author's id
mapAuthors := map[int64]domain.Author{}
for _, article := range data { //nolint
mapAuthors[article.Author.ID] = domain.Author{}
}
// Using goroutine to fetch the author's detail
chanAuthor := make(chan domain.Author)
for authorID := range mapAuthors {
authorID := authorID
g.Go(func() error {
res, err := a.authorRepo.GetByID(ctx, authorID)
if err != nil {
return err
}
chanAuthor <- res
return nil
})
}
go func() {
defer close(chanAuthor)
err := g.Wait()
if err != nil {
logrus.Error(err)
return
}
}()
for author := range chanAuthor {
if author != (domain.Author{}) {
mapAuthors[author.ID] = author
}
}
if err := g.Wait(); err != nil {
return nil, err
}
// merge the author's data
for index, item := range data { //nolint
if a, ok := mapAuthors[item.Author.ID]; ok {
data[index].Author = a
}
}
return data, nil
}
func (a *Service) Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) {
res, nextCursor, err = a.articleRepo.Fetch(ctx, cursor, num)
if err != nil {
return nil, "", err
}
res, err = a.fillAuthorDetails(ctx, res)
if err != nil {
nextCursor = ""
}
return
}
func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {
res, err = a.articleRepo.GetByID(ctx, id)
if err != nil {
return
}
resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)
if err != nil {
return domain.Article{}, err
}
res.Author = resAuthor
return
}
func (a *Service) Update(ctx context.Context, ar *domain.Article) (err error) {
ar.UpdatedAt = time.Now()
return a.articleRepo.Update(ctx, ar)
}
func (a *Service) GetByTitle(ctx context.Context, title string) (res domain.Article, err error) {
res, err = a.articleRepo.GetByTitle(ctx, title)
if err != nil {
return
}
resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)
if err != nil {
return domain.Article{}, err
}
res.Author = resAuthor
return
}
func (a *Service) Store(ctx context.Context, m *domain.Article) (err error) {
existedArticle, _ := a.GetByTitle(ctx, m.Title) // ignore if any error
if existedArticle != (domain.Article{}) {
return domain.ErrConflict
}
err = a.articleRepo.Store(ctx, m)
return
}
func (a *Service) Delete(ctx context.Context, id int64) (err error) {
existedArticle, err := a.articleRepo.GetByID(ctx, id)
if err != nil {
return
}
if existedArticle == (domain.Article{}) {
return domain.ErrNotFound
}
return a.articleRepo.Delete(ctx, id)
}
================================================
FILE: article/service_test.go
================================================
package article_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/bxcodec/go-clean-arch/article"
"github.com/bxcodec/go-clean-arch/article/mocks"
"github.com/bxcodec/go-clean-arch/domain"
)
func TestFetchArticle(t *testing.T) {
mockArticleRepo := new(mocks.ArticleRepository)
mockArticle := domain.Article{
Title: "Hello",
Content: "Content",
}
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 := domain.Author{
ID: 1,
Name: "Iman Tumorang",
}
mockAuthorrepo := new(mocks.AuthorRepository)
mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
num := int64(1)
cursor := "12"
list, nextCursor, err := u.Fetch(context.TODO(), cursor, num)
cursorExpected := "next-cursor"
assert.Equal(t, cursorExpected, nextCursor)
assert.NotEmpty(t, nextCursor)
assert.NoError(t, err)
assert.Len(t, list, len(mockListArtilce))
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
t.Run("error-failed", func(t *testing.T) {
mockArticleRepo.On("Fetch", mock.Anything, mock.AnythingOfType("string"),
mock.AnythingOfType("int64")).Return(nil, "", errors.New("Unexpexted Error")).Once()
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
num := int64(1)
cursor := "12"
list, nextCursor, err := u.Fetch(context.TODO(), cursor, num)
assert.Empty(t, nextCursor)
assert.Error(t, err)
assert.Len(t, list, 0)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
}
func TestGetByID(t *testing.T) {
mockArticleRepo := new(mocks.ArticleRepository)
mockArticle := domain.Article{
Title: "Hello",
Content: "Content",
}
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(mocks.AuthorRepository)
mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
a, err := u.GetByID(context.TODO(), mockArticle.ID)
assert.NoError(t, err)
assert.NotNil(t, a)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
t.Run("error-failed", func(t *testing.T) {
mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, errors.New("Unexpected")).Once()
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
a, err := u.GetByID(context.TODO(), mockArticle.ID)
assert.Error(t, err)
assert.Equal(t, domain.Article{}, a)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
}
func TestStore(t *testing.T) {
mockArticleRepo := new(mocks.ArticleRepository)
mockArticle := domain.Article{
Title: "Hello",
Content: "Content",
}
t.Run("success", func(t *testing.T) {
tempMockArticle := mockArticle
tempMockArticle.ID = 0
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(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Store(context.TODO(), &tempMockArticle)
assert.NoError(t, err)
assert.Equal(t, mockArticle.Title, tempMockArticle.Title)
mockArticleRepo.AssertExpectations(t)
})
t.Run("existing-title", func(t *testing.T) {
existingArticle := mockArticle
mockArticleRepo.On("GetByTitle", mock.Anything, mock.AnythingOfType("string")).Return(existingArticle, nil).Once()
mockAuthor := domain.Author{
ID: 1,
Name: "Iman Tumorang",
}
mockAuthorrepo := new(mocks.AuthorRepository)
mockAuthorrepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(mockAuthor, nil)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Store(context.TODO(), &mockArticle)
assert.Error(t, err)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
}
func TestDelete(t *testing.T) {
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("Delete", mock.Anything, mock.AnythingOfType("int64")).Return(nil).Once()
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Delete(context.TODO(), mockArticle.ID)
assert.NoError(t, err)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
t.Run("article-is-not-exist", func(t *testing.T) {
mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, nil).Once()
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Delete(context.TODO(), mockArticle.ID)
assert.Error(t, err)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
t.Run("error-happens-in-db", func(t *testing.T) {
mockArticleRepo.On("GetByID", mock.Anything, mock.AnythingOfType("int64")).Return(domain.Article{}, errors.New("Unexpected Error")).Once()
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Delete(context.TODO(), mockArticle.ID)
assert.Error(t, err)
mockArticleRepo.AssertExpectations(t)
mockAuthorrepo.AssertExpectations(t)
})
}
func TestUpdate(t *testing.T) {
mockArticleRepo := new(mocks.ArticleRepository)
mockArticle := domain.Article{
Title: "Hello",
Content: "Content",
ID: 23,
}
t.Run("success", func(t *testing.T) {
mockArticleRepo.On("Update", mock.Anything, &mockArticle).Once().Return(nil)
mockAuthorrepo := new(mocks.AuthorRepository)
u := article.NewService(mockArticleRepo, mockAuthorrepo)
err := u.Update(context.TODO(), &mockArticle)
assert.NoError(t, err)
mockArticleRepo.AssertExpectations(t)
})
}
================================================
FILE: article.sql
================================================
CREATE DATABASE IF NOT EXISTS `article` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;
USE `article`;
-- MySQL dump 10.13 Distrib 5.7.17, for macos10.12 (x86_64)
--
-- Host: localhost Database: article
-- ------------------------------------------------------
-- Server version 5.7.18
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `article`
--
DROP TABLE IF EXISTS `article`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(45) COLLATE utf8_unicode_ci NOT NULL,
`content` longtext COLLATE utf8_unicode_ci NOT NULL,
`author_id` int(11) DEFAULT '0',
`updated_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `article`
--
LOCK TABLES `article` WRITE;
/*!40000 ALTER TABLE `article` DISABLE KEYS */;
INSERT INTO `article` VALUES (1,'Makan Ayam','<p>But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful.</p>\n\n<p>Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?</p>\n\n<p>On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish.</p>\n\n<p>In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains.</p>\n\n<p>But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness.But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure? On the</p>\n\n',1,'2017-05-18 13:50:19','2017-05-18 13:50:19'),(2,'Makan Ikan','<h1>Odio Mollis Turpis Dictumst</h1>\n\n<p><em>Ut</em> arcu tempor auctor pellentesque vitae lacinia potenti amet tellus sagittis molestie aliquam <strong>est</strong> mi facilisi amet, pretium <strong>torquent</strong> platea curabitur dolor pretium ultricies semper, phasellus commodo montes ut metus neque commodo platea a platea. Urna luctus cubilia faucibus class dolor nonummy orci dictumst amet ligula posuere hendrerit feugiat. Cursus dignissim ligula ultricies <em>leo</em> curae; nibh.</p>\n\n<p>Auctor sodales non euismod eros sodales rhoncus justo sit. Tristique primis <em>montes</em> condimentum <em>luctus</em> sagittis pretium Fringilla ligula sociosqu nibh.</p>\n\n<p>Mus Hymenaeos ultricies primis lacus pretium id. Ullamcorper dapibus magnis tellus maecenas eget purus magna maecenas sollicitudin sagittis convallis senectus maecenas <strong>sociis</strong> purus orci mollis ridiculus velit tristique nulla enim sodales cubilia eleifend.</p>\n\n<p><em>Risus</em> quam lacus sociosqu Malesuada. Mattis pretium etiam egestas. Interdum ultrices <em>luctus</em> luctus rutrum pellentesque amet, tincidunt.</p>\n\n<p>Accumsan at sociis dolor Fusce lacus lorem imperdiet tristique. Est sed. Sapien proin <em>in</em> vivamus sociosqu tempus. Risus. Feugiat. Et nam dapibus <strong>tristique</strong> donec id, mollis euismod. Lorem, nisi.</p>\n\n<p>Ut torquent curabitur blandit sociis nam sollicitudin tristique convallis aptent accumsan aliquam dictum imperdiet lacus imperdiet fermentum cum at urna neque sem curabitur facilisi hymenaeos dapibus. Diam vehicula. Urna hendrerit duis.</p>\n\n<p>Eget Convallis non senectus justo varius, sociis semper ullamcorper donec, molestie curae; metus ut sagittis. Mattis feugiat consectetuer inceptos ac.</p>\n\n<p>Natoque libero egestas vitae egestas aenean viverra nostra ornare. Per. <em>Aenean</em> cum elit ridiculus per.</p>\n\n<p>Massa hymenaeos Gravida parturient Cubilia laoreet, morbi duis interdum neque. Eu natoque elementum placerat sagittis Tincidunt facilisi sollicitudin tristique auctor donec arcu. Purus libero netus.</p>\n\n<p>Curae; erat eget fames sociosqu, egestas auctor est orci luctus. Nibh elit non aenean pulvinar elementum rutrum eleifend habitasse dictum dapibus velit urna cras. Massa elit ac, nascetur. <strong>Ut</strong> vestibulum montes. Lorem a.</p>\n\n<p>Ultricies varius. Dapibus nam sagittis porta augue per. Hac velit. Elementum penatibus. Condimentum velit. Amet integer litora tempor mus eros curabitur Libero.</p>\n\n<p>Dapibus senectus magna. Arcu, dignissim tempor nascetur lobortis conubia ornare netus vivamus. Nascetur ad habitasse elementum rutrum parturient sapien pretium penatibus. Posuere etiam massa nisi. Imperdiet et sem habitasse.</p>\n\n<p>Lorem lectus natoque fames molestie fermentum at leo. Cubilia, fringilla nibh libero tempus. <strong>Hac</strong> platea, volutpat Pretium ultrices dictum. Malesuada ut integer senectus eros phasellus congue nam sociosqu Suspendisse a, a commodo commodo scelerisque.</p>\n\n<p>Convallis sollicitudin non dui elit cubilia quis ullamcorper praesent tincidunt viverra mauris <em>integer</em> nostra gravida enim pellentesque faucibus sociosqu dapibus erat cursus.</p>\n\n<p>Interdum id cras mauris class Cubilia sagittis faucibus consectetuer Per ante lacus. Eget donec nec phasellus. Eu metus tempor suscipit eleifend. Fames at.</p>\n\n Mattis bibendum <em>faucibus</em> nullam. Porta.</p>\n\n<p>Pede neque mollis. Per netus interdum mus eleifend <em>massa</em> aliquet etiam feugiat eget penatibus dapibus cras penatibus ac. Dictum elementum fermentum fermentum. In netus dictumst.</p>\n\n<p>Lacus habitant lobortis. Potenti. Vulputate enim habitasse, tellus <em>parturient</em> litora a orci sociis tellus. Vel cursus nec dolor. Orci lectus tristique augue ad, aenean fringilla volutpat natoque ante. Pretium hymenaeos ridiculus penatibus nisi. Curae;.</p>\n\n<p>Mus. Aenean potenti sit nisi, dui. Consequat. Porta pellentesque lorem, dignissim nibh Diam in pretium venenatis. Quisque molestie.</p>\n\n<p>Vitae felis cum non torquent. Condimentum magna vitae erat diam. Sed duis pharetra dictum a facilisi euismod nullam, dis, risus tellus hac aliquam.</p>\n\n<p>Tellus. Nunc <strong>neque</strong> proin libero <em>praesent</em> nisl torquent integer torquent feugiat urna metus taciti montes enim. Torquent Laoreet, suscipit magna litora cras mattis suspendisse per.</p>\n\n<p>Diam et. Dui purus congue <strong>a</strong> senectus arcu adipiscing netus hendrerit ridiculus cubilia non. Viverra morbi augue luctus ipsum scelerisque habitasse eleifend egestas <em>tempor</em> diam sociosqu imperdiet penatibus <strong>vehicula</strong> placerat eu.</p>\n\n<p>Fusce leo ligula scelerisque malesuada purus adipiscing vehicula praesent, lorem fames massa adipiscing condimentum magna rhoncus purus mattis sem, fringilla natoque potenti pharetra eu nisi est.</p>\n\n<p>Metus mauris luctus sit fermentum cras facilisis. Dapibus augue lobortis sem fames sed quisque sollicitudin risus etiam. Lacus. Leo. Congue eros <em>nam</em> ultrices feugiat. Ante condimentum mus. <em>Curabitur</em> porttitor. Ante varius nullam ullamcorper <strong>gravida</strong> egestas.</p>\n\n<p>Iaculis hymenaeos Phasellus nulla at primis Dis commodo semper ornare turpis amet nulla. Morbi Consectetuer cum a facilisi metus quam interdum imperdiet netus ante urna.</p>',1,'2017-05-18 13:50:19','2017-05-18 13:50:19'),(3,'Makan Sayur','Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi id odio tortor. Pellentesque in efficitur velit. Aenean nec iaculis turpis. Ut eget lorem et velit lacinia mollis finibus vel felis. Sed ut elit leo. Curabitur eu ultrices ligula. Integer pulvinar nisl vitae lacinia porttitor. Maecenas mollis lacus quis turpis semper consequat.\n\nNullam sit amet augue non erat consectetur faucibus vitae eu nisi. Suspendisse non consectetur justo. Duis sed feugiat risus. Pellentesque euismod tellus pellentesque quam condimentum mollis. Phasellus est metus, tempus sit amet viverra tincidunt, lacinia at est. Aenean quis lacus nunc. Suspendisse accumsan nisl sit amet vestibulum molestie. Praesent quis justo congue, condimentum odio non, sollicitudin diam. Sed aliquam risus et urna pulvinar imperdiet. Praesent ac est velit. Sed sit amet volutpat enim, vehicula posuere diam.\n\nNunc sodales, arcu sed euismod sollicitudin, risus nisl fringilla nibh, nec venenatis dolor mi et lorem. Donec dapibus tempus porttitor. Suspendisse et tincidunt dolor. Suspendisse rhoncus faucibus tortor, in condimentum lacus gravida ac. Mauris eleifend blandit erat in interdum. Proin elementum nisi posuere quam scelerisque laoreet. Sed rutrum urna ante, vitae molestie diam lacinia a. In pretium mauris quam. Praesent vehicula odio dui, at sagittis orci bibendum quis.\n\nMauris a euismod ligula. Pellentesque sollicitudin vitae ante eget commodo. Etiam quis interdum lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent a sapien eros. Nam varius quis lorem id ultrices. Etiam posuere tortor nec aliquam convallis. Praesent id tincidunt velit. Cras commodo ex a orci pellentesque bibendum. Duis at ex eu diam tincidunt placerat. Duis odio ante, rutrum ac laoreet eget, fringilla id metus. Vivamus non nisi vestibulum, lacinia elit in, consequat dui. Proin mattis felis metus, ut dignissim tellus finibus eget. Curabitur auctor leo mattis est blandit, eu consectetur sem maximus.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras imperdiet magna lacus, vel luctus quam pulvinar a. In massa turpis, vestibulum vel tortor laoreet, malesuada porttitor nisi. Sed faucibus vulputate nunc, ac semper dui auctor in. Nunc convallis efficitur malesuada. Nulla facilisi. In et tristique est, vel aliquam massa. Donec iaculis, urna rhoncus pharetra tincidunt, arcu risus consequat lacus, sed dapibus nisi elit luctus tellus. You need a little dummy text for your mockup? How quaint.\n\nI bet you’re still using Bootstrap too…',1,'2017-05-18 13:50:19','2017-05-18 13:50:19');
/*!40000 ALTER TABLE `article` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `article_category`
--
DROP TABLE IF EXISTS `article_category`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `article_category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`article_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `composite` (`article_id`,`category_id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `article_category`
--
LOCK TABLES `article_category` WRITE;
/*!40000 ALTER TABLE `article_category` DISABLE KEYS */;
INSERT INTO `article_category` VALUES (1,1,1),(2,1,2),(3,1,3),(4,2,1),(5,2,2),(6,2,3),(7,3,3),(8,4,3),(9,5,2),(11,6,1),(10,6,2);
/*!40000 ALTER TABLE `article_category` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `author`
--
DROP TABLE IF EXISTS `author`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `author` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(200) COLLATE utf8_unicode_ci DEFAULT '""',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `author`
--
LOCK TABLES `author` WRITE;
/*!40000 ALTER TABLE `author` DISABLE KEYS */;
INSERT INTO `author` VALUES (1,'Iman Tumorang','2017-05-18 13:50:19','2017-05-18 13:50:19');
/*!40000 ALTER TABLE `author` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `category`
--
DROP TABLE IF EXISTS `category`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) COLLATE utf8_unicode_ci NOT NULL,
`tag` varchar(45) COLLATE utf8_unicode_ci NOT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `category`
--
LOCK TABLES `category` WRITE;
/*!40000 ALTER TABLE `category` DISABLE KEYS */;
INSERT INTO `category` VALUES (1,'Makanan','food','2017-05-18 13:50:19','2017-05-18 13:50:19'),(2,'Kehidupan','life','2017-05-18 13:50:19','2017-05-18 13:50:19'),(3,'Kasih Sayang','love','2017-05-18 13:50:19','2017-05-18 13:50:19');
/*!40000 ALTER TABLE `category` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2017-12-13 17:17:00
================================================
FILE: compose.yaml
================================================
version: "3.7"
services:
web:
image: go-clean-arch
container_name: article_management_api
ports:
- 9090:9090
depends_on:
mysql:
condition: service_healthy
volumes:
- ./config.json:/app/config.json
mysql:
image: mysql:8.3
container_name: go_clean_arch_mysql
command: mysqld --user=root
volumes:
- ./article.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- 3306:3306
environment:
- MYSQL_DATABASE=article
- MYSQL_USER=user
- MYSQL_PASSWORD=password
- MYSQL_ROOT_PASSWORD=root
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s
retries: 10
================================================
FILE: domain/article.go
================================================
package domain
import (
"time"
)
// Article is representing the Article data struct
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"`
}
================================================
FILE: domain/author.go
================================================
package domain
// Author representing the Author data struct
type Author struct {
ID int64 `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
================================================
FILE: domain/errors.go
================================================
package domain
import "errors"
var (
// ErrInternalServerError will throw if any the Internal Server Error happen
ErrInternalServerError = errors.New("internal Server Error")
// ErrNotFound will throw if the requested item is not exists
ErrNotFound = errors.New("your requested Item is not found")
// ErrConflict will throw if the current action already exists
ErrConflict = errors.New("your Item already exist")
// ErrBadParamInput will throw if the given request-body or params is not valid
ErrBadParamInput = errors.New("given Param is not valid")
)
================================================
FILE: example.env
================================================
DEBUG = True
SERVER_ADDRESS = ":9090"
CONTEXT_TIMEOUT = 2
DATABASE_HOST = "localhost"
DATABASE_PORT = "3306"
DATABASE_USER = "user"
DATABASE_PASS = "password"
DATABASE_NAME = "article"
================================================
FILE: go.mod
================================================
module github.com/bxcodec/go-clean-arch
go 1.20
require (
github.com/go-faker/faker/v4 v4.3.0
github.com/go-sql-driver/mysql v1.7.1
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v4 v4.11.4
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
golang.org/x/sync v0.6.0
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
gopkg.in/go-playground/validator.v9 v9.31.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faker/faker/v4 v4.3.0 h1:UXOW7kn/Mwd0u6MR30JjUKVzguT20EB/hBOddAAO+DY=
github.com/go-faker/faker/v4 v4.3.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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 v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: internal/README.md
================================================
# Internal Directory Guidelines
This directory is designated for internal processes. Golang operates on a package system, where each directory is effectively treated as a package. These packages can be imported into other projects as libraries. To prevent the exposure of detailed implementations, such as database handling or cache management, to the public, these are encapsulated within the internal package.
The concept of the internal folder is inspired directly by the Golang compiler. This approach is detailed in the [Release Notes Go 1.4](https://golang.org/doc/go1.4#internalpackages). Consequently, functions, structures, or interfaces within this directory are inaccessible for importation by external projects but remain available for use within this project.
An "external project" refers to any project distinct from the current one. For instance, if there is an authentication service written in Go and this project, the authentication service can incorporate this project as a module/library. However, it will not have visibility into the specific implementations housed within the /internal directory.
================================================
FILE: internal/repository/helper.go
================================================
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))
}
================================================
FILE: internal/repository/mysql/article.go
================================================
package mysql
import (
"context"
"database/sql"
"fmt"
"github.com/sirupsen/logrus"
"github.com/bxcodec/go-clean-arch/domain"
"github.com/bxcodec/go-clean-arch/internal/repository"
)
type ArticleRepository struct {
Conn *sql.DB
}
// NewArticleRepository will create an object that represent the article.Repository interface
func NewArticleRepository(conn *sql.DB) *ArticleRepository {
return &ArticleRepository{conn}
}
func (m *ArticleRepository) 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)
return nil, err
}
defer func() {
errRow := rows.Close()
if errRow != nil {
logrus.Error(errRow)
}
}()
result = make([]domain.Article, 0)
for rows.Next() {
t := domain.Article{}
authorID := int64(0)
err = rows.Scan(
&t.ID,
&t.Title,
&t.Content,
&authorID,
&t.UpdatedAt,
&t.CreatedAt,
)
if err != nil {
logrus.Error(err)
return nil, err
}
t.Author = domain.Author{
ID: authorID,
}
result = append(result, t)
}
return result, nil
}
func (m *ArticleRepository) 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 ? `
decodedCursor, err := repository.DecodeCursor(cursor)
if err != nil && cursor != "" {
return nil, "", domain.ErrBadParamInput
}
res, err = m.fetch(ctx, query, decodedCursor, num)
if err != nil {
return nil, "", err
}
if len(res) == int(num) {
nextCursor = repository.EncodeCursor(res[len(res)-1].CreatedAt)
}
return
}
func (m *ArticleRepository) 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 domain.Article{}, err
}
if len(list) > 0 {
res = list[0]
} else {
return res, domain.ErrNotFound
}
return
}
func (m *ArticleRepository) 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
}
if len(list) > 0 {
res = list[0]
} else {
return res, domain.ErrNotFound
}
return
}
func (m *ArticleRepository) 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
}
res, err := stmt.ExecContext(ctx, a.Title, a.Content, a.Author.ID, a.UpdatedAt, a.CreatedAt)
if err != nil {
return
}
lastID, err := res.LastInsertId()
if err != nil {
return
}
a.ID = lastID
return
}
func (m *ArticleRepository) 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
}
res, err := stmt.ExecContext(ctx, id)
if err != nil {
return
}
rowsAfected, err := res.RowsAffected()
if err != nil {
return
}
if rowsAfected != 1 {
err = fmt.Errorf("weird Behavior. Total Affected: %d", rowsAfected)
return
}
return
}
func (m *ArticleRepository) 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
}
res, err := stmt.ExecContext(ctx, ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID)
if err != nil {
return
}
affect, err := res.RowsAffected()
if err != nil {
return
}
if affect != 1 {
err = fmt.Errorf("weird Behavior. Total Affected: %d", affect)
return
}
return
}
================================================
FILE: internal/repository/mysql/article_test.go
================================================
package mysql_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
"github.com/bxcodec/go-clean-arch/domain"
"github.com/bxcodec/go-clean-arch/internal/repository"
articleMysqlRepo "github.com/bxcodec/go-clean-arch/internal/repository/mysql"
)
func TestFetchArticle(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
mockArticles := []domain.Article{
{
ID: 1, Title: "title 1", Content: "content 1",
Author: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(),
},
{
ID: 2, Title: "title 2", Content: "content 2",
Author: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(),
},
}
rows := sqlmock.NewRows([]string{"id", "title", "content", "author_id", "updated_at", "created_at"}).
AddRow(mockArticles[0].ID, mockArticles[0].Title, mockArticles[0].Content,
mockArticles[0].Author.ID, mockArticles[0].UpdatedAt, mockArticles[0].CreatedAt).
AddRow(mockArticles[1].ID, mockArticles[1].Title, mockArticles[1].Content,
mockArticles[1].Author.ID, mockArticles[1].UpdatedAt, mockArticles[1].CreatedAt)
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 := articleMysqlRepo.NewArticleRepository(db)
cursor := repository.EncodeCursor(mockArticles[1].CreatedAt)
num := int64(2)
list, nextCursor, err := a.Fetch(context.TODO(), cursor, num)
assert.NotEmpty(t, nextCursor)
assert.NoError(t, err)
assert.Len(t, list, 2)
}
func TestGetArticleByID(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
rows := sqlmock.NewRows([]string{"id", "title", "content", "author_id", "updated_at", "created_at"}).
AddRow(1, "title 1", "Content 1", 1, time.Now(), time.Now())
query := "SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = \\?"
mock.ExpectQuery(query).WillReturnRows(rows)
a := articleMysqlRepo.NewArticleRepository(db)
num := int64(5)
anArticle, err := a.GetByID(context.TODO(), num)
assert.NoError(t, err)
assert.NotNil(t, anArticle)
}
func TestStoreArticle(t *testing.T) {
now := time.Now()
ar := &domain.Article{
Title: "Judul",
Content: "Content",
CreatedAt: now,
UpdatedAt: now,
Author: domain.Author{
ID: 1,
Name: "Iman Tumorang",
},
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
query := "INSERT article SET title=\\? , content=\\? , author_id=\\?, updated_at=\\? , created_at=\\?"
prep := mock.ExpectPrepare(query)
prep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.CreatedAt, ar.UpdatedAt).WillReturnResult(sqlmock.NewResult(12, 1))
a := articleMysqlRepo.NewArticleRepository(db)
err = a.Store(context.TODO(), ar)
assert.NoError(t, err)
assert.Equal(t, int64(12), ar.ID)
}
func TestGetArticleByTitle(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
rows := sqlmock.NewRows([]string{"id", "title", "content", "author_id", "updated_at", "created_at"}).
AddRow(1, "title 1", "Content 1", 1, time.Now(), time.Now())
query := "SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE title = \\?"
mock.ExpectQuery(query).WillReturnRows(rows)
a := articleMysqlRepo.NewArticleRepository(db)
title := "title 1"
anArticle, err := a.GetByTitle(context.TODO(), title)
assert.NoError(t, err)
assert.NotNil(t, anArticle)
}
func TestDeleteArticle(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
query := "DELETE FROM article WHERE id = \\?"
prep := mock.ExpectPrepare(query)
prep.ExpectExec().WithArgs(12).WillReturnResult(sqlmock.NewResult(12, 1))
a := articleMysqlRepo.NewArticleRepository(db)
num := int64(12)
err = a.Delete(context.TODO(), num)
assert.NoError(t, err)
}
func TestUpdateArticle(t *testing.T) {
now := time.Now()
ar := &domain.Article{
ID: 12,
Title: "Judul",
Content: "Content",
CreatedAt: now,
UpdatedAt: now,
Author: domain.Author{
ID: 1,
Name: "Iman Tumorang",
},
}
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
query := "UPDATE article set title=\\?, content=\\?, author_id=\\?, updated_at=\\? WHERE ID = \\?"
prep := mock.ExpectPrepare(query)
prep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID).WillReturnResult(sqlmock.NewResult(12, 1))
a := articleMysqlRepo.NewArticleRepository(db)
err = a.Update(context.TODO(), ar)
assert.NoError(t, err)
}
================================================
FILE: internal/repository/mysql/author.go
================================================
package mysql
import (
"context"
"database/sql"
"github.com/bxcodec/go-clean-arch/domain"
)
type AuthorRepository struct {
DB *sql.DB
}
// NewMysqlAuthorRepository will create an implementation of author.Repository
func NewAuthorRepository(db *sql.DB) *AuthorRepository {
return &AuthorRepository{
DB: db,
}
}
func (m *AuthorRepository) 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...)
res = domain.Author{}
err = row.Scan(
&res.ID,
&res.Name,
&res.CreatedAt,
&res.UpdatedAt,
)
return
}
func (m *AuthorRepository) 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)
}
================================================
FILE: internal/repository/mysql/author_test.go
================================================
package mysql_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
repository "github.com/bxcodec/go-clean-arch/internal/repository/mysql"
)
func TestGetAuthorByID(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
rows := sqlmock.NewRows([]string{"id", "name", "updated_at", "created_at"}).
AddRow(1, "Iman Tumorang", time.Now(), time.Now())
query := "SELECT id, name, created_at, updated_at FROM author WHERE id=\\?"
prep := mock.ExpectPrepare(query)
userID := int64(1)
prep.ExpectQuery().WithArgs(userID).WillReturnRows(rows)
a := repository.NewAuthorRepository(db)
anArticle, err := a.GetByID(context.TODO(), userID)
assert.NoError(t, err)
assert.NotNil(t, anArticle)
}
================================================
FILE: internal/rest/article.go
================================================
package rest
import (
"context"
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
validator "gopkg.in/go-playground/validator.v9"
"github.com/bxcodec/go-clean-arch/domain"
)
// ResponseError represent the response error struct
type ResponseError struct {
Message string `json:"message"`
}
// ArticleService represent the article's usecases
//
//go:generate mockery --name ArticleService
type ArticleService interface {
Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error)
GetByID(ctx context.Context, id int64) (domain.Article, error)
Update(ctx context.Context, ar *domain.Article) error
GetByTitle(ctx context.Context, title string) (domain.Article, error)
Store(context.Context, *domain.Article) error
Delete(ctx context.Context, id int64) error
}
// ArticleHandler represent the httphandler for article
type ArticleHandler struct {
Service ArticleService
}
const defaultNum = 10
// NewArticleHandler will initialize the articles/ resources endpoint
func NewArticleHandler(e *echo.Echo, svc ArticleService) {
handler := &ArticleHandler{
Service: svc,
}
e.GET("/articles", handler.FetchArticle)
e.POST("/articles", handler.Store)
e.GET("/articles/:id", handler.GetByID)
e.DELETE("/articles/:id", handler.Delete)
}
// FetchArticle will fetch the article based on given params
func (a *ArticleHandler) FetchArticle(c echo.Context) error {
numS := c.QueryParam("num")
num, err := strconv.Atoi(numS)
if err != nil || num == 0 {
num = defaultNum
}
cursor := c.QueryParam("cursor")
ctx := c.Request().Context()
listAr, nextCursor, err := a.Service.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)
}
// GetByID will get article by given id
func (a *ArticleHandler) GetByID(c echo.Context) error {
idP, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())
}
id := int64(idP)
ctx := c.Request().Context()
art, err := a.Service.GetByID(ctx, id)
if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}
return c.JSON(http.StatusOK, art)
}
func isRequestValid(m *domain.Article) (bool, error) {
validate := validator.New()
err := validate.Struct(m)
if err != nil {
return false, err
}
return true, nil
}
// Store will store the article by given request body
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())
}
var ok bool
if ok, err = isRequestValid(&article); !ok {
return c.JSON(http.StatusBadRequest, err.Error())
}
ctx := c.Request().Context()
err = a.Service.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, domain.ErrNotFound.Error())
}
id := int64(idP)
ctx := c.Request().Context()
err = a.Service.Delete(ctx, id)
if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
func getStatusCode(err error) int {
if err == nil {
return http.StatusOK
}
logrus.Error(err)
switch err {
case domain.ErrInternalServerError:
return http.StatusInternalServerError
case domain.ErrNotFound:
return http.StatusNotFound
case domain.ErrConflict:
return http.StatusConflict
default:
return http.StatusInternalServerError
}
}
================================================
FILE: internal/rest/article_test.go
================================================
package rest_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
faker "github.com/go-faker/faker/v4"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/bxcodec/go-clean-arch/domain"
"github.com/bxcodec/go-clean-arch/internal/rest"
"github.com/bxcodec/go-clean-arch/internal/rest/mocks"
)
func TestFetch(t *testing.T) {
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)
mockUCase := new(mocks.ArticleService)
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)
e := echo.New()
req, err := http.NewRequestWithContext(context.TODO(),
echo.GET, "/article?num=1&cursor="+cursor, strings.NewReader(""))
assert.NoError(t, err)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := rest.ArticleHandler{
Service: mockUCase,
}
err = handler.FetchArticle(c)
require.NoError(t, err)
responseCursor := rec.Header().Get("X-Cursor")
assert.Equal(t, "10", responseCursor)
assert.Equal(t, http.StatusOK, rec.Code)
mockUCase.AssertExpectations(t)
}
func TestFetchError(t *testing.T) {
mockUCase := new(mocks.ArticleService)
num := 1
cursor := "2"
mockUCase.On("Fetch", mock.Anything, cursor, int64(num)).Return(nil, "", domain.ErrInternalServerError)
e := echo.New()
req, err := http.NewRequestWithContext(context.TODO(), echo.GET, "/article?num=1&cursor="+cursor, strings.NewReader(""))
assert.NoError(t, err)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := rest.ArticleHandler{
Service: mockUCase,
}
err = handler.FetchArticle(c)
require.NoError(t, err)
responseCursor := rec.Header().Get("X-Cursor")
assert.Equal(t, "", responseCursor)
assert.Equal(t, http.StatusInternalServerError, rec.Code)
mockUCase.AssertExpectations(t)
}
func TestGetByID(t *testing.T) {
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)
mockUCase := new(mocks.ArticleService)
num := int(mockArticle.ID)
mockUCase.On("GetByID", mock.Anything, int64(num)).Return(mockArticle, nil)
e := echo.New()
req, err := http.NewRequestWithContext(context.TODO(), echo.GET, "/article/"+strconv.Itoa(num), strings.NewReader(""))
assert.NoError(t, err)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("article/:id")
c.SetParamNames("id")
c.SetParamValues(strconv.Itoa(num))
handler := rest.ArticleHandler{
Service: mockUCase,
}
err = handler.GetByID(c)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
mockUCase.AssertExpectations(t)
}
func TestStore(t *testing.T) {
mockArticle := domain.Article{
Title: "Title",
Content: "Content",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tempMockArticle := mockArticle
tempMockArticle.ID = 0
mockUCase := new(mocks.ArticleService)
j, err := json.Marshal(tempMockArticle)
assert.NoError(t, err)
mockUCase.On("Store", mock.Anything, mock.AnythingOfType("*domain.Article")).Return(nil)
e := echo.New()
req, err := http.NewRequestWithContext(context.TODO(), echo.POST, "/article", strings.NewReader(string(j)))
assert.NoError(t, err)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/article")
handler := rest.ArticleHandler{
Service: mockUCase,
}
err = handler.Store(c)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)
mockUCase.AssertExpectations(t)
}
func TestDelete(t *testing.T) {
var mockArticle domain.Article
err := faker.FakeData(&mockArticle)
assert.NoError(t, err)
mockUCase := new(mocks.ArticleService)
num := int(mockArticle.ID)
mockUCase.On("Delete", mock.Anything, int64(num)).Return(nil)
e := echo.New()
req, err := http.NewRequestWithContext(context.TODO(), echo.DELETE, "/article/"+strconv.Itoa(num), strings.NewReader(""))
assert.NoError(t, err)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("article/:id")
c.SetParamNames("id")
c.SetParamValues(strconv.Itoa(num))
handler := rest.ArticleHandler{
Service: mockUCase,
}
err = handler.Delete(c)
require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, rec.Code)
mockUCase.AssertExpectations(t)
}
================================================
FILE: internal/rest/middleware/cors.go
================================================
package middleware
import "github.com/labstack/echo/v4"
// CORS will handle the CORS middleware
func CORS(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
return next(c)
}
}
================================================
FILE: internal/rest/middleware/cors_test.go
================================================
package middleware_test
import (
"net/http"
test "net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/bxcodec/go-clean-arch/internal/rest/middleware"
)
func TestCORS(t *testing.T) {
e := echo.New()
req := test.NewRequest(echo.GET, "/", nil)
res := test.NewRecorder()
c := e.NewContext(req, res)
h := middleware.CORS(echo.HandlerFunc(func(c echo.Context) error {
return c.NoContent(http.StatusOK)
}))
err := h(c)
require.NoError(t, err)
assert.Equal(t, "*", res.Header().Get("Access-Control-Allow-Origin"))
}
================================================
FILE: internal/rest/middleware/timeout.go
================================================
package middleware
import (
"context"
"time"
echo "github.com/labstack/echo/v4"
)
// SetRequestContextWithTimeout will set the request context with timeout for every incoming HTTP Request
func SetRequestContextWithTimeout(d time.Duration) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), d)
defer cancel()
newRequest := c.Request().WithContext(ctx)
c.SetRequest(newRequest)
return next(c)
}
}
}
================================================
FILE: internal/rest/mocks/ArticleService.go
================================================
// Code generated by mockery v2.42.0. DO NOT EDIT.
package mocks
import (
context "context"
domain "github.com/bxcodec/go-clean-arch/domain"
mock "github.com/stretchr/testify/mock"
)
// ArticleService is an autogenerated mock type for the ArticleService type
type ArticleService struct {
mock.Mock
}
// Delete provides a mock function with given fields: ctx, id
func (_m *ArticleService) Delete(ctx context.Context, id int64) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
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 *ArticleService) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) {
ret := _m.Called(ctx, cursor, num)
if len(ret) == 0 {
panic("no return value specified for Fetch")
}
var r0 []domain.Article
var r1 string
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]domain.Article, string, error)); ok {
return rf(ctx, cursor, num)
}
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)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok {
r1 = rf(ctx, cursor, num)
} else {
r1 = ret.Get(1).(string)
}
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 *ArticleService) GetByID(ctx context.Context, id int64) (domain.Article, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetByID")
}
var r0 domain.Article
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Article, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(domain.Article)
}
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 *ArticleService) GetByTitle(ctx context.Context, title string) (domain.Article, error) {
ret := _m.Called(ctx, title)
if len(ret) == 0 {
panic("no return value specified for GetByTitle")
}
var r0 domain.Article
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Article, error)); ok {
return rf(ctx, title)
}
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok {
r0 = rf(ctx, title)
} else {
r0 = ret.Get(0).(domain.Article)
}
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 *ArticleService) Store(_a0 context.Context, _a1 *domain.Article) error {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for Store")
}
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 *ArticleService) Update(ctx context.Context, ar *domain.Article) error {
ret := _m.Called(ctx, ar)
if len(ret) == 0 {
panic("no return value specified for Update")
}
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
}
// NewArticleService creates a new instance of ArticleService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewArticleService(t interface {
mock.TestingT
Cleanup(func())
}) *ArticleService {
mock := &ArticleService{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
================================================
FILE: internal/workers/.gitkeep
================================================
================================================
FILE: internal/workers/README.md
================================================
If need queue workers/consumer can put it under this directory. See how repository looks like for reference.
================================================
FILE: misc/make/help.Makefile
================================================
dep-gawk:
@ if [ -z "$(shell command -v gawk)" ]; then \
if [ -x /usr/local/bin/brew ]; then $(MAKE) _brew_gawk_install; exit 0; fi; \
if [ -x /usr/bin/apt-get ]; then $(MAKE) _ubuntu_gawk_install; exit 0; fi; \
if [ -x /usr/bin/yum ]; then $(MAKE) _centos_gawk_install; exit 0; fi; \
if [ -x /sbin/apk ]; then $(MAKE) _alpine_gawk_install; exit 0; fi; \
echo "GNU Awk Required, We cannot determine your OS or Package manager. Please install it yourself.";\
exit 1; \
fi
_brew_gawk_install:
@ echo "Instaling gawk using brew... "
@ brew install gawk --quiet
@ echo "done"
_ubuntu_gawk_install:
@ echo "Instaling gawk using apt-get... "
@ apt-get -q install gawk -y
@ echo "done"
_alpine_gawk_install:
@ echo "Instaling gawk using yum... "
@ apk add --update --no-cache gawk
@ echo "done"
_centos_gawk_install:
@ echo "Instaling gawk using yum... "
@ yum install -q -y gawk;
@ echo "done"
help: dep-gawk
@cat $(MAKEFILE_LIST) | \
grep -E '^# ~~~ .*? [~]+$$|^[a-zA-Z0-9_-]+:.*?## .*$$' | \
awk '{if ( $$1=="#" ) { \
match($$0, /^# ~~~ (.+?) [~]+$$/, a);\
{print "\n", a[1], ""}\
} else { \
match($$0, /^([0-9a-zA-Z_-]+):.*?## (.*)$$/, a); \
{printf " - \033[32m%-20s\033[0m %s\n", a[1], a[2]} \
}}'
@echo ""
================================================
FILE: misc/make/tools.Makefile
================================================
# This makefile should be used to hold functions/variables
ifeq ($(ARCH),x86_64)
ARCH := amd64
else ifeq ($(ARCH),aarch64)
ARCH := arm64
endif
define github_url
https://github.com/$(GITHUB)/releases/download/v$(VERSION)/$(ARCHIVE)
endef
# creates a directory bin.
bin:
@ mkdir -p $@
# ~~~ Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ~~ [migrate] ~~~ https://github.com/golang-migrate/migrate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MIGRATE := $(shell command -v migrate || echo "bin/migrate")
migrate: bin/migrate ## Install migrate (database migration)
bin/migrate: VERSION := 4.17.0
bin/migrate: GITHUB := golang-migrate/migrate
bin/migrate: ARCHIVE := migrate.$(OSTYPE)-$(ARCH).tar.gz
bin/migrate: bin
@ printf "Install migrate... "
@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - ./migrate > $@ && chmod +x $@
@ echo "done."
# ~~ [ air ] ~~~ https://github.com/cosmtrek/air ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
AIR := $(shell command -v air || echo "bin/air")
air: bin/air ## Installs air (go file watcher)
bin/air: VERSION := 1.49.0
bin/air: GITHUB := cosmtrek/air
bin/air: ARCHIVE := air_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz
bin/air: bin
@ printf "Install air... "
@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - air > $@ && chmod +x $@
@ echo "done."
# ~~ [ gotestsum ] ~~~ https://github.com/gotestyourself/gotestsum ~~~~~~~~~~~~~~~~~~~~~~~
GOTESTSUM := $(shell command -v gotestsum || echo "bin/gotestsum")
gotestsum: bin/gotestsum ## Installs gotestsum (testing go code)
bin/gotestsum: VERSION := 1.11.0
bin/gotestsum: GITHUB := gotestyourself/gotestsum
bin/gotestsum: ARCHIVE := gotestsum_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz
bin/gotestsum: bin
@ printf "Install gotestsum... "
@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - gotestsum > $@ && chmod +x $@
@ echo "done."
# ~~ [ tparse ] ~~~ https://github.com/mfridman/tparse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TPARSE := $(shell command -v tparse || echo "bin/tparse")
tparse: bin/tparse ## Installs tparse (testing go code)
# eg https://github.com/mfridman/tparse/releases/download/v0.13.2/tparse_darwin_arm64
bin/tparse: VERSION := 0.13.2
bin/tparse: GITHUB := mfridman/tparse
bin/tparse: ARCHIVE := tparse_$(OSTYPE)_$(ARCH)
bin/tparse: bin
@ printf "Install tparse... "
@ curl -Ls $(call github_url) > $@ && chmod +x $@
@ echo "done."
# ~~ [ mockery ] ~~~ https://github.com/vektra/mockery ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MOCKERY := $(shell command -v mockery || echo "bin/mockery")
mockery: bin/mockery ## Installs mockery (mocks generation)
bin/mockery: VERSION := 2.42.0
bin/mockery: GITHUB := vektra/mockery
bin/mockery: ARCHIVE := mockery_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz
bin/mockery: bin
@ printf "Install mockery... "
@ curl -Ls $(call github_url) | tar -zOxf - mockery > $@ && chmod +x $@
@ echo "done."
# ~~ [ golangci-lint ] ~~~ https://github.com/golangci/golangci-lint ~~~~~~~~~~~~~~~~~~~~~
GOLANGCI := $(shell command -v golangci-lint || echo "bin/golangci-lint")
golangci-lint: bin/golangci-lint ## Installs golangci-lint (linter)
bin/golangci-lint: VERSION := 1.56.2
bin/golangci-lint: GITHUB := golangci/golangci-lint
bin/golangci-lint: ARCHIVE := golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH).tar.gz
bin/golangci-lint: bin
@ printf "Install golangci-linter... "
@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - $(shell printf golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH)/golangci-lint | tr A-Z a-z ) > $@ && chmod +x $@
@ echo "done."
gitextract_qu10t1uh/
├── .air.toml
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ └── gotest.yml
├── .gitignore
├── .golangci.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app/
│ └── main.go
├── article/
│ ├── mocks/
│ │ ├── ArticleRepository.go
│ │ └── AuthorRepository.go
│ ├── service.go
│ └── service_test.go
├── article.sql
├── compose.yaml
├── domain/
│ ├── article.go
│ ├── author.go
│ └── errors.go
├── example.env
├── go.mod
├── go.sum
├── internal/
│ ├── README.md
│ ├── repository/
│ │ ├── helper.go
│ │ └── mysql/
│ │ ├── article.go
│ │ ├── article_test.go
│ │ ├── author.go
│ │ └── author_test.go
│ ├── rest/
│ │ ├── article.go
│ │ ├── article_test.go
│ │ ├── middleware/
│ │ │ ├── cors.go
│ │ │ ├── cors_test.go
│ │ │ └── timeout.go
│ │ └── mocks/
│ │ └── ArticleService.go
│ └── workers/
│ ├── .gitkeep
│ └── README.md
└── misc/
└── make/
├── help.Makefile
└── tools.Makefile
SYMBOL INDEX (87 symbols across 19 files)
FILE: app/main.go
constant defaultTimeout (line 24) | defaultTimeout = 30
constant defaultAddress (line 25) | defaultAddress = ":9090"
function init (line 28) | func init() {
function main (line 35) | func main() {
FILE: article.sql
type `article` (line 27) | CREATE TABLE `article` (
type `article_category` (line 55) | CREATE TABLE `article_category` (
type `author` (line 81) | CREATE TABLE `author` (
type `category` (line 107) | CREATE TABLE `category` (
FILE: article/mocks/ArticleRepository.go
type ArticleRepository (line 13) | type ArticleRepository struct
method Delete (line 18) | func (_m *ArticleRepository) Delete(ctx context.Context, id int64) err...
method Fetch (line 36) | func (_m *ArticleRepository) Fetch(ctx context.Context, cursor string,...
method GetByID (line 73) | func (_m *ArticleRepository) GetByID(ctx context.Context, id int64) (d...
method GetByTitle (line 101) | func (_m *ArticleRepository) GetByTitle(ctx context.Context, title str...
method Store (line 129) | func (_m *ArticleRepository) Store(ctx context.Context, a *domain.Arti...
method Update (line 147) | func (_m *ArticleRepository) Update(ctx context.Context, ar *domain.Ar...
function NewArticleRepository (line 166) | func NewArticleRepository(t interface {
FILE: article/mocks/AuthorRepository.go
type AuthorRepository (line 13) | type AuthorRepository struct
method GetByID (line 18) | func (_m *AuthorRepository) GetByID(ctx context.Context, id int64) (do...
function NewAuthorRepository (line 47) | func NewAuthorRepository(t interface {
FILE: article/service.go
type ArticleRepository (line 16) | type ArticleRepository interface
type AuthorRepository (line 28) | type AuthorRepository interface
type Service (line 32) | type Service struct
method fillAuthorDetails (line 50) | func (a *Service) fillAuthorDetails(ctx context.Context, data []domain...
method Fetch (line 101) | func (a *Service) Fetch(ctx context.Context, cursor string, num int64)...
method GetByID (line 114) | func (a *Service) GetByID(ctx context.Context, id int64) (res domain.A...
method Update (line 128) | func (a *Service) Update(ctx context.Context, ar *domain.Article) (err...
method GetByTitle (line 133) | func (a *Service) GetByTitle(ctx context.Context, title string) (res d...
method Store (line 148) | func (a *Service) Store(ctx context.Context, m *domain.Article) (err e...
method Delete (line 158) | func (a *Service) Delete(ctx context.Context, id int64) (err error) {
function NewService (line 38) | func NewService(a ArticleRepository, ar AuthorRepository) *Service {
FILE: article/service_test.go
function TestFetchArticle (line 16) | func TestFetchArticle(t *testing.T) {
function TestGetByID (line 67) | func TestGetByID(t *testing.T) {
function TestStore (line 108) | func TestStore(t *testing.T) {
function TestDelete (line 150) | func TestDelete(t *testing.T) {
function TestUpdate (line 197) | func TestUpdate(t *testing.T) {
FILE: domain/article.go
type Article (line 8) | type Article struct
FILE: domain/author.go
type Author (line 4) | type Author struct
FILE: internal/repository/helper.go
constant timeFormat (line 9) | timeFormat = "2006-01-02T15:04:05.999Z07:00"
function DecodeCursor (line 13) | func DecodeCursor(encodedTime string) (time.Time, error) {
function EncodeCursor (line 26) | func EncodeCursor(t time.Time) string {
FILE: internal/repository/mysql/article.go
type ArticleRepository (line 14) | type ArticleRepository struct
method fetch (line 23) | func (m *ArticleRepository) fetch(ctx context.Context, query string, a...
method Fetch (line 63) | func (m *ArticleRepository) Fetch(ctx context.Context, cursor string, ...
method GetByID (line 83) | func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (re...
method GetByTitle (line 101) | func (m *ArticleRepository) GetByTitle(ctx context.Context, title stri...
method Store (line 118) | func (m *ArticleRepository) Store(ctx context.Context, a *domain.Artic...
method Delete (line 137) | func (m *ArticleRepository) Delete(ctx context.Context, id int64) (err...
method Update (line 162) | func (m *ArticleRepository) Update(ctx context.Context, ar *domain.Art...
function NewArticleRepository (line 19) | func NewArticleRepository(conn *sql.DB) *ArticleRepository {
FILE: internal/repository/mysql/article_test.go
function TestFetchArticle (line 16) | func TestFetchArticle(t *testing.T) {
function TestGetArticleByID (line 51) | func TestGetArticleByID(t *testing.T) {
function TestStoreArticle (line 71) | func TestStoreArticle(t *testing.T) {
function TestGetArticleByTitle (line 99) | func TestGetArticleByTitle(t *testing.T) {
function TestDeleteArticle (line 119) | func TestDeleteArticle(t *testing.T) {
function TestUpdateArticle (line 137) | func TestUpdateArticle(t *testing.T) {
FILE: internal/repository/mysql/author.go
type AuthorRepository (line 10) | type AuthorRepository struct
method getOne (line 21) | func (m *AuthorRepository) getOne(ctx context.Context, query string, a...
method GetByID (line 38) | func (m *AuthorRepository) GetByID(ctx context.Context, id int64) (dom...
function NewAuthorRepository (line 15) | func NewAuthorRepository(db *sql.DB) *AuthorRepository {
FILE: internal/repository/mysql/author_test.go
function TestGetAuthorByID (line 14) | func TestGetAuthorByID(t *testing.T) {
FILE: internal/rest/article.go
type ResponseError (line 16) | type ResponseError struct
type ArticleService (line 23) | type ArticleService interface
type ArticleHandler (line 33) | type ArticleHandler struct
method FetchArticle (line 51) | func (a *ArticleHandler) FetchArticle(c echo.Context) error {
method GetByID (line 72) | func (a *ArticleHandler) GetByID(c echo.Context) error {
method Store (line 99) | func (a *ArticleHandler) Store(c echo.Context) (err error) {
method Delete (line 121) | func (a *ArticleHandler) Delete(c echo.Context) error {
constant defaultNum (line 37) | defaultNum = 10
function NewArticleHandler (line 40) | func NewArticleHandler(e *echo.Echo, svc ArticleService) {
function isRequestValid (line 89) | func isRequestValid(m *domain.Article) (bool, error) {
function getStatusCode (line 138) | func getStatusCode(err error) int {
FILE: internal/rest/article_test.go
function TestFetch (line 24) | func TestFetch(t *testing.T) {
function TestFetchError (line 54) | func TestFetchError(t *testing.T) {
function TestGetByID (line 78) | func TestGetByID(t *testing.T) {
function TestStore (line 108) | func TestStore(t *testing.T) {
function TestDelete (line 144) | func TestDelete(t *testing.T) {
FILE: internal/rest/middleware/cors.go
function CORS (line 6) | func CORS(next echo.HandlerFunc) echo.HandlerFunc {
FILE: internal/rest/middleware/cors_test.go
function TestCORS (line 15) | func TestCORS(t *testing.T) {
FILE: internal/rest/middleware/timeout.go
function SetRequestContextWithTimeout (line 11) | func SetRequestContextWithTimeout(d time.Duration) echo.MiddlewareFunc {
FILE: internal/rest/mocks/ArticleService.go
type ArticleService (line 13) | type ArticleService struct
method Delete (line 18) | func (_m *ArticleService) Delete(ctx context.Context, id int64) error {
method Fetch (line 36) | func (_m *ArticleService) Fetch(ctx context.Context, cursor string, nu...
method GetByID (line 73) | func (_m *ArticleService) GetByID(ctx context.Context, id int64) (doma...
method GetByTitle (line 101) | func (_m *ArticleService) GetByTitle(ctx context.Context, title string...
method Store (line 129) | func (_m *ArticleService) Store(_a0 context.Context, _a1 *domain.Artic...
method Update (line 147) | func (_m *ArticleService) Update(ctx context.Context, ar *domain.Artic...
function NewArticleService (line 166) | func NewArticleService(t interface {
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
{
"path": ".air.toml",
"chars": 1069,
"preview": "# Config file for [Air](https://github.com/cosmtrek/air) in TOML format\n\n# Working directory\n# . or absolute path, pleas"
},
{
"path": ".dockerignore",
"chars": 13,
"preview": "engine\n*.out\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 18,
"preview": "github: [bxcodec]\n"
},
{
"path": ".github/dependabot.yml",
"chars": 527,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/gotest.yml",
"chars": 390,
"preview": "name: Go Test\n\non:\n push:\n branches: [\"main\", \"chore/upgrade-linter\"]\n pull_request:\n branches: [\"main\"]\n\njobs:\n"
},
{
"path": ".gitignore",
"chars": 74,
"preview": "\nvendor/\narticle_clean\n_*\n*.test\n.DS_Store\nengine\nbin/\n*.out\ntmp_app/\n.env"
},
{
"path": ".golangci.yaml",
"chars": 1377,
"preview": "linters-settings:\n govet:\n check-shadowing: true\n settings:\n printf:\n funcs:\n - (github.com/"
},
{
"path": "Dockerfile",
"chars": 387,
"preview": "# Builder\nFROM golang:1.20.7-alpine3.17 as builder\n\nRUN apk update && apk upgrade && \\\n apk --update add git make bas"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2019 Iman Tumorang\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "Makefile",
"chars": 4706,
"preview": "# Database\nMYSQL_USER ?= user\nMYSQL_PASSWORD ?= password\nMYSQL_ADDRESS ?= 127.0.0.1:3306\nMYSQL_DATABASE ?= article\n\n# Ex"
},
{
"path": "README.md",
"chars": 4574,
"preview": "# go-clean-arch\n\n## Changelog\n\n- **v1**: checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1) <"
},
{
"path": "app/main.go",
"chars": 2122,
"preview": "package main\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t_ \"github.com/go-sql-driver/m"
},
{
"path": "article/mocks/ArticleRepository.go",
"chars": 4350,
"preview": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxco"
},
{
"path": "article/mocks/AuthorRepository.go",
"chars": 1436,
"preview": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxco"
},
{
"path": "article/service.go",
"chars": 4095,
"preview": "package article\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/"
},
{
"path": "article/service_test.go",
"chars": 6736,
"preview": "package article_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stret"
},
{
"path": "article.sql",
"chars": 15590,
"preview": "CREATE DATABASE IF NOT EXISTS `article` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;\nUSE `article`;\n"
},
{
"path": "compose.yaml",
"chars": 700,
"preview": "version: \"3.7\"\nservices:\n web:\n image: go-clean-arch\n container_name: article_management_api\n ports:\n - 9"
},
{
"path": "domain/article.go",
"chars": 377,
"preview": "package domain\n\nimport (\n\t\"time\"\n)\n\n// Article is representing the Article data struct\ntype Article struct {\n\tID "
},
{
"path": "domain/author.go",
"chars": 223,
"preview": "package domain\n\n// Author representing the Author data struct\ntype Author struct {\n\tID int64 `json:\"id\"`\n\tName "
},
{
"path": "domain/errors.go",
"chars": 563,
"preview": "package domain\n\nimport \"errors\"\n\nvar (\n\t// ErrInternalServerError will throw if any the Internal Server Error happen\n\tEr"
},
{
"path": "example.env",
"chars": 184,
"preview": "DEBUG = True\nSERVER_ADDRESS = \":9090\"\nCONTEXT_TIMEOUT = 2\nDATABASE_HOST = \"localhost\"\nDATABASE_PORT = \"3306\"\nDATABASE_US"
},
{
"path": "go.mod",
"chars": 1377,
"preview": "module github.com/bxcodec/go-clean-arch\n\ngo 1.20\n\nrequire (\n\tgithub.com/go-faker/faker/v4 v4.3.0\n\tgithub.com/go-sql-driv"
},
{
"path": "go.sum",
"chars": 6551,
"preview": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go"
},
{
"path": "internal/README.md",
"chars": 1122,
"preview": "# Internal Directory Guidelines\n\nThis directory is designated for internal processes. Golang operates on a package syste"
},
{
"path": "internal/repository/helper.go",
"chars": 674,
"preview": "package repository\n\nimport (\n\t\"encoding/base64\"\n\t\"time\"\n)\n\nconst (\n\ttimeFormat = \"2006-01-02T15:04:05.999Z07:00\" // redu"
},
{
"path": "internal/repository/mysql/article.go",
"chars": 4022,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/bxcodec/go-clean-"
},
{
"path": "internal/repository/mysql/article_test.go",
"chars": 5096,
"preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tsqlmock \"gopkg.in/DAT"
},
{
"path": "internal/repository/mysql/author.go",
"chars": 901,
"preview": "package mysql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/bxcodec/go-clean-arch/domain\"\n)\n\ntype AuthorRepository "
},
{
"path": "internal/repository/mysql/author_test.go",
"chars": 874,
"preview": "package mysql_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tsqlmock \"gopkg.in/DAT"
},
{
"path": "internal/rest/article.go",
"chars": 3864,
"preview": "package rest\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/sirupsen/logrus\"\n\t"
},
{
"path": "internal/rest/article_test.go",
"chars": 4527,
"preview": "package rest_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing"
},
{
"path": "internal/rest/middleware/cors.go",
"chars": 273,
"preview": "package middleware\n\nimport \"github.com/labstack/echo/v4\"\n\n// CORS will handle the CORS middleware\nfunc CORS(next echo.Ha"
},
{
"path": "internal/rest/middleware/cors_test.go",
"chars": 626,
"preview": "package middleware_test\n\nimport (\n\t\"net/http\"\n\ttest \"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"git"
},
{
"path": "internal/rest/middleware/timeout.go",
"chars": 544,
"preview": "package middleware\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\techo \"github.com/labstack/echo/v4\"\n)\n\n// SetRequestContextWithTimeout "
},
{
"path": "internal/rest/mocks/ArticleService.go",
"chars": 4316,
"preview": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxco"
},
{
"path": "internal/workers/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "internal/workers/README.md",
"chars": 109,
"preview": "If need queue workers/consumer can put it under this directory. See how repository looks like for reference.\n"
},
{
"path": "misc/make/help.Makefile",
"chars": 1266,
"preview": "dep-gawk:\n\t@ if [ -z \"$(shell command -v gawk)\" ]; then \\\n\t\tif [ -x /usr/local/bin/brew ]; then $(MAKE) _brew_gawk_inst"
},
{
"path": "misc/make/tools.Makefile",
"chars": 3603,
"preview": "# This makefile should be used to hold functions/variables\n\nifeq ($(ARCH),x86_64)\n\tARCH := amd64\nelse ifeq ($(ARCH),aarc"
}
]
About this extraction
This page contains the full source code of the bxcodec/go-clean-arch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (88.2 KB), approximately 27.5k tokens, and a symbol index with 87 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.