commit 3db4550ee07cf0e9df41c65b558fcaebb72d03c6 Author: Elton Minetto Date: Thu Jun 25 11:46:45 2020 -0300 feat: domain finished diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -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/ diff --git a/.idea/clean-architecture-go-v2.iml b/.idea/clean-architecture-go-v2.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/clean-architecture-go-v2.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c6f90e2 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..d3ea937 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..18e4e69 --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0d9153 --- /dev/null +++ b/README.md @@ -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 +``` \ No newline at end of file diff --git a/api/handler/bookmark.go b/api/handler/bookmark.go new file mode 100644 index 0000000..4a155a4 --- /dev/null +++ b/api/handler/bookmark.go @@ -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") +} diff --git a/api/handler/bookmark_test.go b/api/handler/bookmark_test.go new file mode 100644 index 0000000..fab4679 --- /dev/null +++ b/api/handler/bookmark_test.go @@ -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) +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..5b69a8f --- /dev/null +++ b/api/main.go @@ -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()) + } +} diff --git a/api/middleware/cors.go b/api/middleware/cors.go new file mode 100644 index 0000000..7a4f6b8 --- /dev/null +++ b/api/middleware/cors.go @@ -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) +} diff --git a/api/middleware/metrics.go b/api/middleware/metrics.go new file mode 100755 index 0000000..03bb8ca --- /dev/null +++ b/api/middleware/metrics.go @@ -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) + } +} diff --git a/api/middleware/validate.go b/api/middleware/validate.go new file mode 100644 index 0000000..8e24703 --- /dev/null +++ b/api/middleware/validate.go @@ -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)) + } +} diff --git a/bin/api b/bin/api new file mode 100755 index 0000000..630e9d4 Binary files /dev/null and b/bin/api differ diff --git a/bin/search b/bin/search new file mode 100755 index 0000000..6953cae Binary files /dev/null and b/bin/search differ diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..59143ad --- /dev/null +++ b/cmd/main.go @@ -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) + //} +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..278103e --- /dev/null +++ b/cmd/main_test.go @@ -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) +} diff --git a/config/config_dev.go b/config/config_dev.go new file mode 100644 index 0000000..ffac8a2 --- /dev/null +++ b/config/config_dev.go @@ -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/" +) diff --git a/config/config_prod.go b/config/config_prod.go new file mode 100644 index 0000000..2afa50e --- /dev/null +++ b/config/config_prod.go @@ -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/" +) diff --git a/config/config_staging.go b/config/config_staging.go new file mode 100644 index 0000000..65991a1 --- /dev/null +++ b/config/config_staging.go @@ -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/" +) diff --git a/config/config_testing.go b/config/config_testing.go new file mode 100644 index 0000000..2ea19df --- /dev/null +++ b/config/config_testing.go @@ -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/" +) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..00513ec --- /dev/null +++ b/docker-compose.yml @@ -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" \ No newline at end of file diff --git a/domain/entity/book/book.go b/domain/entity/book/book.go new file mode 100644 index 0000000..cd6a575 --- /dev/null +++ b/domain/entity/book/book.go @@ -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 +} diff --git a/domain/entity/book/fixture.go b/domain/entity/book/fixture.go new file mode 100644 index 0000000..97a3bca --- /dev/null +++ b/domain/entity/book/fixture.go @@ -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(), + } +} diff --git a/domain/entity/book/interface.go b/domain/entity/book/interface.go new file mode 100644 index 0000000..2496a28 --- /dev/null +++ b/domain/entity/book/interface.go @@ -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 +} \ No newline at end of file diff --git a/domain/entity/book/mock/book.go b/domain/entity/book/mock/book.go new file mode 100644 index 0000000..335a121 --- /dev/null +++ b/domain/entity/book/mock/book.go @@ -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) +} diff --git a/domain/entity/book/repository_inmem.go b/domain/entity/book/repository_inmem.go new file mode 100644 index 0000000..8e5a788 --- /dev/null +++ b/domain/entity/book/repository_inmem.go @@ -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 +} diff --git a/domain/entity/book/service.go b/domain/entity/book/service.go new file mode 100644 index 0000000..682c105 --- /dev/null +++ b/domain/entity/book/service.go @@ -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) +} diff --git a/domain/entity/book/service_test.go b/domain/entity/book/service_test.go new file mode 100644 index 0000000..e73ec36 --- /dev/null +++ b/domain/entity/book/service_test.go @@ -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) +} diff --git a/domain/entity/entity.go b/domain/entity/entity.go new file mode 100644 index 0000000..714caf8 --- /dev/null +++ b/domain/entity/entity.go @@ -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 +} + + diff --git a/domain/entity/user/fixture.go b/domain/entity/user/fixture.go new file mode 100644 index 0000000..8ddb1fb --- /dev/null +++ b/domain/entity/user/fixture.go @@ -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(), + } +} \ No newline at end of file diff --git a/domain/entity/user/interface.go b/domain/entity/user/interface.go new file mode 100644 index 0000000..eb5f309 --- /dev/null +++ b/domain/entity/user/interface.go @@ -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 +} \ No newline at end of file diff --git a/domain/entity/user/mock/user.go b/domain/entity/user/mock/user.go new file mode 100644 index 0000000..b8aba8d --- /dev/null +++ b/domain/entity/user/mock/user.go @@ -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) +} diff --git a/domain/entity/user/repository_inmem.go b/domain/entity/user/repository_inmem.go new file mode 100644 index 0000000..977689e --- /dev/null +++ b/domain/entity/user/repository_inmem.go @@ -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 +} diff --git a/domain/entity/user/service.go b/domain/entity/user/service.go new file mode 100644 index 0000000..e6a2e2a --- /dev/null +++ b/domain/entity/user/service.go @@ -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) +} diff --git a/domain/entity/user/service_test.go b/domain/entity/user/service_test.go new file mode 100644 index 0000000..d2ed57e --- /dev/null +++ b/domain/entity/user/service_test.go @@ -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) +} diff --git a/domain/entity/user/user.go b/domain/entity/user/user.go new file mode 100644 index 0000000..5183b01 --- /dev/null +++ b/domain/entity/user/user.go @@ -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 +} diff --git a/domain/error.go b/domain/error.go new file mode 100644 index 0000000..e235ef3 --- /dev/null +++ b/domain/error.go @@ -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") diff --git a/domain/loan/interface.go b/domain/loan/interface.go new file mode 100644 index 0000000..1ad50b2 --- /dev/null +++ b/domain/loan/interface.go @@ -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 +} diff --git a/domain/loan/mock/loan.go b/domain/loan/mock/loan.go new file mode 100644 index 0000000..8369c26 --- /dev/null +++ b/domain/loan/mock/loan.go @@ -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) +} diff --git a/domain/loan/service.go b/domain/loan/service.go new file mode 100644 index 0000000..6c15d26 --- /dev/null +++ b/domain/loan/service.go @@ -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 +} diff --git a/domain/loan/service_test.go b/domain/loan/service_test.go new file mode 100644 index 0000000..020c545 --- /dev/null +++ b/domain/loan/service_test.go @@ -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) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..977b9e8 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5f4cff9 --- /dev/null +++ b/go.sum @@ -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= diff --git a/infra/prometheus/prometheus.yml b/infra/prometheus/prometheus.yml new file mode 100644 index 0000000..cac9d93 --- /dev/null +++ b/infra/prometheus/prometheus.yml @@ -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'] \ No newline at end of file diff --git a/pkg/metric/interface.go b/pkg/metric/interface.go new file mode 100755 index 0000000..b270be3 --- /dev/null +++ b/pkg/metric/interface.go @@ -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) +} diff --git a/pkg/metric/prometheus.go b/pkg/metric/prometheus.go new file mode 100755 index 0000000..0694d5b --- /dev/null +++ b/pkg/metric/prometheus.go @@ -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) +}