feat: domain finished

This commit is contained in:
Elton Minetto
2020-06-25 11:46:45 -03:00
commit 3db4550ee0
47 changed files with 2646 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/clean-architecture-go-v2.iml" filepath="$PROJECT_DIR$/.idea/clean-architecture-go-v2.iml" />
</modules>
</component>
</project>
+83
View File
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="fmt $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="go" />
<option name="immediateSync" value="false" />
<option name="name" value="go fmt" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$GoExecPath$" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Current File" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs>
<env name="GOROOT" value="$GOROOT$" />
<env name="GOPATH" value="$GOPATH$" />
<env name="PATH" value="$GoBinDirs$" />
</envs>
</TaskOptions>
<TaskOptions isEnabled="true">
<option name="arguments" value="-w $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="go" />
<option name="immediateSync" value="false" />
<option name="name" value="goimports" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="goimports" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Current File" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs>
<env name="GOROOT" value="$GOROOT$" />
<env name="GOPATH" value="$GOPATH$" />
<env name="PATH" value="$GoBinDirs$" />
</envs>
</TaskOptions>
<TaskOptions isEnabled="true">
<option name="arguments" value="-set_exit_status $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="NEVER" />
<option name="fileExtension" value="go" />
<option name="immediateSync" value="false" />
<option name="name" value="golint" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array>
<FilterInfo>
<option name="description" value="" />
<option name="name" value="" />
<option name="regExp" value="$PROJECT_DIR$/../clean-architecture-go-v2:$LINE$:$COLUMN$: $MESSAGE$" />
</FilterInfo>
</array>
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="golint" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Current File" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$ProjectFileDir$" />
<envs>
<env name="GOROOT" value="$GOROOT$" />
<env name="GOPATH" value="$GOPATH$" />
<env name="PATH" value="$GoBinDirs$" />
</envs>
</TaskOptions>
</component>
</project>
+44
View File
@@ -0,0 +1,44 @@
.PHONY: all
all: build
FORCE: ;
SHELL := env LIBRARY_ENV=$(LIBRARY_ENV) $(SHELL)
LIBRARY_ENV ?= dev
BIN_DIR = $(PWD)/bin
.PHONY: build
clean:
rm -rf bin/*
dependencies:
go mod download
build: dependencies build-api build-cmd
build-api:
go build -tags $(LIBRARY_ENV) -o ./bin/api api/main.go
build-cmd:
go build -tags $(LIBRARY_ENV) -o ./bin/search cmd/main.go
linux-binaries:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -tags "$(LIBRARY_ENV) netgo" -installsuffix netgo -o $(BIN_DIR)/api api/main.go
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -tags "$(LIBRARY_ENV) netgo" -installsuffix netgo -o $(BIN_DIR)/search cmd/main.go
ci: dependencies test
build-mocks:
@go get github.com/golang/mock/gomock
@go install github.com/golang/mock/mockgen
@~/go/bin/mockgen -source=domain/entity/book/interface.go -destination=domain/entity/book/mock/book.go -package=mock
@~/go/bin/mockgen -source=domain/entity/user/interface.go -destination=domain/entity/user/mock/user.go -package=mock
@~/go/bin/mockgen -source=domain/loan/interface.go -destination=domain/loan/mock/loan.go -package=mock
test:
go test -tags testing ./...
fmt: ## gofmt and goimports all go files
find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done
+60
View File
@@ -0,0 +1,60 @@
# Clean Architecture in Go
[![Build Status](https://travis-ci.org/eminetto/clean-architecture-go.svg?branch=master)](https://travis-ci.org/eminetto/clean-architecture-go)
Clean Architecture sample
## Post
[https://medium.com/@eminetto/clean-architecture-using-golang-b63587aa5e3f](https://medium.com/@eminetto/clean-architecture-using-golang-b63587aa5e3f)
## Build
make
## Run tests
make test
## API requests
### Add a bookmark
```
curl -X "POST" "http://localhost:8080/v1/bookmark" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d $'{
"tags": [
"git",
"social"
],
"name": "Github",
"description": "Github site",
"link": "http://github.com"
}'
```
### Search a bookmark
```
curl "http://localhost:8080/v1/bookmark?name=github" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json'
```
### Show all bookmarks
```
curl "http://localhost:8080/v1/bookmark" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json'
```
## CMD
### Search for a bookmark
```
./bin/search github
```
+153
View File
@@ -0,0 +1,153 @@
package handler
import (
"encoding/json"
"github.com/eminetto/clean-architecture-go/pkg/middleware"
"log"
"net/http"
"time"
"github.com/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go/pkg/bookmark"
"github.com/eminetto/clean-architecture-go/pkg/entity"
"github.com/gorilla/mux"
valid "github.com/asaskevich/govalidator"
)
//BookmarkInput data
type BookmarkInput struct {
ID entity.ID `json:"id" valid:"type(entity.ID)"`
Name string `json:"name" valid:"stringlength(1|50),required"`
Description string `json:"description" valid:"stringlength(1|150),required"`
Link string `json:"link" valid:"url,required"`
Tags []string `json:"tags" valid:"-"`
Favorite bool `json:"favorite" valid:"required"`
CreatedAt time.Time `json:"created_at" valid:"-"`
}
func (v *BookmarkInput) Validate() error {
_, err := valid.ValidateStruct(*v)
return err
}
func bookmarkIndex(service bookmark.UseCase) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading bookmarks"
var data []*entity.Bookmark
var err error
name := r.URL.Query().Get("name")
switch {
case name == "":
data, err = service.FindAll()
default:
data, err = service.Search(name)
}
w.Header().Set("Content-Type", "application/json")
if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
return
}
if data == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(errorMessage))
return
}
if err := json.NewEncoder(w).Encode(data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
}
})
}
func bookmarkAdd(service bookmark.UseCase) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error adding bookmark"
i := r.Context().Value("InputParam").(*BookmarkInput)
b := &entity.Bookmark{
Name: i.Name,
Description: i.Description,
Link: i.Link,
Tags: i.Tags,
Favorite: i.Favorite,
}
_, err := service.Store(b)
if err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
return
}
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(b); err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
return
}
})
}
func bookmarkFind(service bookmark.UseCase) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading bookmark"
vars := mux.Vars(r)
id := vars["id"]
data, err := service.Find(entity.StringToID(id))
w.Header().Set("Content-Type", "application/json")
if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
return
}
if data == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(errorMessage))
return
}
if err := json.NewEncoder(w).Encode(data); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
}
})
}
func bookmarkDelete(service bookmark.UseCase) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error removing bookmark"
vars := mux.Vars(r)
id := vars["id"]
err := service.Delete(entity.StringToID(id))
w.Header().Set("Content-Type", "application/json")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage))
return
}
})
}
//MakeBookmarkHandlers make url handlers
func MakeBookmarkHandlers(r *mux.Router, n negroni.Negroni, service bookmark.UseCase) {
r.Handle("/v1/bookmark", n.With(
negroni.Wrap(bookmarkIndex(service)),
)).Methods("GET", "OPTIONS").Name("bookmarkIndex")
r.Handle("/v1/bookmark", n.With(
negroni.HandlerFunc(middleware.Validate(&BookmarkInput{})),
negroni.Wrap(bookmarkAdd(service)),
)).Methods("POST", "OPTIONS").Name("bookmarkAdd")
r.Handle("/v1/bookmark/{id}", n.With(
negroni.Wrap(bookmarkFind(service)),
)).Methods("GET", "OPTIONS").Name("bookmarkFind")
r.Handle("/v1/bookmark/{id}", n.With(
negroni.Wrap(bookmarkDelete(service)),
)).Methods("DELETE", "OPTIONS").Name("bookmarkDelete")
}
+173
View File
@@ -0,0 +1,173 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go/pkg/bookmark/mock"
"github.com/eminetto/clean-architecture-go/pkg/entity"
"github.com/golang/mock/gomock"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestBookmarkIndex(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
r := mux.NewRouter()
n := negroni.New()
MakeBookmarkHandlers(r, *n, service)
path, err := r.GetRoute("bookmarkIndex").GetPathTemplate()
assert.Nil(t, err)
assert.Equal(t, "/v1/bookmark", path)
b := &entity.Bookmark{
Name: "Elton Minetto",
Description: "Minetto's page",
Link: "http://www.eltonminetto.net",
Tags: []string{"golang", "php", "linux", "mac"},
Favorite: true,
}
service.EXPECT().
FindAll().
Return([]*entity.Bookmark{b}, nil)
ts := httptest.NewServer(bookmarkIndex(service))
defer ts.Close()
res, err := http.Get(ts.URL)
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}
func TestBookmarkIndexNotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
ts := httptest.NewServer(bookmarkIndex(service))
defer ts.Close()
service.EXPECT().
Search("github").
Return(nil, entity.ErrNotFound)
res, err := http.Get(ts.URL + "?name=github")
assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
}
func TestBookmarkSearch(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
b := &entity.Bookmark{
Name: "Elton Minetto",
Description: "Minetto's page",
Link: "http://www.eltonminetto.net",
Tags: []string{"golang", "php", "linux", "mac"},
Favorite: true,
}
service.EXPECT().
Search("minetto").
Return([]*entity.Bookmark{b}, nil)
ts := httptest.NewServer(bookmarkIndex(service))
defer ts.Close()
res, err := http.Get(ts.URL + "?name=minetto")
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
}
func TestBookmarkAdd(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
r := mux.NewRouter()
n := negroni.New()
MakeBookmarkHandlers(r, *n, service)
path, err := r.GetRoute("bookmarkAdd").GetPathTemplate()
assert.Nil(t, err)
assert.Equal(t, "/v1/bookmark", path)
service.EXPECT().
Store(gomock.Any()).
Return(entity.NewID(), nil)
ts := httptest.NewServer(r)
defer ts.Close()
payload := fmt.Sprintf(`{
"name": "Github",
"description": "Github site",
"link": "http://github.com",
"tags": [
"git",
"social"
]
}`)
resp, _ := http.Post(ts.URL+"/v1/bookmark", "application/json", strings.NewReader(payload))
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var b *entity.Bookmark
json.NewDecoder(resp.Body).Decode(&b)
assert.Equal(t, "http://github.com", b.Link)
}
func TestBookmarkFind(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
r := mux.NewRouter()
n := negroni.New()
MakeBookmarkHandlers(r, *n, service)
path, err := r.GetRoute("bookmarkFind").GetPathTemplate()
assert.Nil(t, err)
assert.Equal(t, "/v1/bookmark/{id}", path)
b := &entity.Bookmark{
ID: entity.NewID(),
Name: "Elton Minetto",
Description: "Minetto's page",
Link: "http://www.eltonminetto.net",
Tags: []string{"golang", "php", "linux", "mac"},
Favorite: true,
}
service.EXPECT().
Find(b.ID).
Return(b, nil)
handler := bookmarkFind(service)
r.Handle("/v1/bookmark/{id}", handler)
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/v1/bookmark/" + b.ID.String())
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
var d *entity.Bookmark
json.NewDecoder(res.Body).Decode(&d)
assert.NotNil(t, d)
assert.Equal(t, b.ID, d.ID)
}
func TestBookmarkRemove(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
service := mock.NewMockUseCase(controller)
r := mux.NewRouter()
n := negroni.New()
MakeBookmarkHandlers(r, *n, service)
path, err := r.GetRoute("bookmarkDelete").GetPathTemplate()
assert.Nil(t, err)
assert.Equal(t, "/v1/bookmark/{id}", path)
b := &entity.Bookmark{
ID: entity.NewID(),
Name: "Elton Minetto",
Description: "Minetto's page",
Link: "http://www.eltonminetto.net",
Tags: []string{"golang", "php", "linux", "mac"},
Favorite: false,
}
service.EXPECT().Delete(b.ID).Return(nil)
handler := bookmarkDelete(service)
req, _ := http.NewRequest("DELETE", "/v1/bookmark/"+b.ID.String(), nil)
r.Handle("/v1/bookmark/{id}", handler).Methods("DELETE", "OPTIONS")
rr := httptest.NewRecorder()
r.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go/api/handler"
"github.com/eminetto/clean-architecture-go/config"
"github.com/eminetto/clean-architecture-go/pkg/bookmark"
"github.com/eminetto/clean-architecture-go/pkg/middleware"
"github.com/eminetto/clean-architecture-go/pkg/metric"
"github.com/gorilla/context"
"github.com/gorilla/mux"
"github.com/juju/mgosession"
mgo "gopkg.in/mgo.v2"
)
func main() {
session, err := mgo.Dial(config.MONGODB_HOST)
if err != nil {
log.Fatal(err.Error())
}
defer session.Close()
r := mux.NewRouter()
mPool := mgosession.NewPool(nil, session, config.MONGODB_CONNECTION_POOL)
defer mPool.Close()
bookmarkRepo := bookmark.NewMongoRepository(mPool, config.MONGODB_DATABASE)
bookmarkService := bookmark.NewService(bookmarkRepo)
metricService, err := metric.NewPrometheusService()
if err != nil {
log.Fatal(err.Error())
}
//handlers
n := negroni.New(
negroni.HandlerFunc(middleware.Cors),
negroni.HandlerFunc(middleware.Metrics(metricService)),
negroni.NewLogger(),
)
//bookmark
handler.MakeBookmarkHandlers(r, *n, bookmarkService)
http.Handle("/", r)
http.Handle("/metrics", promhttp.Handler())
r.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
logger := log.New(os.Stderr, "logger: ", log.Lshortfile)
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Addr: ":" + strconv.Itoa(config.API_PORT),
Handler: context.ClearHandler(http.DefaultServeMux),
ErrorLog: logger,
}
err = srv.ListenAndServe()
if err != nil {
log.Fatal(err.Error())
}
}
+17
View File
@@ -0,0 +1,17 @@
package middleware
import (
"net/http"
)
//Cors adiciona os headers para suportar o CORS nos navegadores
func Cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type")
w.Header().Set("Content-Type", "application/json")
if r.Method == "OPTIONS" {
return
}
next(w, r)
}
+23
View File
@@ -0,0 +1,23 @@
package middleware
import (
"net/http"
"strconv"
"github.com/eminetto/clean-architecture-go/pkg/metric"
"github.com/codegangsta/negroni"
)
//Metrics to prometheus
func Metrics(mService metric.UseCase) negroni.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
appMetric := metric.NewHTTP(r.URL.Path, r.Method)
appMetric.Started()
next(w, r)
res := w.(negroni.ResponseWriter)
appMetric.Finished()
appMetric.StatusCode = strconv.Itoa(res.Status())
mService.SaveHTTP(appMetric)
}
}
+29
View File
@@ -0,0 +1,29 @@
package middleware
import (
"context"
"encoding/json"
"github.com/eminetto/clean-architecture-go/pkg/entity"
"net/http"
"github.com/codegangsta/negroni"
)
//Validate
func Validate(e entity.Validable) negroni.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := json.NewDecoder(r.Body).Decode(&e)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
err = e.Validate()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
ctx := context.WithValue(r.Context(), "InputParam", e)
next(w, r.WithContext(ctx))
}
}
Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"fmt"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
)
//func handleParams() (string, error) {
// if len(os.Args) < 2 {
// return "", errors.New("Invalid query")
// }
// return os.Args[1], nil
//}
func main() {
u := &user.User{
ID: entity.NewID(),
FirstName: "Elton",
}
b := &book.Book{
ID: entity.NewID(),
Title:"Book",
}
fmt.Println(u)
fmt.Println(b)
//metricService, err := metric.NewPrometheusService()
//if err != nil {
// log.Fatal(err.Error())
//}
//appMetric := metric.NewCLI("search")
//appMetric.Started()
//query, err := handleParams()
//if err != nil {
// log.Fatal(err.Error())
//}
//
//session, err := mgo.Dial(config.MONGODB_HOST)
//if err != nil {
// log.Fatal(err.Error())
//}
//defer session.Close()
//
//mPool := mgosession.NewPool(nil, session, config.MONGODB_CONNECTION_POOL)
//defer mPool.Close()
//
//bookmarkRepo := bookmark.NewMongoRepository(mPool, config.MONGODB_DATABASE)
//bookmarkService := bookmark.NewService(bookmarkRepo)
//all, err := bookmarkService.Search(query)
//if err != nil {
// log.Fatal(err)
//}
//if len(all) == 0 {
// log.Fatal(entity.ErrNotFound.Error())
//}
//for _, j := range all {
// fmt.Printf("%s %s %v \n", j.Name, j.Link, j.Tags)
//}
//appMetric.Finished()
//err = metricService.SaveCLI(appMetric)
//if err != nil {
// log.Fatal(err)
//}
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithArgs(t *testing.T) {
os.Args = []string{"search", "github"}
query, err := handleParams()
assert.Nil(t, err)
assert.Equal(t, "github", query)
}
func TestWithoutArgs(t *testing.T) {
os.Args = []string{"search"}
query, err := handleParams()
assert.NotNil(t, err)
assert.Equal(t, "Invalid query", err.Error())
assert.Equal(t, "", query)
}
+11
View File
@@ -0,0 +1,11 @@
// +build dev
package config
const (
MONGODB_HOST = "mongodb://127.0.0.1:27017"
MONGODB_DATABASE = "bookmark"
MONGODB_CONNECTION_POOL = 5
API_PORT = 8080
PROMETHEUS_PUSHGATEWAY = "http://localhost:9091/"
)
+11
View File
@@ -0,0 +1,11 @@
// +build prod
package config
const (
MONGODB_HOST = "mongodb://127.0.0.1:27017"
MONGODB_DATABASE = "bookmark"
MONGODB_CONNECTION_POOL = 50
API_PORT = 8080
PROMETHEUS_PUSHGATEWAY = "http://localhost:9091/"
)
+11
View File
@@ -0,0 +1,11 @@
// +build staging
package config
const (
MONGODB_HOST = "mongodb://127.0.0.1:27017"
MONGODB_DATABASE = "bookmark"
MONGODB_CONNECTION_POOL = 5
API_PORT = 8080
PROMETHEUS_PUSHGATEWAY = "http://localhost:9091/"
)
+11
View File
@@ -0,0 +1,11 @@
// +build testing
package config
const (
MONGODB_HOST = "mongodb://127.0.0.1:27017"
MONGODB_DATABASE = "bookmark"
MONGODB_CONNECTION_POOL = 5
API_PORT = 8080
PROMETHEUS_PUSHGATEWAY = "http://localhost:9091/"
)
+40
View File
@@ -0,0 +1,40 @@
version: "3"
services:
mongodb:
image: mongo
ports:
- "27017:27017"
container_name: bookmark-mongodb
network_mode: "bridge"
node:
image: node:8-alpine
network_mode: "bridge"
volumes:
- ./web:/web
- /tmp:/tmp
grafana:
image: grafana/grafana
ports:
- "3000:3000"
container_name: bookmark-grafana
network_mode: "bridge"
depends_on:
- prometheus
- prometheus-pushgateway
prometheus:
image: prom/prometheus
ports:
- 9090:9090
command:
- --config.file=/etc/prometheus/prometheus.yml
volumes:
- ./infra/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
container_name: bookmark-prometheus
network_mode: "bridge"
prometheus-pushgateway:
image: prom/pushgateway
container_name: bookmark-pushgateway
expose:
- 9091
ports:
- "9091:9091"
+18
View File
@@ -0,0 +1,18 @@
package book
import (
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//Book data
type Book struct {
ID entity.ID
Title string
Author string
Pages int
Quantity int
CreatedAt time.Time
UpdatedAt time.Time
}
+18
View File
@@ -0,0 +1,18 @@
package book
import (
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
func NewFixtureBook() *Book {
return &Book{
ID: entity.NewID(),
Title: "I Am Ozzy",
Author: "Ozzy Osbourne",
Pages: 294,
Quantity: 1,
CreatedAt: time.Now(),
}
}
+29
View File
@@ -0,0 +1,29 @@
package book
import "github.com/eminetto/clean-architecture-go-v2/domain/entity"
//Reader interface
type Reader interface {
Get(id entity.ID) (*Book, error)
Search(query string) ([]*Book, error)
List() ([]*Book, error)
}
//Writer book writer
type Writer interface {
Create(e *Book) (entity.ID, error)
Update(e *Book) error
Delete(id entity.ID) error
}
//Repository repository interface
type Repository interface {
Reader
Writer
}
//UseCase use case interface
type UseCase interface {
Reader
Writer
}
+368
View File
@@ -0,0 +1,368 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: domain/entity/book/interface.go
// Package mock is a generated GoMock package.
package mock
import (
entity "github.com/eminetto/clean-architecture-go-v2/domain/entity"
book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockReader is a mock of Reader interface
type MockReader struct {
ctrl *gomock.Controller
recorder *MockReaderMockRecorder
}
// MockReaderMockRecorder is the mock recorder for MockReader
type MockReaderMockRecorder struct {
mock *MockReader
}
// NewMockReader creates a new mock instance
func NewMockReader(ctrl *gomock.Controller) *MockReader {
mock := &MockReader{ctrl: ctrl}
mock.recorder = &MockReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockReader) EXPECT() *MockReaderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockReader) Get(id entity.ID) (*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockReader)(nil).Get), id)
}
// Search mocks base method
func (m *MockReader) Search(query string) ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockReader)(nil).Search), query)
}
// List mocks base method
func (m *MockReader) List() ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockReaderMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockReader)(nil).List))
}
// MockWriter is a mock of Writer interface
type MockWriter struct {
ctrl *gomock.Controller
recorder *MockWriterMockRecorder
}
// MockWriterMockRecorder is the mock recorder for MockWriter
type MockWriterMockRecorder struct {
mock *MockWriter
}
// NewMockWriter creates a new mock instance
func NewMockWriter(ctrl *gomock.Controller) *MockWriter {
mock := &MockWriter{ctrl: ctrl}
mock.recorder = &MockWriterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
return m.recorder
}
// Create mocks base method
func (m *MockWriter) Create(e *book.Book) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockWriter)(nil).Create), e)
}
// Update mocks base method
func (m *MockWriter) Update(e *book.Book) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockWriterMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWriter)(nil).Update), e)
}
// Delete mocks base method
func (m *MockWriter) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockWriterMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWriter)(nil).Delete), id)
}
// MockRepository is a mock of Repository interface
type MockRepository struct {
ctrl *gomock.Controller
recorder *MockRepositoryMockRecorder
}
// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
mock *MockRepository
}
// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
mock := &MockRepository{ctrl: ctrl}
mock.recorder = &MockRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockRepository) Get(id entity.ID) (*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRepository)(nil).Get), id)
}
// Search mocks base method
func (m *MockRepository) Search(query string) ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockRepository)(nil).Search), query)
}
// List mocks base method
func (m *MockRepository) List() ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockRepositoryMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRepository)(nil).List))
}
// Create mocks base method
func (m *MockRepository) Create(e *book.Book) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), e)
}
// Update mocks base method
func (m *MockRepository) Update(e *book.Book) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockRepositoryMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), e)
}
// Delete mocks base method
func (m *MockRepository) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockRepositoryMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id)
}
// MockUseCase is a mock of UseCase interface
type MockUseCase struct {
ctrl *gomock.Controller
recorder *MockUseCaseMockRecorder
}
// MockUseCaseMockRecorder is the mock recorder for MockUseCase
type MockUseCaseMockRecorder struct {
mock *MockUseCase
}
// NewMockUseCase creates a new mock instance
func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase {
mock := &MockUseCase{ctrl: ctrl}
mock.recorder = &MockUseCaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockUseCase) Get(id entity.ID) (*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockUseCaseMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUseCase)(nil).Get), id)
}
// Search mocks base method
func (m *MockUseCase) Search(query string) ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockUseCaseMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockUseCase)(nil).Search), query)
}
// List mocks base method
func (m *MockUseCase) List() ([]*book.Book, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*book.Book)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockUseCaseMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUseCase)(nil).List))
}
// Create mocks base method
func (m *MockUseCase) Create(e *book.Book) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockUseCaseMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUseCase)(nil).Create), e)
}
// Update mocks base method
func (m *MockUseCase) Update(e *book.Book) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockUseCaseMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUseCase)(nil).Update), e)
}
// Delete mocks base method
func (m *MockUseCase) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockUseCaseMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUseCase)(nil).Delete), id)
}
+78
View File
@@ -0,0 +1,78 @@
package book
import (
"strings"
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//IRepo in memory repo
type IRepo struct {
m map[entity.ID]*Book
}
//NewInmemRepository create new repository
func NewInmemRepository() *IRepo {
var m = map[entity.ID]*Book{}
return &IRepo{
m: m,
}
}
//Create a book
func (r *IRepo) Create(e *Book) (entity.ID, error) {
r.m[e.ID] = e
return e.ID, nil
}
//Get a book
func (r *IRepo) Get(id entity.ID) (*Book, error) {
if r.m[id] == nil {
return nil, domain.ErrNotFound
}
return r.m[id], nil
}
//Update a book
func (r *IRepo) Update(e *Book) error {
_, err := r.Get(e.ID)
if err != nil {
return err
}
r.m[e.ID] = e
return nil
}
//Search books
func (r *IRepo) Search(query string) ([]*Book, error) {
var d []*Book
for _, j := range r.m {
if strings.Contains(strings.ToLower(j.Title), query) {
d = append(d, j)
}
}
if len(d) == 0 {
return nil, domain.ErrNotFound
}
return d, nil
}
//List books
func (r *IRepo) List() ([]*Book, error) {
var d []*Book
for _, j := range r.m {
d = append(d, j)
}
return d, nil
}
//Delete a book
func (r *IRepo) Delete(id entity.ID) error {
if r.m[id] == nil {
return domain.ErrNotFound
}
r.m[id] = nil
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package book
import (
"strings"
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//Service service interface
type Service struct {
repo Repository
}
//NewService create new use case
func NewService(r Repository) *Service {
return &Service{
repo: r,
}
}
//Create a book
func (s *Service) Create(e *Book) (entity.ID, error) {
e.ID = entity.NewID()
e.CreatedAt = time.Now()
return s.repo.Create(e)
}
//Get a book
func (s *Service) Get(id entity.ID) (*Book, error) {
return s.repo.Get(id)
}
//Search books
func (s *Service) Search(query string) ([]*Book, error) {
return s.repo.Search(strings.ToLower(query))
}
//List books
func (s *Service) List() ([]*Book, error) {
return s.repo.List()
}
//Delete a book
func (s *Service) Delete(id entity.ID) error {
_, err := s.Get(id)
if err != nil {
return err
}
return s.repo.Delete(id)
}
//Update a book
func (s *Service) Update(e *Book) error {
return s.repo.Update(e)
}
+81
View File
@@ -0,0 +1,81 @@
package book
import (
"github.com/eminetto/clean-architecture-go-v2/domain"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Create(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u := NewFixtureBook()
id, err := service.Create(u)
assert.Nil(t, err)
assert.Equal(t, u.ID, id)
assert.False(t, u.CreatedAt.IsZero())
}
func Test_SearchAndFind(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u1 := NewFixtureBook()
u2 := NewFixtureBook()
u2.Title = "Lemmy: Biography"
uID, _ := service.Create(u1)
_, _ = service.Create(u2)
t.Run("search", func(t *testing.T) {
c, err := service.Search("ozzy")
assert.Nil(t, err)
assert.Equal(t, 1, len(c))
assert.Equal(t, "I Am Ozzy", c[0].Title)
c, err = service.Search("dio")
assert.Equal(t, domain.ErrNotFound, err)
assert.Nil(t, c)
})
t.Run("list all", func(t *testing.T) {
all, err := service.List()
assert.Nil(t, err)
assert.Equal(t, 2, len(all))
})
t.Run("get", func(t *testing.T) {
saved, err := service.Get(uID)
assert.Nil(t, err)
assert.Equal(t, u1.Title, saved.Title)
})
}
func Test_Update(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u := NewFixtureBook()
id, err := service.Create(u)
assert.Nil(t, err)
saved, _ := service.Get(id)
saved.Title = "Lemmy: Biography"
assert.Nil(t, service.Update(saved))
updated, err := service.Get(id)
assert.Nil(t, err)
assert.Equal(t, "Lemmy: Biography", updated.Title)
}
func TestDelete(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u1 := NewFixtureBook()
u2 := NewFixtureBook()
u2ID, _ := service.Create(u2)
err := service.Delete(u1.ID)
assert.Equal(t, domain.ErrNotFound, err)
err = service.Delete(u2ID)
assert.Nil(t, err)
_, err = service.Get(u2ID)
assert.Equal(t, domain.ErrNotFound, err)
}
+16
View File
@@ -0,0 +1,16 @@
package entity
import "github.com/google/uuid"
type ID = uuid.UUID
func NewID() ID {
return ID(uuid.New())
}
func StringToID(s string) (ID, error) {
id, err := uuid.Parse(s)
return ID(id), err
}
+16
View File
@@ -0,0 +1,16 @@
package user
import (
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
"time"
)
func NewFixtureUser() *User {
return &User{
ID: entity.NewID(),
Email:"ozzy@metalgods.net",
FirstName:"Ozzy",
LastName: "Osbourne",
CreatedAt: time.Now(),
}
}
+29
View File
@@ -0,0 +1,29 @@
package user
import "github.com/eminetto/clean-architecture-go-v2/domain/entity"
//Reader interface
type Reader interface {
Get(id entity.ID) (*User, error)
Search(query string) ([]*User, error)
List() ([]*User, error)
}
//Writer user writer
type Writer interface {
Create(e *User) (entity.ID, error)
Update(e *User) error
Delete(id entity.ID) error
}
//Repository repository interface
type Repository interface {
Reader
Writer
}
//UseCase use case interface
type UseCase interface {
Reader
Writer
}
+368
View File
@@ -0,0 +1,368 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: domain/entity/user/interface.go
// Package mock is a generated GoMock package.
package mock
import (
entity "github.com/eminetto/clean-architecture-go-v2/domain/entity"
user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockReader is a mock of Reader interface
type MockReader struct {
ctrl *gomock.Controller
recorder *MockReaderMockRecorder
}
// MockReaderMockRecorder is the mock recorder for MockReader
type MockReaderMockRecorder struct {
mock *MockReader
}
// NewMockReader creates a new mock instance
func NewMockReader(ctrl *gomock.Controller) *MockReader {
mock := &MockReader{ctrl: ctrl}
mock.recorder = &MockReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockReader) EXPECT() *MockReaderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockReader) Get(id entity.ID) (*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockReader)(nil).Get), id)
}
// Search mocks base method
func (m *MockReader) Search(query string) ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockReader)(nil).Search), query)
}
// List mocks base method
func (m *MockReader) List() ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockReaderMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockReader)(nil).List))
}
// MockWriter is a mock of Writer interface
type MockWriter struct {
ctrl *gomock.Controller
recorder *MockWriterMockRecorder
}
// MockWriterMockRecorder is the mock recorder for MockWriter
type MockWriterMockRecorder struct {
mock *MockWriter
}
// NewMockWriter creates a new mock instance
func NewMockWriter(ctrl *gomock.Controller) *MockWriter {
mock := &MockWriter{ctrl: ctrl}
mock.recorder = &MockWriterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
return m.recorder
}
// Create mocks base method
func (m *MockWriter) Create(e *user.User) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockWriter)(nil).Create), e)
}
// Update mocks base method
func (m *MockWriter) Update(e *user.User) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockWriterMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockWriter)(nil).Update), e)
}
// Delete mocks base method
func (m *MockWriter) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockWriterMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockWriter)(nil).Delete), id)
}
// MockRepository is a mock of Repository interface
type MockRepository struct {
ctrl *gomock.Controller
recorder *MockRepositoryMockRecorder
}
// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
mock *MockRepository
}
// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
mock := &MockRepository{ctrl: ctrl}
mock.recorder = &MockRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockRepository) Get(id entity.ID) (*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRepository)(nil).Get), id)
}
// Search mocks base method
func (m *MockRepository) Search(query string) ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockRepository)(nil).Search), query)
}
// List mocks base method
func (m *MockRepository) List() ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockRepositoryMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRepository)(nil).List))
}
// Create mocks base method
func (m *MockRepository) Create(e *user.User) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), e)
}
// Update mocks base method
func (m *MockRepository) Update(e *user.User) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockRepositoryMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), e)
}
// Delete mocks base method
func (m *MockRepository) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockRepositoryMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id)
}
// MockUseCase is a mock of UseCase interface
type MockUseCase struct {
ctrl *gomock.Controller
recorder *MockUseCaseMockRecorder
}
// MockUseCaseMockRecorder is the mock recorder for MockUseCase
type MockUseCaseMockRecorder struct {
mock *MockUseCase
}
// NewMockUseCase creates a new mock instance
func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase {
mock := &MockUseCase{ctrl: ctrl}
mock.recorder = &MockUseCaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockUseCase) Get(id entity.ID) (*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get
func (mr *MockUseCaseMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUseCase)(nil).Get), id)
}
// Search mocks base method
func (m *MockUseCase) Search(query string) ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockUseCaseMockRecorder) Search(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockUseCase)(nil).Search), query)
}
// List mocks base method
func (m *MockUseCase) List() ([]*user.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*user.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockUseCaseMockRecorder) List() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUseCase)(nil).List))
}
// Create mocks base method
func (m *MockUseCase) Create(e *user.User) (entity.ID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create
func (mr *MockUseCaseMockRecorder) Create(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUseCase)(nil).Create), e)
}
// Update mocks base method
func (m *MockUseCase) Update(e *user.User) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockUseCaseMockRecorder) Update(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUseCase)(nil).Update), e)
}
// Delete mocks base method
func (m *MockUseCase) Delete(id entity.ID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockUseCaseMockRecorder) Delete(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUseCase)(nil).Delete), id)
}
+78
View File
@@ -0,0 +1,78 @@
package user
import (
"strings"
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//IRepo in memory repo
type IRepo struct {
m map[entity.ID]*User
}
//NewInmemRepository create new repository
func NewInmemRepository() *IRepo {
var m = map[entity.ID]*User{}
return &IRepo{
m: m,
}
}
//Create an user
func (r *IRepo) Create(e *User) (entity.ID, error) {
r.m[e.ID] = e
return e.ID, nil
}
//Get an user
func (r *IRepo) Get(id entity.ID) (*User, error) {
if r.m[id] == nil {
return nil, domain.ErrNotFound
}
return r.m[id], nil
}
//Update an user
func (r *IRepo) Update(e *User) error {
_, err := r.Get(e.ID)
if err != nil {
return err
}
r.m[e.ID] = e
return nil
}
//Search users
func (r *IRepo) Search(query string) ([]*User, error) {
var d []*User
for _, j := range r.m {
if strings.Contains(strings.ToLower(j.FirstName), query) {
d = append(d, j)
}
}
if len(d) == 0 {
return nil, domain.ErrNotFound
}
return d, nil
}
//List users
func (r *IRepo) List() ([]*User, error) {
var d []*User
for _, j := range r.m {
d = append(d, j)
}
return d, nil
}
//Delete an user
func (r *IRepo) Delete(id entity.ID) error {
if r.m[id] == nil {
return domain.ErrNotFound
}
r.m[id] = nil
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package user
import (
"strings"
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//Service service interface
type Service struct {
repo Repository
}
//NewService create new use case
func NewService(r Repository) *Service {
return &Service{
repo: r,
}
}
//Create an user
func (s *Service) Create(e *User) (entity.ID, error) {
e.ID = entity.NewID()
e.CreatedAt = time.Now()
return s.repo.Create(e)
}
//Get an user
func (s *Service) Get(id entity.ID) (*User, error) {
return s.repo.Get(id)
}
//Search users
func (s *Service) Search(query string) ([]*User, error) {
return s.repo.Search(strings.ToLower(query))
}
//List users
func (s *Service) List() ([]*User, error) {
return s.repo.List()
}
//Delete an user
func (s *Service) Delete(id entity.ID) error {
_, err := s.Get(id)
if err != nil {
return err
}
return s.repo.Delete(id)
}
//Update an user
func (s *Service) Update(e *User) error {
e.UpdatedAt = time.Now()
return s.repo.Update(e)
}
+87
View File
@@ -0,0 +1,87 @@
package user
import (
"testing"
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
"github.com/stretchr/testify/assert"
)
func Test_Create(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u := NewFixtureUser()
id, err := service.Create(u)
assert.Nil(t, err)
assert.Equal(t, u.ID, id)
assert.False(t, u.CreatedAt.IsZero())
assert.True(t, u.UpdatedAt.IsZero())
}
func Test_SearchAndFind(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u1 := NewFixtureUser()
u2 := NewFixtureUser()
u2.FirstName = "Lemmy"
uID, _ := service.Create(u1)
_, _ = service.Create(u2)
t.Run("search", func(t *testing.T) {
c, err := service.Search("ozzy")
assert.Nil(t, err)
assert.Equal(t, 1, len(c))
assert.Equal(t, "Osbourne", c[0].LastName)
c, err = service.Search("dio")
assert.Equal(t, domain.ErrNotFound, err)
assert.Nil(t, c)
})
t.Run("list all", func(t *testing.T) {
all, err := service.List()
assert.Nil(t, err)
assert.Equal(t, 2, len(all))
})
t.Run("get", func(t *testing.T) {
saved, err := service.Get(uID)
assert.Nil(t, err)
assert.Equal(t, u1.FirstName, saved.FirstName)
})
}
func Test_Update(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u := NewFixtureUser()
id, err := service.Create(u)
assert.Nil(t, err)
saved, _ := service.Get(id)
saved.FirstName = "Dio"
saved.Books = append(saved.Books, entity.NewID())
assert.Nil(t, service.Update(saved))
updated, err := service.Get(id)
assert.Nil(t, err)
assert.Equal(t, "Dio", updated.FirstName)
assert.False(t, u.UpdatedAt.IsZero())
assert.Equal(t, 1, len(updated.Books))
}
func TestDelete(t *testing.T) {
repo := NewInmemRepository()
service := NewService(repo)
u1 := NewFixtureUser()
u2 := NewFixtureUser()
u2ID, _ := service.Create(u2)
err := service.Delete(u1.ID)
assert.Equal(t, domain.ErrNotFound, err)
err = service.Delete(u2ID)
assert.Nil(t, err)
_, err = service.Get(u2ID)
assert.Equal(t, domain.ErrNotFound, err)
}
+19
View File
@@ -0,0 +1,19 @@
package user
import (
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
)
//User data
type User struct {
ID entity.ID
Email string
Password string
FirstName string
LastName string
CreatedAt time.Time
UpdatedAt time.Time
Books []entity.ID
}
+18
View File
@@ -0,0 +1,18 @@
package domain
import "errors"
//ErrNotFound not found
var ErrNotFound = errors.New("Not found")
//ErrCannotBeDeleted cannot be deleted
var ErrCannotBeDeleted = errors.New("Cannot Be Deleted")
//ErrNotEnoughBooks cannot borrow
var ErrNotEnoughBooks = errors.New("Not enough books")
//ErrBookAlreadyBorrowed cannot borrow
var ErrBookAlreadyBorrowed = errors.New("Book already borrowed")
//ErrBookNotBorrowed cannot return
var ErrBookNotBorrowed = errors.New("Book not borrowed")
+12
View File
@@ -0,0 +1,12 @@
package loan
import (
"github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
)
//UseCase use case interface
type UseCase interface {
Borrow(u *user.User, b *book.Book) error
Return(b *book.Book) error
}
+63
View File
@@ -0,0 +1,63 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: domain/loan/interface.go
// Package mock is a generated GoMock package.
package mock
import (
book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockUseCase is a mock of UseCase interface
type MockUseCase struct {
ctrl *gomock.Controller
recorder *MockUseCaseMockRecorder
}
// MockUseCaseMockRecorder is the mock recorder for MockUseCase
type MockUseCaseMockRecorder struct {
mock *MockUseCase
}
// NewMockUseCase creates a new mock instance
func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase {
mock := &MockUseCase{ctrl: ctrl}
mock.recorder = &MockUseCaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder {
return m.recorder
}
// Borrow mocks base method
func (m *MockUseCase) Borrow(u *user.User, b *book.Book) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Borrow", u, b)
ret0, _ := ret[0].(error)
return ret0
}
// Borrow indicates an expected call of Borrow
func (mr *MockUseCaseMockRecorder) Borrow(u, b interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Borrow", reflect.TypeOf((*MockUseCase)(nil).Borrow), u, b)
}
// Return mocks base method
func (m *MockUseCase) Return(b *book.Book) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Return", b)
ret0, _ := ret[0].(error)
return ret0
}
// Return indicates an expected call of Return
func (mr *MockUseCaseMockRecorder) Return(b interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Return", reflect.TypeOf((*MockUseCase)(nil).Return), b)
}
+101
View File
@@ -0,0 +1,101 @@
package loan
import (
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
)
//Service service interface
type Service struct {
uService user.UseCase
bService book.UseCase
}
//NewService create new use case
func NewService(u user.UseCase, b book.UseCase) *Service {
return &Service{
uService: u,
bService: b,
}
}
//Borrow borrow a book to an user
func (s *Service) Borrow(u *user.User, b *book.Book) error {
u, err := s.uService.Get(u.ID)
if err != nil {
return err
}
b, err = s.bService.Get(b.ID)
if err != nil {
return err
}
if b.Quantity <= 0 {
return domain.ErrNotEnoughBooks
}
for _, v := range u.Books {
if v == b.ID {
return domain.ErrBookAlreadyBorrowed
}
}
u.Books = append(u.Books, b.ID)
err = s.uService.Update(u)
if err != nil {
return err
}
b.Quantity--
err = s.bService.Update(b)
if err != nil {
return err
}
return nil
}
//Return return a book
func (s *Service) Return(b *book.Book) error {
b, err := s.bService.Get(b.ID)
if err != nil {
return err
}
all, err := s.uService.List()
if err != nil {
return err
}
borrowed := false
var borrowedBy entity.ID
for _, u := range all {
for _, bookID := range u.Books {
if bookID == b.ID {
borrowed = true
borrowedBy = u.ID
break
}
}
}
if !borrowed {
return domain.ErrBookNotBorrowed
}
u, err := s.uService.Get(borrowedBy)
if err != nil {
return err
}
for i, j := range u.Books {
if j == b.ID {
u.Books = append(u.Books[:i], u.Books[i+1:]...)
err = s.uService.Update(u)
if err != nil {
return err
}
break
}
}
b.Quantity++
err = s.bService.Update(b)
if err != nil {
return err
}
return nil
}
+101
View File
@@ -0,0 +1,101 @@
package loan
import (
"testing"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
bmock "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
umock "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func Test_Borrow(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
uMock := umock.NewMockUseCase(controller)
bMock := bmock.NewMockUseCase(controller)
service := NewService(uMock, bMock)
t.Run("user not found", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
uMock.EXPECT().Get(u.ID).Return(nil, domain.ErrNotFound)
err := service.Borrow(u, b)
assert.Equal(t, domain.ErrNotFound, err)
})
t.Run("book not found", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
uMock.EXPECT().Get(u.ID).Return(u, nil)
bMock.EXPECT().Get(b.ID).Return(nil, domain.ErrNotFound)
err := service.Borrow(u, b)
assert.Equal(t, domain.ErrNotFound, err)
})
t.Run("not enough books to borrow", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
b.Quantity = 0
uMock.EXPECT().Get(u.ID).Return(u, nil)
bMock.EXPECT().Get(b.ID).Return(b, nil)
err := service.Borrow(u, b)
assert.Equal(t, domain.ErrNotEnoughBooks, err)
})
t.Run("book already borrowed", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
u.Books = []entity.ID{b.ID}
b.Quantity = 1
uMock.EXPECT().Get(u.ID).Return(u, nil)
bMock.EXPECT().Get(b.ID).Return(b, nil)
err := service.Borrow(u, b)
assert.Equal(t, domain.ErrBookAlreadyBorrowed, err)
})
t.Run("sucess", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
uMock.EXPECT().Get(u.ID).Return(u, nil)
bMock.EXPECT().Get(b.ID).Return(b, nil)
uMock.EXPECT().Update(u).Return(nil)
bMock.EXPECT().Update(b).Return(nil)
err := service.Borrow(u, b)
assert.Nil(t, err)
})
}
func Test_Return(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
uMock := umock.NewMockUseCase(controller)
bMock := bmock.NewMockUseCase(controller)
service := NewService(uMock, bMock)
t.Run("book not found", func(t *testing.T) {
b := book.NewFixtureBook()
bMock.EXPECT().Get(b.ID).Return(nil, domain.ErrNotFound)
err := service.Return(b)
assert.Equal(t, domain.ErrNotFound, err)
})
t.Run("book not borrowed", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
bMock.EXPECT().Get(b.ID).Return(b, nil)
uMock.EXPECT().List().Return([]*user.User{u}, nil)
err := service.Return(b)
assert.Equal(t, domain.ErrBookNotBorrowed, err)
})
t.Run("success", func(t *testing.T) {
u := user.NewFixtureUser()
b := book.NewFixtureBook()
u.Books = []entity.ID{b.ID}
bMock.EXPECT().Get(b.ID).Return(b, nil)
uMock.EXPECT().Get(u.ID).Return(u, nil)
uMock.EXPECT().List().Return([]*user.User{u}, nil)
uMock.EXPECT().Update(u).Return(nil)
bMock.EXPECT().Update(b).Return(nil)
err := service.Return(b)
assert.Nil(t, err)
})
}
+9
View File
@@ -0,0 +1,9 @@
module github.com/eminetto/clean-architecture-go-v2
go 1.14
require (
github.com/golang/mock v1.4.3
github.com/google/uuid v1.1.1
github.com/stretchr/testify v1.6.1
)
+24
View File
@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+26
View File
@@ -0,0 +1,26 @@
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
- job_name: bookmark
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:8080']
- job_name: pushgateway
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:9091']
+64
View File
@@ -0,0 +1,64 @@
package metric
import "time"
//CLI define a CLI app
type CLI struct {
Name string
StartedAt time.Time
FinishedAt time.Time
Duration float64
}
// NewCLI create a new CLI app
func NewCLI(name string) *CLI {
return &CLI{
Name: name,
}
}
//Started start monitoring the app
func (c *CLI) Started() {
c.StartedAt = time.Now()
}
// Finished app finished
func (c *CLI) Finished() {
c.FinishedAt = time.Now()
c.Duration = time.Since(c.StartedAt).Seconds()
}
//HTTP application
type HTTP struct {
Handler string
Method string
StatusCode string
StartedAt time.Time
FinishedAt time.Time
Duration float64
}
//NewHTTP create a new HTTP app
func NewHTTP(handler string, method string) *HTTP {
return &HTTP{
Handler: handler,
Method: method,
}
}
//Started start monitoring the app
func (h *HTTP) Started() {
h.StartedAt = time.Now()
}
// Finished app finished
func (h *HTTP) Finished() {
h.FinishedAt = time.Now()
h.Duration = time.Since(h.StartedAt).Seconds()
}
//UseCase definition
type UseCase interface {
SaveCLI(c *CLI) error
SaveHTTP(h *HTTP)
}
+55
View File
@@ -0,0 +1,55 @@
package metric
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/push"
"github.com/eminetto/clean-architecture-go/config"
)
//Service implements UseCase interface
type Service struct {
pHistogram *prometheus.HistogramVec
httpRequestHistogram *prometheus.HistogramVec
}
//NewPrometheusService create a new prometheus service
func NewPrometheusService() (*Service, error) {
cli := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "pushgateway",
Name: "cmd_duration_seconds",
Help: "CLI application execution in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"name"})
http := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "http",
Name: "request_duration_seconds",
Help: "The latency of the HTTP requests.",
Buckets: prometheus.DefBuckets,
}, []string{"handler", "method", "code"})
s := &Service{
pHistogram: cli,
httpRequestHistogram: http,
}
err := prometheus.Register(s.pHistogram)
if err != nil && err.Error() != "duplicate metrics collector registration attempted" {
return nil, err
}
err = prometheus.Register(s.httpRequestHistogram)
if err != nil && err.Error() != "duplicate metrics collector registration attempted" {
return nil, err
}
return s, nil
}
//SaveCLI send metrics to server
func (s *Service) SaveCLI(c *CLI) error {
gatewayURL := config.PROMETHEUS_PUSHGATEWAY
s.pHistogram.WithLabelValues(c.Name).Observe(c.Duration)
return push.New(gatewayURL, "cmd_job").Collector(s.pHistogram).Push()
}
//SaveHTTP send metrics to server
func (s *Service) SaveHTTP(h *HTTP) {
s.httpRequestHistogram.WithLabelValues(h.Handler, h.Method, h.StatusCode).Observe(h.Duration)
}