[
  {
    "path": ".air.toml",
    "content": "# Config file for [Air](https://github.com/cosmtrek/air) in TOML format\n\n# Working directory\n# . or absolute path, please note that the directories following must be under root\nroot = \".\"\ntmp_dir = \"tmp_app\"\n\n[build]\n# Just plain old shell command. You could use `make` as well.\ncmd = \"go build -o ./tmp_app/app/engine ./app/.\"\n# Binary file yields from `cmd`.\nbin = \"tmp_app/app\"\n# Customize binary.\nfull_bin = \"./tmp_app/app/engine\"\n# This log file places in your tmp_dir.\nlog = \"air_errors.log\"\n# Watch these filename extensions.\ninclude_ext = [\"go\", \"yaml\", \"toml\"]\n# Exclude specific regular expressions.\nexclude_regex = [\"_test\\\\.go\"]\n# Ignore these filename extensions or directories.\nexclude_dir = [\"tmp_app\", \"tmp\"]\n# It's not necessary to trigger build each time file changes if it's too frequent.\ndelay = 1000 # ms\n\n[log]\n# Show log time\ntime = true\n\n[color]\n# Customize each part's color. If no color found, use the raw app log.\nmain = \"magenta\"\nwatcher = \"cyan\"\nbuild = \"yellow\"\nrunner = \"green\"\n\n[misc]\n# Delete tmp directory on exit\nclean_on_exit = true\n"
  },
  {
    "path": ".dockerignore",
    "content": "engine\n*.out\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [bxcodec]\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/gotest.yml",
    "content": "name: Go Test\n\non:\n  push:\n    branches: [\"main\", \"chore/upgrade-linter\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Go\n        uses: actions/setup-go@v3\n        with:\n          go-version: 1.19\n\n      - name: Linter\n        run: make lint\n\n      - name: Test\n        run: make tests\n"
  },
  {
    "path": ".gitignore",
    "content": "\nvendor/\narticle_clean\n_*\n*.test\n.DS_Store\nengine\nbin/\n*.out\ntmp_app/\n.env"
  },
  {
    "path": ".golangci.yaml",
    "content": "linters-settings:\n  govet:\n    check-shadowing: true\n    settings:\n      printf:\n        funcs:\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf\n          - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf\n  gocyclo:\n    min-complexity: 50\n  maligned:\n    suggest-new: true\n  dupl:\n    threshold: 100\n  goconst:\n    min-len: 2\n    min-occurrences: 2\n  misspell:\n    locale: US\n  revive:\n    confidence: 0.8\n  lll:\n    line-length: 160\n    # tab width in spaces. Default to 1.\n    tab-width: 1\n  funlen:\n    lines: 150\n    statements: 80\n\nlinters:\n  # please, do not use `enable-all`: it's deprecated and will be removed soon.\n  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint\n  disable-all: true\n  enable:\n    - errcheck\n    - funlen\n    - goconst\n    - gocyclo\n    - gosec\n    - gosimple\n    - govet\n    - ineffassign\n    - lll\n    - misspell\n    - revive\n    - staticcheck\n    - typecheck\n    - unconvert\n    - unparam\n    - unused\n\n  # don't enable:\n  # - gochecknoglobals\n  # - gocognit\n  # - godox\n  # - maligned\n  # - prealloc\n\nrun:\n  skip-dirs:\n    # - test/testdata_etc\n  skip-files:\n    - \".*_test\\\\.go$\"\n\nissues:\n  exclude-rules:\n  #\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Builder\nFROM golang:1.20.7-alpine3.17 as builder\n\nRUN apk update && apk upgrade && \\\n    apk --update add git make bash build-base\n\nWORKDIR /app\n\nCOPY . .\n\nRUN make build\n\n# Distribution\nFROM alpine:latest\n\nRUN apk update && apk upgrade && \\\n    apk --update --no-cache add tzdata && \\\n    mkdir /app \n\nWORKDIR /app \n\nEXPOSE 9090\n\nCOPY --from=builder /app/engine /app/\n\nCMD /app/engine"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Iman Tumorang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Database\nMYSQL_USER ?= user\nMYSQL_PASSWORD ?= password\nMYSQL_ADDRESS ?= 127.0.0.1:3306\nMYSQL_DATABASE ?= article\n\n# Exporting bin folder to the path for makefile\nexport PATH   := $(PWD)/bin:$(PATH)\n# Default Shell\nexport SHELL  := bash\n# Type of OS: Linux or Darwin.\nexport OSTYPE := $(shell uname -s | tr A-Z a-z)\nexport ARCH := $(shell uname -m)\n\n\n\n# --- Tooling & Variables ----------------------------------------------------------------\ninclude ./misc/make/tools.Makefile\ninclude ./misc/make/help.Makefile\n\n# ~~~ Development Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nup: dev-env dev-air             ## Startup / Spinup Docker Compose and air\ndown: docker-stop               ## Stop Docker\ndestroy: docker-teardown clean  ## Teardown (removes volumes, tmp files, etc...)\n\ninstall-deps: migrate air gotestsum tparse mockery ## Install Development Dependencies (localy).\ndeps: $(MIGRATE) $(AIR) $(GOTESTSUM) $(TPARSE) $(MOCKERY) $(GOLANGCI) ## Checks for Global Development Dependencies.\ndeps:\n\t@echo \"Required Tools Are Available\"\n\ndev-env: ## Bootstrap Environment (with a Docker-Compose help).\n\t@ docker-compose up -d --build mysql\n\ndev-env-test: dev-env ## Run application (within a Docker-Compose help)\n\t@ $(MAKE) image-build\n\tdocker-compose up web\n\ndev-air: $(AIR) ## Starts AIR ( Continuous Development app).\n\tair\n\ndocker-stop:\n\t@ docker-compose down\n\ndocker-teardown:\n\t@ docker-compose down --remove-orphans -v\n\n# ~~~ Code Actions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nlint: $(GOLANGCI) ## Runs golangci-lint with predefined configuration\n\t@echo \"Applying linter\"\n\tgolangci-lint version\n\tgolangci-lint run -c .golangci.yaml ./...\n\n# -trimpath - will remove the filepathes from the reports, good to same money on network trafic,\n#             focus on bug reports, and find issues fast.\n# - race    - adds a racedetector, in case of racecondition, you can catch report with sentry.\n#             https://golang.org/doc/articles/race_detector.html\n#\n# todo(butuzov): add additional flags to compiler to have an `version` flag.\nbuild: ## Builds binary\n\t@ printf \"Building aplication... \"\n\t@ go build \\\n\t\t-trimpath  \\\n\t\t-o engine \\\n\t\t./app/\n\t@ echo \"done\"\n\n\nbuild-race: ## Builds binary (with -race flag)\n\t@ printf \"Building aplication with race flag... \"\n\t@ go build \\\n\t\t-trimpath  \\\n\t\t-race      \\\n\t\t-o engine \\\n\t\t./app/\n\t@ echo \"done\"\n\n\ngo-generate: $(MOCKERY) ## Runs go generte ./...\n\tgo generate ./...\n\n\nTESTS_ARGS := --format testname --jsonfile gotestsum.json.out\nTESTS_ARGS += --max-fails 2\nTESTS_ARGS += -- ./...\nTESTS_ARGS += -test.parallel 2\nTESTS_ARGS += -test.count    1\nTESTS_ARGS += -test.failfast\nTESTS_ARGS += -test.coverprofile   coverage.out\nTESTS_ARGS += -test.timeout        5s\nTESTS_ARGS += -race\n\ntests: $(GOTESTSUM)\n\t@ gotestsum $(TESTS_ARGS) -short\n\ntests-complete: tests $(TPARSE) ## Run Tests & parse details\n\t@cat gotestsum.json.out | $(TPARSE) -all -notests\n\n# ~~~ Docker Build ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n.ONESHELL:\nimage-build:\n\t@ echo \"Docker Build\"\n\t@ DOCKER_BUILDKIT=0 docker build \\\n\t\t--file Dockerfile \\\n\t\t--tag go-clean-arch \\\n\t\t\t.\n\n# Commenting this as this not relevant for the project, we load the DB data from the SQL file.\n# please refer this when introducing the database schema migrations.\n\n# # ~~~ Database Migrations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n \n\n# MYSQL_DSN := \"mysql://$(MYSQL_USER):$(MYSQL_PASSWORD)@tcp($(MYSQL_ADDRESS))/$(MYSQL_DATABASE)\"\n\n# migrate-up: $(MIGRATE) ## Apply all (or N up) migrations.\n# \t@ read -p \"How many migration you wants to perform (default value: [all]): \" N; \\\n# \tmigrate  -database $(MYSQL_DSN) -path=misc/migrations up ${NN}\n\n# .PHONY: migrate-down\n# migrate-down: $(MIGRATE) ## Apply all (or N down) migrations.\n# \t@ read -p \"How many migration you wants to perform (default value: [all]): \" N; \\\n# \tmigrate  -database $(MYSQL_DSN) -path=misc/migrations down ${NN}\n\n# .PHONY: migrate-drop\n# migrate-drop: $(MIGRATE) ## Drop everything inside the database.\n# \tmigrate  -database $(MYSQL_DSN) -path=misc/migrations drop\n\n# .PHONY: migrate-create\n# migrate-create: $(MIGRATE) ## Create a set of up/down migrations with a specified name.\n# \t@ read -p \"Please provide name for the migration: \" Name; \\\n# \tmigrate create -ext sql -dir misc/migrations $${Name}\n\n# ~~~ Cleans ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nclean: clean-artifacts clean-docker\n\nclean-artifacts: ## Removes Artifacts (*.out)\n\t@printf \"Cleanning artifacts... \"\n\t@rm -f *.out\n\t@echo \"done.\"\n\n\nclean-docker: ## Removes dangling docker images\n\t@ docker image prune -f\n"
  },
  {
    "path": "README.md",
    "content": "# go-clean-arch\n\n## Changelog\n\n- **v1**: checkout to the [v1 branch](https://github.com/bxcodec/go-clean-arch/tree/v1) <br>\n  Proposed on 2017, archived to v1 branch on 2018 <br>\n  Desc: Initial proposal by me. The story can be read here: https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047\n\n- **v2**: checkout to the [v2 branch](https://github.com/bxcodec/go-clean-arch/tree/v2) <br>\n  Proposed on 2018, archived to v2 branch on 2020 <br>\n  Desc: Improvement from v1. The story can be read here: https://medium.com/@imantumorang/trying-clean-architecture-on-golang-2-44d615bf8fdf\n\n- **v3**: checkout to the [v3 branch](https://github.com/bxcodec/go-clean-arch/tree/v3) <br>\n  Proposed on 2019, merged to master on 2020. <br>\n  Desc: Introducing Domain package, the details can be seen on this PR [#21](https://github.com/bxcodec/go-clean-arch/pull/21)\n\n- **v4**: master branch\n  Proposed on 2024, merged to master on 2024. <br>\n  Desc:\n\n  - Declare Interfaces to the consuming side,\n  - Introduce `internal` package\n  - Introduce `Service-focused` package.\n\n  Details can be seen in this PR [#88](https://github.com/bxcodec/go-clean-arch/pull/88).<br>\n\n> ### Author's Note\n>\n> 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.\n\n## Description\n\nThis is an example of implementation of Clean Architecture in Go (Golang) projects.\n\nRule of Clean Architecture by Uncle Bob\n\n- 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.\n- Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.\n- 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.\n- 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.\n- Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.\n\nMore at https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html\n\nThis project has 4 Domain layer :\n\n- Models Layer\n- Repository Layer\n- Usecase Layer\n- Delivery Layer\n\n#### The diagram:\n\n![golang clean architecture](https://github.com/bxcodec/go-clean-arch/raw/master/clean-arch.png)\n\nThe original explanation about this project's structure can read from this medium's post : https://medium.com/@imantumorang/golang-clean-archithecture-efd6d7c43047.\nIt 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.\n\n### How To Run This Project\n\n> Make Sure you have run the article.sql in your mysql\n\nSince the project is already use Go Module, I recommend to put the source code in any folder but GOPATH.\n\n#### Run the Testing\n\n```bash\n$ make tests\n```\n\n#### Run the Applications\n\nHere is the steps to run it with `docker-compose`\n\n```bash\n#move to directory\n$ cd workspace\n\n# Clone into your workspace\n$ git clone https://github.com/bxcodec/go-clean-arch.git\n\n#move to project\n$ cd go-clean-arch\n\n# copy the example.env to .env\n$ cp example.env .env\n\n# Run the application\n$ make up\n\n# The hot reload will running\n\n# Execute the call in another terminal\n$ curl localhost:9090/articles\n```\n\n### Tools Used:\n\nIn 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.\n\n- All libraries listed in [`go.mod`](https://github.com/bxcodec/go-clean-arch/blob/master/go.mod)\n- [\"github.com/vektra/mockery\".](https://github.com/vektra/mockery) To Generate Mocks for testing needs.\n"
  },
  {
    "path": "app/main.go",
    "content": "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/mysql\"\n\t\"github.com/labstack/echo/v4\"\n\n\tmysqlRepo \"github.com/bxcodec/go-clean-arch/internal/repository/mysql\"\n\n\t\"github.com/bxcodec/go-clean-arch/article\"\n\t\"github.com/bxcodec/go-clean-arch/internal/rest\"\n\t\"github.com/bxcodec/go-clean-arch/internal/rest/middleware\"\n\t\"github.com/joho/godotenv\"\n)\n\nconst (\n\tdefaultTimeout = 30\n\tdefaultAddress = \":9090\"\n)\n\nfunc init() {\n\terr := godotenv.Load()\n\tif err != nil {\n\t\tlog.Fatal(\"Error loading .env file\")\n\t}\n}\n\nfunc main() {\n\t//prepare database\n\tdbHost := os.Getenv(\"DATABASE_HOST\")\n\tdbPort := os.Getenv(\"DATABASE_PORT\")\n\tdbUser := os.Getenv(\"DATABASE_USER\")\n\tdbPass := os.Getenv(\"DATABASE_PASS\")\n\tdbName := os.Getenv(\"DATABASE_NAME\")\n\tconnection := fmt.Sprintf(\"%s:%s@tcp(%s:%s)/%s\", dbUser, dbPass, dbHost, dbPort, dbName)\n\tval := url.Values{}\n\tval.Add(\"parseTime\", \"1\")\n\tval.Add(\"loc\", \"Asia/Jakarta\")\n\tdsn := fmt.Sprintf(\"%s?%s\", connection, val.Encode())\n\tdbConn, err := sql.Open(`mysql`, dsn)\n\tif err != nil {\n\t\tlog.Fatal(\"failed to open connection to database\", err)\n\t}\n\terr = dbConn.Ping()\n\tif err != nil {\n\t\tlog.Fatal(\"failed to ping database \", err)\n\t}\n\n\tdefer func() {\n\t\terr := dbConn.Close()\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"got error when closing the DB connection\", err)\n\t\t}\n\t}()\n\t// prepare echo\n\n\te := echo.New()\n\te.Use(middleware.CORS)\n\ttimeoutStr := os.Getenv(\"CONTEXT_TIMEOUT\")\n\ttimeout, err := strconv.Atoi(timeoutStr)\n\tif err != nil {\n\t\tlog.Println(\"failed to parse timeout, using default timeout\")\n\t\ttimeout = defaultTimeout\n\t}\n\ttimeoutContext := time.Duration(timeout) * time.Second\n\te.Use(middleware.SetRequestContextWithTimeout(timeoutContext))\n\n\t// Prepare Repository\n\tauthorRepo := mysqlRepo.NewAuthorRepository(dbConn)\n\tarticleRepo := mysqlRepo.NewArticleRepository(dbConn)\n\n\t// Build service Layer\n\tsvc := article.NewService(articleRepo, authorRepo)\n\trest.NewArticleHandler(e, svc)\n\n\t// Start Server\n\taddress := os.Getenv(\"SERVER_ADDRESS\")\n\tif address == \"\" {\n\t\taddress = defaultAddress\n\t}\n\tlog.Fatal(e.Start(address)) //nolint\n}\n"
  },
  {
    "path": "article/mocks/ArticleRepository.go",
    "content": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxcodec/go-clean-arch/domain\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// ArticleRepository is an autogenerated mock type for the ArticleRepository type\ntype ArticleRepository struct {\n\tmock.Mock\n}\n\n// Delete provides a mock function with given fields: ctx, id\nfunc (_m *ArticleRepository) Delete(ctx context.Context, id int64) error {\n\tret := _m.Called(ctx, id)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Delete\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Fetch provides a mock function with given fields: ctx, cursor, num\nfunc (_m *ArticleRepository) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) {\n\tret := _m.Called(ctx, cursor, num)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Fetch\")\n\t}\n\n\tvar r0 []domain.Article\n\tvar r1 string\n\tvar r2 error\n\tif rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]domain.Article, string, error)); ok {\n\t\treturn rf(ctx, cursor, num)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, string, int64) []domain.Article); ok {\n\t\tr0 = rf(ctx, cursor, num)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]domain.Article)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok {\n\t\tr1 = rf(ctx, cursor, num)\n\t} else {\n\t\tr1 = ret.Get(1).(string)\n\t}\n\n\tif rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok {\n\t\tr2 = rf(ctx, cursor, num)\n\t} else {\n\t\tr2 = ret.Error(2)\n\t}\n\n\treturn r0, r1, r2\n}\n\n// GetByID provides a mock function with given fields: ctx, id\nfunc (_m *ArticleRepository) GetByID(ctx context.Context, id int64) (domain.Article, error) {\n\tret := _m.Called(ctx, id)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetByID\")\n\t}\n\n\tvar r0 domain.Article\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Article, error)); ok {\n\t\treturn rf(ctx, id)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Get(0).(domain.Article)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {\n\t\tr1 = rf(ctx, id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetByTitle provides a mock function with given fields: ctx, title\nfunc (_m *ArticleRepository) GetByTitle(ctx context.Context, title string) (domain.Article, error) {\n\tret := _m.Called(ctx, title)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetByTitle\")\n\t}\n\n\tvar r0 domain.Article\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(context.Context, string) (domain.Article, error)); ok {\n\t\treturn rf(ctx, title)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok {\n\t\tr0 = rf(ctx, title)\n\t} else {\n\t\tr0 = ret.Get(0).(domain.Article)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, title)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Store provides a mock function with given fields: ctx, a\nfunc (_m *ArticleRepository) Store(ctx context.Context, a *domain.Article) error {\n\tret := _m.Called(ctx, a)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Store\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok {\n\t\tr0 = rf(ctx, a)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Update provides a mock function with given fields: ctx, ar\nfunc (_m *ArticleRepository) Update(ctx context.Context, ar *domain.Article) error {\n\tret := _m.Called(ctx, ar)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Update\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok {\n\t\tr0 = rf(ctx, ar)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewArticleRepository(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *ArticleRepository {\n\tmock := &ArticleRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "article/mocks/AuthorRepository.go",
    "content": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxcodec/go-clean-arch/domain\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// AuthorRepository is an autogenerated mock type for the AuthorRepository type\ntype AuthorRepository struct {\n\tmock.Mock\n}\n\n// GetByID provides a mock function with given fields: ctx, id\nfunc (_m *AuthorRepository) GetByID(ctx context.Context, id int64) (domain.Author, error) {\n\tret := _m.Called(ctx, id)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetByID\")\n\t}\n\n\tvar r0 domain.Author\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Author, error)); ok {\n\t\treturn rf(ctx, id)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) domain.Author); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Get(0).(domain.Author)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {\n\t\tr1 = rf(ctx, id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewAuthorRepository(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *AuthorRepository {\n\tmock := &AuthorRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "article/service.go",
    "content": "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/bxcodec/go-clean-arch/domain\"\n)\n\n// ArticleRepository represent the article's repository contract\n//\n//go:generate mockery --name ArticleRepository\ntype ArticleRepository interface {\n\tFetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error)\n\tGetByID(ctx context.Context, id int64) (domain.Article, error)\n\tGetByTitle(ctx context.Context, title string) (domain.Article, error)\n\tUpdate(ctx context.Context, ar *domain.Article) error\n\tStore(ctx context.Context, a *domain.Article) error\n\tDelete(ctx context.Context, id int64) error\n}\n\n// AuthorRepository represent the author's repository contract\n//\n//go:generate mockery --name AuthorRepository\ntype AuthorRepository interface {\n\tGetByID(ctx context.Context, id int64) (domain.Author, error)\n}\n\ntype Service struct {\n\tarticleRepo ArticleRepository\n\tauthorRepo  AuthorRepository\n}\n\n// NewService will create a new article service object\nfunc NewService(a ArticleRepository, ar AuthorRepository) *Service {\n\treturn &Service{\n\t\tarticleRepo: a,\n\t\tauthorRepo:  ar,\n\t}\n}\n\n/*\n* In this function below, I'm using errgroup with the pipeline pattern\n* Look how this works in this package explanation\n* in godoc: https://godoc.org/golang.org/x/sync/errgroup#ex-Group--Pipeline\n */\nfunc (a *Service) fillAuthorDetails(ctx context.Context, data []domain.Article) ([]domain.Article, error) {\n\tg, ctx := errgroup.WithContext(ctx)\n\t// Get the author's id\n\tmapAuthors := map[int64]domain.Author{}\n\n\tfor _, article := range data { //nolint\n\t\tmapAuthors[article.Author.ID] = domain.Author{}\n\t}\n\t// Using goroutine to fetch the author's detail\n\tchanAuthor := make(chan domain.Author)\n\tfor authorID := range mapAuthors {\n\t\tauthorID := authorID\n\t\tg.Go(func() error {\n\t\t\tres, err := a.authorRepo.GetByID(ctx, authorID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tchanAuthor <- res\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tgo func() {\n\t\tdefer close(chanAuthor)\n\t\terr := g.Wait()\n\t\tif err != nil {\n\t\t\tlogrus.Error(err)\n\t\t\treturn\n\t\t}\n\n\t}()\n\n\tfor author := range chanAuthor {\n\t\tif author != (domain.Author{}) {\n\t\t\tmapAuthors[author.ID] = author\n\t\t}\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// merge the author's data\n\tfor index, item := range data { //nolint\n\t\tif a, ok := mapAuthors[item.Author.ID]; ok {\n\t\t\tdata[index].Author = a\n\t\t}\n\t}\n\treturn data, nil\n}\n\nfunc (a *Service) Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) {\n\tres, nextCursor, err = a.articleRepo.Fetch(ctx, cursor, num)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tres, err = a.fillAuthorDetails(ctx, res)\n\tif err != nil {\n\t\tnextCursor = \"\"\n\t}\n\treturn\n}\n\nfunc (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {\n\tres, err = a.articleRepo.GetByID(ctx, id)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)\n\tif err != nil {\n\t\treturn domain.Article{}, err\n\t}\n\tres.Author = resAuthor\n\treturn\n}\n\nfunc (a *Service) Update(ctx context.Context, ar *domain.Article) (err error) {\n\tar.UpdatedAt = time.Now()\n\treturn a.articleRepo.Update(ctx, ar)\n}\n\nfunc (a *Service) GetByTitle(ctx context.Context, title string) (res domain.Article, err error) {\n\tres, err = a.articleRepo.GetByTitle(ctx, title)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tresAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID)\n\tif err != nil {\n\t\treturn domain.Article{}, err\n\t}\n\n\tres.Author = resAuthor\n\treturn\n}\n\nfunc (a *Service) Store(ctx context.Context, m *domain.Article) (err error) {\n\texistedArticle, _ := a.GetByTitle(ctx, m.Title) // ignore if any error\n\tif existedArticle != (domain.Article{}) {\n\t\treturn domain.ErrConflict\n\t}\n\n\terr = a.articleRepo.Store(ctx, m)\n\treturn\n}\n\nfunc (a *Service) Delete(ctx context.Context, id int64) (err error) {\n\texistedArticle, err := a.articleRepo.GetByID(ctx, id)\n\tif err != nil {\n\t\treturn\n\t}\n\tif existedArticle == (domain.Article{}) {\n\t\treturn domain.ErrNotFound\n\t}\n\treturn a.articleRepo.Delete(ctx, id)\n}\n"
  },
  {
    "path": "article/service_test.go",
    "content": "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/stretchr/testify/mock\"\n\n\t\"github.com/bxcodec/go-clean-arch/article\"\n\t\"github.com/bxcodec/go-clean-arch/article/mocks\"\n\t\"github.com/bxcodec/go-clean-arch/domain\"\n)\n\nfunc TestFetchArticle(t *testing.T) {\n\tmockArticleRepo := new(mocks.ArticleRepository)\n\tmockArticle := domain.Article{\n\t\tTitle:   \"Hello\",\n\t\tContent: \"Content\",\n\t}\n\n\tmockListArtilce := make([]domain.Article, 0)\n\tmockListArtilce = append(mockListArtilce, mockArticle)\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"Fetch\", mock.Anything, mock.AnythingOfType(\"string\"),\n\t\t\tmock.AnythingOfType(\"int64\")).Return(mockListArtilce, \"next-cursor\", nil).Once()\n\t\tmockAuthor := domain.Author{\n\t\t\tID:   1,\n\t\t\tName: \"Iman Tumorang\",\n\t\t}\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tmockAuthorrepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(mockAuthor, nil)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\t\tnum := int64(1)\n\t\tcursor := \"12\"\n\t\tlist, nextCursor, err := u.Fetch(context.TODO(), cursor, num)\n\t\tcursorExpected := \"next-cursor\"\n\t\tassert.Equal(t, cursorExpected, nextCursor)\n\t\tassert.NotEmpty(t, nextCursor)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, list, len(mockListArtilce))\n\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n\n\tt.Run(\"error-failed\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"Fetch\", mock.Anything, mock.AnythingOfType(\"string\"),\n\t\t\tmock.AnythingOfType(\"int64\")).Return(nil, \"\", errors.New(\"Unexpexted Error\")).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\t\tnum := int64(1)\n\t\tcursor := \"12\"\n\t\tlist, nextCursor, err := u.Fetch(context.TODO(), cursor, num)\n\n\t\tassert.Empty(t, nextCursor)\n\t\tassert.Error(t, err)\n\t\tassert.Len(t, list, 0)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n}\n\nfunc TestGetByID(t *testing.T) {\n\tmockArticleRepo := new(mocks.ArticleRepository)\n\tmockArticle := domain.Article{\n\t\tTitle:   \"Hello\",\n\t\tContent: \"Content\",\n\t}\n\tmockAuthor := domain.Author{\n\t\tID:   1,\n\t\tName: \"Iman Tumorang\",\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(mockArticle, nil).Once()\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tmockAuthorrepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(mockAuthor, nil)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\ta, err := u.GetByID(context.TODO(), mockArticle.ID)\n\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, a)\n\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n\tt.Run(\"error-failed\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(domain.Article{}, errors.New(\"Unexpected\")).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\ta, err := u.GetByID(context.TODO(), mockArticle.ID)\n\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, domain.Article{}, a)\n\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n}\n\nfunc TestStore(t *testing.T) {\n\tmockArticleRepo := new(mocks.ArticleRepository)\n\tmockArticle := domain.Article{\n\t\tTitle:   \"Hello\",\n\t\tContent: \"Content\",\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\ttempMockArticle := mockArticle\n\t\ttempMockArticle.ID = 0\n\t\tmockArticleRepo.On(\"GetByTitle\", mock.Anything, mock.AnythingOfType(\"string\")).Return(domain.Article{}, domain.ErrNotFound).Once()\n\t\tmockArticleRepo.On(\"Store\", mock.Anything, mock.AnythingOfType(\"*domain.Article\")).Return(nil).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Store(context.TODO(), &tempMockArticle)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, mockArticle.Title, tempMockArticle.Title)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t})\n\tt.Run(\"existing-title\", func(t *testing.T) {\n\t\texistingArticle := mockArticle\n\t\tmockArticleRepo.On(\"GetByTitle\", mock.Anything, mock.AnythingOfType(\"string\")).Return(existingArticle, nil).Once()\n\t\tmockAuthor := domain.Author{\n\t\t\tID:   1,\n\t\t\tName: \"Iman Tumorang\",\n\t\t}\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tmockAuthorrepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(mockAuthor, nil)\n\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Store(context.TODO(), &mockArticle)\n\n\t\tassert.Error(t, err)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n}\n\nfunc TestDelete(t *testing.T) {\n\tmockArticleRepo := new(mocks.ArticleRepository)\n\tmockArticle := domain.Article{\n\t\tTitle:   \"Hello\",\n\t\tContent: \"Content\",\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(mockArticle, nil).Once()\n\n\t\tmockArticleRepo.On(\"Delete\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(nil).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Delete(context.TODO(), mockArticle.ID)\n\n\t\tassert.NoError(t, err)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n\tt.Run(\"article-is-not-exist\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(domain.Article{}, nil).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Delete(context.TODO(), mockArticle.ID)\n\n\t\tassert.Error(t, err)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n\tt.Run(\"error-happens-in-db\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"GetByID\", mock.Anything, mock.AnythingOfType(\"int64\")).Return(domain.Article{}, errors.New(\"Unexpected Error\")).Once()\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Delete(context.TODO(), mockArticle.ID)\n\n\t\tassert.Error(t, err)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t\tmockAuthorrepo.AssertExpectations(t)\n\t})\n}\n\nfunc TestUpdate(t *testing.T) {\n\tmockArticleRepo := new(mocks.ArticleRepository)\n\tmockArticle := domain.Article{\n\t\tTitle:   \"Hello\",\n\t\tContent: \"Content\",\n\t\tID:      23,\n\t}\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tmockArticleRepo.On(\"Update\", mock.Anything, &mockArticle).Once().Return(nil)\n\n\t\tmockAuthorrepo := new(mocks.AuthorRepository)\n\t\tu := article.NewService(mockArticleRepo, mockAuthorrepo)\n\n\t\terr := u.Update(context.TODO(), &mockArticle)\n\t\tassert.NoError(t, err)\n\t\tmockArticleRepo.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "article.sql",
    "content": "CREATE DATABASE  IF NOT EXISTS `article` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;\nUSE `article`;\n-- MySQL dump 10.13  Distrib 5.7.17, for macos10.12 (x86_64)\n--\n-- Host: localhost    Database: article\n-- ------------------------------------------------------\n-- Server version\t5.7.18\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `article`\n--\n\nDROP TABLE IF EXISTS `article`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `article` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `title` varchar(45) COLLATE utf8_unicode_ci NOT NULL,\n  `content` longtext COLLATE utf8_unicode_ci NOT NULL,\n  `author_id` int(11) DEFAULT '0',\n  `updated_at` datetime DEFAULT NULL,\n  `created_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `article`\n--\n\nLOCK TABLES `article` WRITE;\n/*!40000 ALTER TABLE `article` DISABLE KEYS */;\nINSERT 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');\n/*!40000 ALTER TABLE `article` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `article_category`\n--\n\nDROP TABLE IF EXISTS `article_category`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `article_category` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `article_id` int(11) NOT NULL,\n  `category_id` int(11) NOT NULL,\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `composite` (`article_id`,`category_id`)\n) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `article_category`\n--\n\nLOCK TABLES `article_category` WRITE;\n/*!40000 ALTER TABLE `article_category` DISABLE KEYS */;\nINSERT 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);\n/*!40000 ALTER TABLE `article_category` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `author`\n--\n\nDROP TABLE IF EXISTS `author`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `author` (\n  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(200) COLLATE utf8_unicode_ci DEFAULT '\"\"',\n  `created_at` datetime DEFAULT NULL,\n  `updated_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `author`\n--\n\nLOCK TABLES `author` WRITE;\n/*!40000 ALTER TABLE `author` DISABLE KEYS */;\nINSERT INTO `author` VALUES (1,'Iman Tumorang','2017-05-18 13:50:19','2017-05-18 13:50:19');\n/*!40000 ALTER TABLE `author` ENABLE KEYS */;\nUNLOCK TABLES;\n\n--\n-- Table structure for table `category`\n--\n\nDROP TABLE IF EXISTS `category`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `category` (\n  `id` int(11) NOT NULL AUTO_INCREMENT,\n  `name` varchar(45) COLLATE utf8_unicode_ci NOT NULL,\n  `tag` varchar(45) COLLATE utf8_unicode_ci NOT NULL,\n  `created_at` datetime DEFAULT NULL,\n  `updated_at` datetime DEFAULT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Dumping data for table `category`\n--\n\nLOCK TABLES `category` WRITE;\n/*!40000 ALTER TABLE `category` DISABLE KEYS */;\nINSERT 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');\n/*!40000 ALTER TABLE `category` ENABLE KEYS */;\nUNLOCK TABLES;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2017-12-13 17:17:00\n"
  },
  {
    "path": "compose.yaml",
    "content": "version: \"3.7\"\nservices:\n  web:\n    image: go-clean-arch\n    container_name: article_management_api\n    ports:\n      - 9090:9090\n    depends_on:\n      mysql:\n        condition: service_healthy\n    volumes:\n      - ./config.json:/app/config.json\n\n  mysql:\n    image: mysql:8.3\n    container_name: go_clean_arch_mysql\n    command: mysqld --user=root\n    volumes:\n      - ./article.sql:/docker-entrypoint-initdb.d/init.sql\n    ports:\n      - 3306:3306\n    environment:\n      - MYSQL_DATABASE=article\n      - MYSQL_USER=user\n      - MYSQL_PASSWORD=password\n      - MYSQL_ROOT_PASSWORD=root\n    healthcheck:\n      test: [\"CMD\", \"mysqladmin\", \"ping\", \"-h\", \"localhost\"]\n      timeout: 5s\n      retries: 10\n"
  },
  {
    "path": "domain/article.go",
    "content": "package domain\n\nimport (\n\t\"time\"\n)\n\n// Article is representing the Article data struct\ntype Article struct {\n\tID        int64     `json:\"id\"`\n\tTitle     string    `json:\"title\" validate:\"required\"`\n\tContent   string    `json:\"content\" validate:\"required\"`\n\tAuthor    Author    `json:\"author\"`\n\tUpdatedAt time.Time `json:\"updated_at\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n}\n"
  },
  {
    "path": "domain/author.go",
    "content": "package domain\n\n// Author representing the Author data struct\ntype Author struct {\n\tID        int64  `json:\"id\"`\n\tName      string `json:\"name\"`\n\tCreatedAt string `json:\"created_at\"`\n\tUpdatedAt string `json:\"updated_at\"`\n}\n"
  },
  {
    "path": "domain/errors.go",
    "content": "package domain\n\nimport \"errors\"\n\nvar (\n\t// ErrInternalServerError will throw if any the Internal Server Error happen\n\tErrInternalServerError = errors.New(\"internal Server Error\")\n\t// ErrNotFound will throw if the requested item is not exists\n\tErrNotFound = errors.New(\"your requested Item is not found\")\n\t// ErrConflict will throw if the current action already exists\n\tErrConflict = errors.New(\"your Item already exist\")\n\t// ErrBadParamInput will throw if the given request-body or params is not valid\n\tErrBadParamInput = errors.New(\"given Param is not valid\")\n)\n"
  },
  {
    "path": "example.env",
    "content": "DEBUG = True\nSERVER_ADDRESS = \":9090\"\nCONTEXT_TIMEOUT = 2\nDATABASE_HOST = \"localhost\"\nDATABASE_PORT = \"3306\"\nDATABASE_USER = \"user\"\nDATABASE_PASS = \"password\"\nDATABASE_NAME = \"article\""
  },
  {
    "path": "go.mod",
    "content": "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-driver/mysql v1.7.1\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/labstack/echo/v4 v4.11.4\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/stretchr/testify v1.9.0\n\tgolang.org/x/sync v0.6.0\n\tgopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0\n\tgopkg.in/go-playground/validator.v9 v9.31.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/labstack/gommon v0.4.2 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.2.2 // indirect\n\tgolang.org/x/crypto v0.19.0 // indirect\n\tgolang.org/x/net v0.21.0 // indirect\n\tgolang.org/x/sys v0.17.0 // indirect\n\tgolang.org/x/text v0.14.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect\n\tgopkg.in/go-playground/assert.v1 v1.2.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-faker/faker/v4 v4.3.0 h1:UXOW7kn/Mwd0u6MR30JjUKVzguT20EB/hBOddAAO+DY=\ngithub.com/go-faker/faker/v4 v4.3.0/go.mod h1:F/bBy8GH9NxOxMInug5Gx4WYeG6fHJZ8Ol/dhcpRub4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=\ngithub.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=\ngithub.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=\ngithub.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=\ngithub.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=\ngithub.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngolang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=\ngopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=\ngopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=\ngopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=\ngopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/README.md",
    "content": "# Internal Directory Guidelines\n\nThis 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.\n\nThe 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.\n\nAn \"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.\n"
  },
  {
    "path": "internal/repository/helper.go",
    "content": "package repository\n\nimport (\n\t\"encoding/base64\"\n\t\"time\"\n)\n\nconst (\n\ttimeFormat = \"2006-01-02T15:04:05.999Z07:00\" // reduce precision from RFC3339Nano as date format\n)\n\n// DecodeCursor will decode cursor from user for mysql\nfunc DecodeCursor(encodedTime string) (time.Time, error) {\n\tbyt, err := base64.StdEncoding.DecodeString(encodedTime)\n\tif err != nil {\n\t\treturn time.Time{}, err\n\t}\n\n\ttimeString := string(byt)\n\tt, err := time.Parse(timeFormat, timeString)\n\n\treturn t, err\n}\n\n// EncodeCursor will encode cursor from mysql to user\nfunc EncodeCursor(t time.Time) string {\n\ttimeString := t.Format(timeFormat)\n\n\treturn base64.StdEncoding.EncodeToString([]byte(timeString))\n}\n"
  },
  {
    "path": "internal/repository/mysql/article.go",
    "content": "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-arch/domain\"\n\t\"github.com/bxcodec/go-clean-arch/internal/repository\"\n)\n\ntype ArticleRepository struct {\n\tConn *sql.DB\n}\n\n// NewArticleRepository will create an object that represent the article.Repository interface\nfunc NewArticleRepository(conn *sql.DB) *ArticleRepository {\n\treturn &ArticleRepository{conn}\n}\n\nfunc (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) {\n\trows, err := m.Conn.QueryContext(ctx, query, args...)\n\tif err != nil {\n\t\tlogrus.Error(err)\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\terrRow := rows.Close()\n\t\tif errRow != nil {\n\t\t\tlogrus.Error(errRow)\n\t\t}\n\t}()\n\n\tresult = make([]domain.Article, 0)\n\tfor rows.Next() {\n\t\tt := domain.Article{}\n\t\tauthorID := int64(0)\n\t\terr = rows.Scan(\n\t\t\t&t.ID,\n\t\t\t&t.Title,\n\t\t\t&t.Content,\n\t\t\t&authorID,\n\t\t\t&t.UpdatedAt,\n\t\t\t&t.CreatedAt,\n\t\t)\n\n\t\tif err != nil {\n\t\t\tlogrus.Error(err)\n\t\t\treturn nil, err\n\t\t}\n\t\tt.Author = domain.Author{\n\t\t\tID: authorID,\n\t\t}\n\t\tresult = append(result, t)\n\t}\n\n\treturn result, nil\n}\n\nfunc (m *ArticleRepository) Fetch(ctx context.Context, cursor string, num int64) (res []domain.Article, nextCursor string, err error) {\n\tquery := `SELECT id,title,content, author_id, updated_at, created_at\n  \t\t\t\t\t\tFROM article WHERE created_at > ? ORDER BY created_at LIMIT ? `\n\n\tdecodedCursor, err := repository.DecodeCursor(cursor)\n\tif err != nil && cursor != \"\" {\n\t\treturn nil, \"\", domain.ErrBadParamInput\n\t}\n\n\tres, err = m.fetch(ctx, query, decodedCursor, num)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tif len(res) == int(num) {\n\t\tnextCursor = repository.EncodeCursor(res[len(res)-1].CreatedAt)\n\t}\n\n\treturn\n}\nfunc (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) {\n\tquery := `SELECT id,title,content, author_id, updated_at, created_at\n  \t\t\t\t\t\tFROM article WHERE ID = ?`\n\n\tlist, err := m.fetch(ctx, query, id)\n\tif err != nil {\n\t\treturn domain.Article{}, err\n\t}\n\n\tif len(list) > 0 {\n\t\tres = list[0]\n\t} else {\n\t\treturn res, domain.ErrNotFound\n\t}\n\n\treturn\n}\n\nfunc (m *ArticleRepository) GetByTitle(ctx context.Context, title string) (res domain.Article, err error) {\n\tquery := `SELECT id,title,content, author_id, updated_at, created_at\n  \t\t\t\t\t\tFROM article WHERE title = ?`\n\n\tlist, err := m.fetch(ctx, query, title)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif len(list) > 0 {\n\t\tres = list[0]\n\t} else {\n\t\treturn res, domain.ErrNotFound\n\t}\n\treturn\n}\n\nfunc (m *ArticleRepository) Store(ctx context.Context, a *domain.Article) (err error) {\n\tquery := `INSERT  article SET title=? , content=? , author_id=?, updated_at=? , created_at=?`\n\tstmt, err := m.Conn.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tres, err := stmt.ExecContext(ctx, a.Title, a.Content, a.Author.ID, a.UpdatedAt, a.CreatedAt)\n\tif err != nil {\n\t\treturn\n\t}\n\tlastID, err := res.LastInsertId()\n\tif err != nil {\n\t\treturn\n\t}\n\ta.ID = lastID\n\treturn\n}\n\nfunc (m *ArticleRepository) Delete(ctx context.Context, id int64) (err error) {\n\tquery := \"DELETE FROM article WHERE id = ?\"\n\n\tstmt, err := m.Conn.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tres, err := stmt.ExecContext(ctx, id)\n\tif err != nil {\n\t\treturn\n\t}\n\n\trowsAfected, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif rowsAfected != 1 {\n\t\terr = fmt.Errorf(\"weird  Behavior. Total Affected: %d\", rowsAfected)\n\t\treturn\n\t}\n\n\treturn\n}\nfunc (m *ArticleRepository) Update(ctx context.Context, ar *domain.Article) (err error) {\n\tquery := `UPDATE article set title=?, content=?, author_id=?, updated_at=? WHERE ID = ?`\n\n\tstmt, err := m.Conn.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tres, err := stmt.ExecContext(ctx, ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID)\n\tif err != nil {\n\t\treturn\n\t}\n\taffect, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn\n\t}\n\tif affect != 1 {\n\t\terr = fmt.Errorf(\"weird  Behavior. Total Affected: %d\", affect)\n\t\treturn\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "internal/repository/mysql/article_test.go",
    "content": "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/DATA-DOG/go-sqlmock.v1\"\n\n\t\"github.com/bxcodec/go-clean-arch/domain\"\n\t\"github.com/bxcodec/go-clean-arch/internal/repository\"\n\tarticleMysqlRepo \"github.com/bxcodec/go-clean-arch/internal/repository/mysql\"\n)\n\nfunc TestFetchArticle(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\tmockArticles := []domain.Article{\n\t\t{\n\t\t\tID: 1, Title: \"title 1\", Content: \"content 1\",\n\t\t\tAuthor: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(),\n\t\t},\n\t\t{\n\t\t\tID: 2, Title: \"title 2\", Content: \"content 2\",\n\t\t\tAuthor: domain.Author{ID: 1}, UpdatedAt: time.Now(), CreatedAt: time.Now(),\n\t\t},\n\t}\n\n\trows := sqlmock.NewRows([]string{\"id\", \"title\", \"content\", \"author_id\", \"updated_at\", \"created_at\"}).\n\t\tAddRow(mockArticles[0].ID, mockArticles[0].Title, mockArticles[0].Content,\n\t\t\tmockArticles[0].Author.ID, mockArticles[0].UpdatedAt, mockArticles[0].CreatedAt).\n\t\tAddRow(mockArticles[1].ID, mockArticles[1].Title, mockArticles[1].Content,\n\t\t\tmockArticles[1].Author.ID, mockArticles[1].UpdatedAt, mockArticles[1].CreatedAt)\n\n\tquery := \"SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE created_at > \\\\? ORDER BY created_at LIMIT \\\\?\"\n\n\tmock.ExpectQuery(query).WillReturnRows(rows)\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\tcursor := repository.EncodeCursor(mockArticles[1].CreatedAt)\n\tnum := int64(2)\n\tlist, nextCursor, err := a.Fetch(context.TODO(), cursor, num)\n\tassert.NotEmpty(t, nextCursor)\n\tassert.NoError(t, err)\n\tassert.Len(t, list, 2)\n}\n\nfunc TestGetArticleByID(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\trows := sqlmock.NewRows([]string{\"id\", \"title\", \"content\", \"author_id\", \"updated_at\", \"created_at\"}).\n\t\tAddRow(1, \"title 1\", \"Content 1\", 1, time.Now(), time.Now())\n\n\tquery := \"SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = \\\\?\"\n\n\tmock.ExpectQuery(query).WillReturnRows(rows)\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\n\tnum := int64(5)\n\tanArticle, err := a.GetByID(context.TODO(), num)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, anArticle)\n}\n\nfunc TestStoreArticle(t *testing.T) {\n\tnow := time.Now()\n\tar := &domain.Article{\n\t\tTitle:     \"Judul\",\n\t\tContent:   \"Content\",\n\t\tCreatedAt: now,\n\t\tUpdatedAt: now,\n\t\tAuthor: domain.Author{\n\t\t\tID:   1,\n\t\t\tName: \"Iman Tumorang\",\n\t\t},\n\t}\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\tquery := \"INSERT  article SET title=\\\\? , content=\\\\? , author_id=\\\\?, updated_at=\\\\? , created_at=\\\\?\"\n\tprep := mock.ExpectPrepare(query)\n\tprep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.CreatedAt, ar.UpdatedAt).WillReturnResult(sqlmock.NewResult(12, 1))\n\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\n\terr = a.Store(context.TODO(), ar)\n\tassert.NoError(t, err)\n\tassert.Equal(t, int64(12), ar.ID)\n}\n\nfunc TestGetArticleByTitle(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\trows := sqlmock.NewRows([]string{\"id\", \"title\", \"content\", \"author_id\", \"updated_at\", \"created_at\"}).\n\t\tAddRow(1, \"title 1\", \"Content 1\", 1, time.Now(), time.Now())\n\n\tquery := \"SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE title = \\\\?\"\n\n\tmock.ExpectQuery(query).WillReturnRows(rows)\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\n\ttitle := \"title 1\"\n\tanArticle, err := a.GetByTitle(context.TODO(), title)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, anArticle)\n}\n\nfunc TestDeleteArticle(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\tquery := \"DELETE FROM article WHERE id = \\\\?\"\n\n\tprep := mock.ExpectPrepare(query)\n\tprep.ExpectExec().WithArgs(12).WillReturnResult(sqlmock.NewResult(12, 1))\n\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\n\tnum := int64(12)\n\terr = a.Delete(context.TODO(), num)\n\tassert.NoError(t, err)\n}\n\nfunc TestUpdateArticle(t *testing.T) {\n\tnow := time.Now()\n\tar := &domain.Article{\n\t\tID:        12,\n\t\tTitle:     \"Judul\",\n\t\tContent:   \"Content\",\n\t\tCreatedAt: now,\n\t\tUpdatedAt: now,\n\t\tAuthor: domain.Author{\n\t\t\tID:   1,\n\t\t\tName: \"Iman Tumorang\",\n\t\t},\n\t}\n\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\tquery := \"UPDATE article set title=\\\\?, content=\\\\?, author_id=\\\\?, updated_at=\\\\? WHERE ID = \\\\?\"\n\n\tprep := mock.ExpectPrepare(query)\n\tprep.ExpectExec().WithArgs(ar.Title, ar.Content, ar.Author.ID, ar.UpdatedAt, ar.ID).WillReturnResult(sqlmock.NewResult(12, 1))\n\n\ta := articleMysqlRepo.NewArticleRepository(db)\n\n\terr = a.Update(context.TODO(), ar)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/repository/mysql/author.go",
    "content": "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 struct {\n\tDB *sql.DB\n}\n\n// NewMysqlAuthorRepository will create an implementation of author.Repository\nfunc NewAuthorRepository(db *sql.DB) *AuthorRepository {\n\treturn &AuthorRepository{\n\t\tDB: db,\n\t}\n}\n\nfunc (m *AuthorRepository) getOne(ctx context.Context, query string, args ...interface{}) (res domain.Author, err error) {\n\tstmt, err := m.DB.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn domain.Author{}, err\n\t}\n\trow := stmt.QueryRowContext(ctx, args...)\n\tres = domain.Author{}\n\n\terr = row.Scan(\n\t\t&res.ID,\n\t\t&res.Name,\n\t\t&res.CreatedAt,\n\t\t&res.UpdatedAt,\n\t)\n\treturn\n}\n\nfunc (m *AuthorRepository) GetByID(ctx context.Context, id int64) (domain.Author, error) {\n\tquery := `SELECT id, name, created_at, updated_at FROM author WHERE id=?`\n\treturn m.getOne(ctx, query, id)\n}\n"
  },
  {
    "path": "internal/repository/mysql/author_test.go",
    "content": "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/DATA-DOG/go-sqlmock.v1\"\n\n\trepository \"github.com/bxcodec/go-clean-arch/internal/repository/mysql\"\n)\n\nfunc TestGetAuthorByID(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tif err != nil {\n\t\tt.Fatalf(\"an error '%s' was not expected when opening a stub database connection\", err)\n\t}\n\n\trows := sqlmock.NewRows([]string{\"id\", \"name\", \"updated_at\", \"created_at\"}).\n\t\tAddRow(1, \"Iman Tumorang\", time.Now(), time.Now())\n\n\tquery := \"SELECT id, name, created_at, updated_at FROM author WHERE id=\\\\?\"\n\n\tprep := mock.ExpectPrepare(query)\n\tuserID := int64(1)\n\tprep.ExpectQuery().WithArgs(userID).WillReturnRows(rows)\n\n\ta := repository.NewAuthorRepository(db)\n\n\tanArticle, err := a.GetByID(context.TODO(), userID)\n\tassert.NoError(t, err)\n\tassert.NotNil(t, anArticle)\n}\n"
  },
  {
    "path": "internal/rest/article.go",
    "content": "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\tvalidator \"gopkg.in/go-playground/validator.v9\"\n\n\t\"github.com/bxcodec/go-clean-arch/domain\"\n)\n\n// ResponseError represent the response error struct\ntype ResponseError struct {\n\tMessage string `json:\"message\"`\n}\n\n// ArticleService represent the article's usecases\n//\n//go:generate mockery --name ArticleService\ntype ArticleService interface {\n\tFetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error)\n\tGetByID(ctx context.Context, id int64) (domain.Article, error)\n\tUpdate(ctx context.Context, ar *domain.Article) error\n\tGetByTitle(ctx context.Context, title string) (domain.Article, error)\n\tStore(context.Context, *domain.Article) error\n\tDelete(ctx context.Context, id int64) error\n}\n\n// ArticleHandler  represent the httphandler for article\ntype ArticleHandler struct {\n\tService ArticleService\n}\n\nconst defaultNum = 10\n\n// NewArticleHandler will initialize the articles/ resources endpoint\nfunc NewArticleHandler(e *echo.Echo, svc ArticleService) {\n\thandler := &ArticleHandler{\n\t\tService: svc,\n\t}\n\te.GET(\"/articles\", handler.FetchArticle)\n\te.POST(\"/articles\", handler.Store)\n\te.GET(\"/articles/:id\", handler.GetByID)\n\te.DELETE(\"/articles/:id\", handler.Delete)\n}\n\n// FetchArticle will fetch the article based on given params\nfunc (a *ArticleHandler) FetchArticle(c echo.Context) error {\n\n\tnumS := c.QueryParam(\"num\")\n\tnum, err := strconv.Atoi(numS)\n\tif err != nil || num == 0 {\n\t\tnum = defaultNum\n\t}\n\n\tcursor := c.QueryParam(\"cursor\")\n\tctx := c.Request().Context()\n\n\tlistAr, nextCursor, err := a.Service.Fetch(ctx, cursor, int64(num))\n\tif err != nil {\n\t\treturn c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})\n\t}\n\n\tc.Response().Header().Set(`X-Cursor`, nextCursor)\n\treturn c.JSON(http.StatusOK, listAr)\n}\n\n// GetByID will get article by given id\nfunc (a *ArticleHandler) GetByID(c echo.Context) error {\n\tidP, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\treturn c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())\n\t}\n\n\tid := int64(idP)\n\tctx := c.Request().Context()\n\n\tart, err := a.Service.GetByID(ctx, id)\n\tif err != nil {\n\t\treturn c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})\n\t}\n\n\treturn c.JSON(http.StatusOK, art)\n}\n\nfunc isRequestValid(m *domain.Article) (bool, error) {\n\tvalidate := validator.New()\n\terr := validate.Struct(m)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// Store will store the article by given request body\nfunc (a *ArticleHandler) Store(c echo.Context) (err error) {\n\tvar article domain.Article\n\terr = c.Bind(&article)\n\tif err != nil {\n\t\treturn c.JSON(http.StatusUnprocessableEntity, err.Error())\n\t}\n\n\tvar ok bool\n\tif ok, err = isRequestValid(&article); !ok {\n\t\treturn c.JSON(http.StatusBadRequest, err.Error())\n\t}\n\n\tctx := c.Request().Context()\n\terr = a.Service.Store(ctx, &article)\n\tif err != nil {\n\t\treturn c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})\n\t}\n\n\treturn c.JSON(http.StatusCreated, article)\n}\n\n// Delete will delete article by given param\nfunc (a *ArticleHandler) Delete(c echo.Context) error {\n\tidP, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\treturn c.JSON(http.StatusNotFound, domain.ErrNotFound.Error())\n\t}\n\n\tid := int64(idP)\n\tctx := c.Request().Context()\n\n\terr = a.Service.Delete(ctx, id)\n\tif err != nil {\n\t\treturn c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})\n\t}\n\n\treturn c.NoContent(http.StatusNoContent)\n}\n\nfunc getStatusCode(err error) int {\n\tif err == nil {\n\t\treturn http.StatusOK\n\t}\n\n\tlogrus.Error(err)\n\tswitch err {\n\tcase domain.ErrInternalServerError:\n\t\treturn http.StatusInternalServerError\n\tcase domain.ErrNotFound:\n\t\treturn http.StatusNotFound\n\tcase domain.ErrConflict:\n\t\treturn http.StatusConflict\n\tdefault:\n\t\treturn http.StatusInternalServerError\n\t}\n}\n"
  },
  {
    "path": "internal/rest/article_test.go",
    "content": "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\"\n\t\"time\"\n\n\tfaker \"github.com/go-faker/faker/v4\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bxcodec/go-clean-arch/domain\"\n\t\"github.com/bxcodec/go-clean-arch/internal/rest\"\n\t\"github.com/bxcodec/go-clean-arch/internal/rest/mocks\"\n)\n\nfunc TestFetch(t *testing.T) {\n\tvar mockArticle domain.Article\n\terr := faker.FakeData(&mockArticle)\n\tassert.NoError(t, err)\n\tmockUCase := new(mocks.ArticleService)\n\tmockListArticle := make([]domain.Article, 0)\n\tmockListArticle = append(mockListArticle, mockArticle)\n\tnum := 1\n\tcursor := \"2\"\n\tmockUCase.On(\"Fetch\", mock.Anything, cursor, int64(num)).Return(mockListArticle, \"10\", nil)\n\n\te := echo.New()\n\treq, err := http.NewRequestWithContext(context.TODO(),\n\t\techo.GET, \"/article?num=1&cursor=\"+cursor, strings.NewReader(\"\"))\n\tassert.NoError(t, err)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := rest.ArticleHandler{\n\t\tService: mockUCase,\n\t}\n\terr = handler.FetchArticle(c)\n\trequire.NoError(t, err)\n\n\tresponseCursor := rec.Header().Get(\"X-Cursor\")\n\tassert.Equal(t, \"10\", responseCursor)\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tmockUCase.AssertExpectations(t)\n}\n\nfunc TestFetchError(t *testing.T) {\n\tmockUCase := new(mocks.ArticleService)\n\tnum := 1\n\tcursor := \"2\"\n\tmockUCase.On(\"Fetch\", mock.Anything, cursor, int64(num)).Return(nil, \"\", domain.ErrInternalServerError)\n\n\te := echo.New()\n\treq, err := http.NewRequestWithContext(context.TODO(), echo.GET, \"/article?num=1&cursor=\"+cursor, strings.NewReader(\"\"))\n\tassert.NoError(t, err)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\thandler := rest.ArticleHandler{\n\t\tService: mockUCase,\n\t}\n\terr = handler.FetchArticle(c)\n\trequire.NoError(t, err)\n\n\tresponseCursor := rec.Header().Get(\"X-Cursor\")\n\tassert.Equal(t, \"\", responseCursor)\n\tassert.Equal(t, http.StatusInternalServerError, rec.Code)\n\tmockUCase.AssertExpectations(t)\n}\n\nfunc TestGetByID(t *testing.T) {\n\tvar mockArticle domain.Article\n\terr := faker.FakeData(&mockArticle)\n\tassert.NoError(t, err)\n\n\tmockUCase := new(mocks.ArticleService)\n\n\tnum := int(mockArticle.ID)\n\n\tmockUCase.On(\"GetByID\", mock.Anything, int64(num)).Return(mockArticle, nil)\n\n\te := echo.New()\n\treq, err := http.NewRequestWithContext(context.TODO(), echo.GET, \"/article/\"+strconv.Itoa(num), strings.NewReader(\"\"))\n\tassert.NoError(t, err)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tc.SetPath(\"article/:id\")\n\tc.SetParamNames(\"id\")\n\tc.SetParamValues(strconv.Itoa(num))\n\thandler := rest.ArticleHandler{\n\t\tService: mockUCase,\n\t}\n\terr = handler.GetByID(c)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusOK, rec.Code)\n\tmockUCase.AssertExpectations(t)\n}\n\nfunc TestStore(t *testing.T) {\n\tmockArticle := domain.Article{\n\t\tTitle:     \"Title\",\n\t\tContent:   \"Content\",\n\t\tCreatedAt: time.Now(),\n\t\tUpdatedAt: time.Now(),\n\t}\n\n\ttempMockArticle := mockArticle\n\ttempMockArticle.ID = 0\n\tmockUCase := new(mocks.ArticleService)\n\n\tj, err := json.Marshal(tempMockArticle)\n\tassert.NoError(t, err)\n\n\tmockUCase.On(\"Store\", mock.Anything, mock.AnythingOfType(\"*domain.Article\")).Return(nil)\n\n\te := echo.New()\n\treq, err := http.NewRequestWithContext(context.TODO(), echo.POST, \"/article\", strings.NewReader(string(j)))\n\tassert.NoError(t, err)\n\treq.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tc.SetPath(\"/article\")\n\n\thandler := rest.ArticleHandler{\n\t\tService: mockUCase,\n\t}\n\terr = handler.Store(c)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusCreated, rec.Code)\n\tmockUCase.AssertExpectations(t)\n}\n\nfunc TestDelete(t *testing.T) {\n\tvar mockArticle domain.Article\n\terr := faker.FakeData(&mockArticle)\n\tassert.NoError(t, err)\n\n\tmockUCase := new(mocks.ArticleService)\n\n\tnum := int(mockArticle.ID)\n\n\tmockUCase.On(\"Delete\", mock.Anything, int64(num)).Return(nil)\n\n\te := echo.New()\n\treq, err := http.NewRequestWithContext(context.TODO(), echo.DELETE, \"/article/\"+strconv.Itoa(num), strings.NewReader(\"\"))\n\tassert.NoError(t, err)\n\n\trec := httptest.NewRecorder()\n\tc := e.NewContext(req, rec)\n\tc.SetPath(\"article/:id\")\n\tc.SetParamNames(\"id\")\n\tc.SetParamValues(strconv.Itoa(num))\n\thandler := rest.ArticleHandler{\n\t\tService: mockUCase,\n\t}\n\terr = handler.Delete(c)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, http.StatusNoContent, rec.Code)\n\tmockUCase.AssertExpectations(t)\n}\n"
  },
  {
    "path": "internal/rest/middleware/cors.go",
    "content": "package middleware\n\nimport \"github.com/labstack/echo/v4\"\n\n// CORS will handle the CORS middleware\nfunc CORS(next echo.HandlerFunc) echo.HandlerFunc {\n\treturn func(c echo.Context) error {\n\t\tc.Response().Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\treturn next(c)\n\t}\n}\n"
  },
  {
    "path": "internal/rest/middleware/cors_test.go",
    "content": "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\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bxcodec/go-clean-arch/internal/rest/middleware\"\n)\n\nfunc TestCORS(t *testing.T) {\n\te := echo.New()\n\treq := test.NewRequest(echo.GET, \"/\", nil)\n\tres := test.NewRecorder()\n\tc := e.NewContext(req, res)\n\n\th := middleware.CORS(echo.HandlerFunc(func(c echo.Context) error {\n\t\treturn c.NoContent(http.StatusOK)\n\t}))\n\n\terr := h(c)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"*\", res.Header().Get(\"Access-Control-Allow-Origin\"))\n}\n"
  },
  {
    "path": "internal/rest/middleware/timeout.go",
    "content": "package middleware\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\techo \"github.com/labstack/echo/v4\"\n)\n\n// SetRequestContextWithTimeout will set the request context with timeout for every incoming HTTP Request\nfunc SetRequestContextWithTimeout(d time.Duration) echo.MiddlewareFunc {\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(c echo.Context) error {\n\t\t\tctx, cancel := context.WithTimeout(c.Request().Context(), d)\n\t\t\tdefer cancel()\n\n\t\t\tnewRequest := c.Request().WithContext(ctx)\n\t\t\tc.SetRequest(newRequest)\n\t\t\treturn next(c)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/rest/mocks/ArticleService.go",
    "content": "// Code generated by mockery v2.42.0. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tdomain \"github.com/bxcodec/go-clean-arch/domain\"\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// ArticleService is an autogenerated mock type for the ArticleService type\ntype ArticleService struct {\n\tmock.Mock\n}\n\n// Delete provides a mock function with given fields: ctx, id\nfunc (_m *ArticleService) Delete(ctx context.Context, id int64) error {\n\tret := _m.Called(ctx, id)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Delete\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Fetch provides a mock function with given fields: ctx, cursor, num\nfunc (_m *ArticleService) Fetch(ctx context.Context, cursor string, num int64) ([]domain.Article, string, error) {\n\tret := _m.Called(ctx, cursor, num)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Fetch\")\n\t}\n\n\tvar r0 []domain.Article\n\tvar r1 string\n\tvar r2 error\n\tif rf, ok := ret.Get(0).(func(context.Context, string, int64) ([]domain.Article, string, error)); ok {\n\t\treturn rf(ctx, cursor, num)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, string, int64) []domain.Article); ok {\n\t\tr0 = rf(ctx, cursor, num)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).([]domain.Article)\n\t\t}\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, string, int64) string); ok {\n\t\tr1 = rf(ctx, cursor, num)\n\t} else {\n\t\tr1 = ret.Get(1).(string)\n\t}\n\n\tif rf, ok := ret.Get(2).(func(context.Context, string, int64) error); ok {\n\t\tr2 = rf(ctx, cursor, num)\n\t} else {\n\t\tr2 = ret.Error(2)\n\t}\n\n\treturn r0, r1, r2\n}\n\n// GetByID provides a mock function with given fields: ctx, id\nfunc (_m *ArticleService) GetByID(ctx context.Context, id int64) (domain.Article, error) {\n\tret := _m.Called(ctx, id)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetByID\")\n\t}\n\n\tvar r0 domain.Article\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) (domain.Article, error)); ok {\n\t\treturn rf(ctx, id)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, int64) domain.Article); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Get(0).(domain.Article)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {\n\t\tr1 = rf(ctx, id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetByTitle provides a mock function with given fields: ctx, title\nfunc (_m *ArticleService) GetByTitle(ctx context.Context, title string) (domain.Article, error) {\n\tret := _m.Called(ctx, title)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for GetByTitle\")\n\t}\n\n\tvar r0 domain.Article\n\tvar r1 error\n\tif rf, ok := ret.Get(0).(func(context.Context, string) (domain.Article, error)); ok {\n\t\treturn rf(ctx, title)\n\t}\n\tif rf, ok := ret.Get(0).(func(context.Context, string) domain.Article); ok {\n\t\tr0 = rf(ctx, title)\n\t} else {\n\t\tr0 = ret.Get(0).(domain.Article)\n\t}\n\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, title)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Store provides a mock function with given fields: _a0, _a1\nfunc (_m *ArticleService) Store(_a0 context.Context, _a1 *domain.Article) error {\n\tret := _m.Called(_a0, _a1)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Store\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok {\n\t\tr0 = rf(_a0, _a1)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Update provides a mock function with given fields: ctx, ar\nfunc (_m *ArticleService) Update(ctx context.Context, ar *domain.Article) error {\n\tret := _m.Called(ctx, ar)\n\n\tif len(ret) == 0 {\n\t\tpanic(\"no return value specified for Update\")\n\t}\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, *domain.Article) error); ok {\n\t\tr0 = rf(ctx, ar)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// 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.\n// The first argument is typically a *testing.T value.\nfunc NewArticleService(t interface {\n\tmock.TestingT\n\tCleanup(func())\n}) *ArticleService {\n\tmock := &ArticleService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "internal/workers/.gitkeep",
    "content": ""
  },
  {
    "path": "internal/workers/README.md",
    "content": "If need queue workers/consumer can put it under this directory. See how repository looks like for reference.\n"
  },
  {
    "path": "misc/make/help.Makefile",
    "content": "dep-gawk:\n\t@ if [ -z \"$(shell command -v gawk)\" ]; then  \\\n\t\tif [ -x /usr/local/bin/brew ]; then $(MAKE) _brew_gawk_install; exit 0; fi; \\\n\t\tif [ -x /usr/bin/apt-get ]; then $(MAKE) _ubuntu_gawk_install; exit 0; fi; \\\n\t\tif [ -x /usr/bin/yum ]; then  $(MAKE) _centos_gawk_install; exit 0; fi; \\\n\t\tif [ -x /sbin/apk ]; then  $(MAKE) _alpine_gawk_install; exit 0; fi; \\\n\t\techo  \"GNU Awk Required, We cannot determine your OS or Package manager. Please install it yourself.\";\\\n\t\texit 1; \\\n\tfi\n\n_brew_gawk_install:\n\t@ echo \"Instaling gawk using brew... \"\n\t@ brew install gawk --quiet\n\t@ echo \"done\"\n\n_ubuntu_gawk_install:\n\t@ echo \"Instaling gawk using apt-get... \"\n\t@ apt-get -q install gawk -y\n\t@ echo \"done\"\n\n_alpine_gawk_install:\n\t@ echo \"Instaling gawk using yum... \"\n\t@ apk add --update --no-cache gawk\n\t@ echo \"done\"\n\n_centos_gawk_install:\n\t@ echo \"Instaling gawk using yum... \"\n\t@ yum install -q -y gawk;\n\t@ echo \"done\"\n\nhelp: dep-gawk\n\t@cat $(MAKEFILE_LIST) | \\\n\t\tgrep -E '^# ~~~ .*? [~]+$$|^[a-zA-Z0-9_-]+:.*?## .*$$' | \\\n\t\tawk '{if ( $$1==\"#\" ) { \\\n\t\t\tmatch($$0, /^# ~~~ (.+?) [~]+$$/, a);\\\n\t\t\t{print \"\\n\", a[1], \"\"}\\\n\t\t} else { \\\n\t\t\tmatch($$0, /^([0-9a-zA-Z_-]+):.*?## (.*)$$/, a); \\\n\t\t\t{printf \"  - \\033[32m%-20s\\033[0m %s\\n\",   a[1], a[2]} \\\n \t\t}}'\n\t@echo \"\""
  },
  {
    "path": "misc/make/tools.Makefile",
    "content": "# This makefile should be used to hold functions/variables\n\nifeq ($(ARCH),x86_64)\n\tARCH := amd64\nelse ifeq ($(ARCH),aarch64)\n\tARCH := arm64 \nendif\n\n\n\ndefine github_url\n    https://github.com/$(GITHUB)/releases/download/v$(VERSION)/$(ARCHIVE)\nendef\n\n# creates a directory bin.\nbin:\n\t@ mkdir -p $@\n\n# ~~~ Tools ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n# ~~ [migrate] ~~~ https://github.com/golang-migrate/migrate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMIGRATE := $(shell command -v migrate || echo \"bin/migrate\")\nmigrate: bin/migrate ## Install migrate (database migration)\n\nbin/migrate: VERSION := 4.17.0\nbin/migrate: GITHUB  := golang-migrate/migrate\nbin/migrate: ARCHIVE := migrate.$(OSTYPE)-$(ARCH).tar.gz\nbin/migrate: bin\n\t@ printf \"Install migrate... \"\n\t@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - ./migrate > $@ && chmod +x $@\n\t@ echo \"done.\"\n\n# ~~ [ air ] ~~~ https://github.com/cosmtrek/air ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAIR := $(shell command -v air || echo \"bin/air\")\nair: bin/air ## Installs air (go file watcher)\n\nbin/air: VERSION := 1.49.0\nbin/air: GITHUB  := cosmtrek/air\nbin/air: ARCHIVE := air_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz\nbin/air: bin\n\t@ printf \"Install air... \"\n\t@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - air > $@ && chmod +x $@\n\t@ echo \"done.\"\n\n\n# ~~ [ gotestsum ] ~~~ https://github.com/gotestyourself/gotestsum ~~~~~~~~~~~~~~~~~~~~~~~\n\nGOTESTSUM := $(shell command -v gotestsum || echo \"bin/gotestsum\")\ngotestsum: bin/gotestsum ## Installs gotestsum (testing go code)\n\nbin/gotestsum: VERSION := 1.11.0\nbin/gotestsum: GITHUB  := gotestyourself/gotestsum\nbin/gotestsum: ARCHIVE := gotestsum_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz\nbin/gotestsum: bin\n\t@ printf \"Install gotestsum... \"\n\t@ curl -Ls $(shell echo $(call github_url) | tr A-Z a-z) | tar -zOxf - gotestsum > $@ && chmod +x $@\n\t@ echo \"done.\"\n\n# ~~ [ tparse ] ~~~ https://github.com/mfridman/tparse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTPARSE := $(shell command -v tparse || echo \"bin/tparse\")\ntparse: bin/tparse ## Installs tparse (testing go code)\n\n# eg https://github.com/mfridman/tparse/releases/download/v0.13.2/tparse_darwin_arm64\nbin/tparse: VERSION := 0.13.2\nbin/tparse: GITHUB  := mfridman/tparse\nbin/tparse: ARCHIVE := tparse_$(OSTYPE)_$(ARCH)\nbin/tparse: bin\n\t@ printf \"Install tparse... \"\n\t@ curl -Ls $(call github_url) > $@ && chmod +x $@\n\t@ echo \"done.\"\n\n# ~~ [ mockery ] ~~~ https://github.com/vektra/mockery ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMOCKERY := $(shell command -v mockery || echo \"bin/mockery\")\nmockery: bin/mockery ## Installs mockery (mocks generation)\n\nbin/mockery: VERSION := 2.42.0\nbin/mockery: GITHUB  := vektra/mockery\nbin/mockery: ARCHIVE := mockery_$(VERSION)_$(OSTYPE)_$(ARCH).tar.gz\nbin/mockery: bin\n\t@ printf \"Install mockery... \"\n\t@ curl -Ls $(call github_url) | tar -zOxf -  mockery > $@ && chmod +x $@\n\t@ echo \"done.\"\n\n# ~~ [ golangci-lint ] ~~~ https://github.com/golangci/golangci-lint ~~~~~~~~~~~~~~~~~~~~~\n\nGOLANGCI := $(shell command -v golangci-lint || echo \"bin/golangci-lint\")\ngolangci-lint: bin/golangci-lint ## Installs golangci-lint (linter)\n\nbin/golangci-lint: VERSION := 1.56.2\nbin/golangci-lint: GITHUB  := golangci/golangci-lint\nbin/golangci-lint: ARCHIVE := golangci-lint-$(VERSION)-$(OSTYPE)-$(ARCH).tar.gz\nbin/golangci-lint: bin\n\t@ printf \"Install golangci-linter... \"\n\t@ 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 $@\n\t@ echo \"done.\""
  }
]