diff --git a/README.md b/README.md index f0d9153..00e150d 100644 --- a/README.md +++ b/README.md @@ -19,42 +19,39 @@ Clean Architecture sample ## API requests -### Add a bookmark +### Add a book ``` -curl -X "POST" "http://localhost:8080/v1/bookmark" \ +curl -X "POST" "http://localhost:8080/v1/book" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d $'{ - "tags": [ - "git", - "social" - ], - "name": "Github", - "description": "Github site", - "link": "http://github.com" + "title": "I Am Ozzy", + "author": "Ozzy Osbourne", + "pages": 294, + "quantity":1 }' ``` -### Search a bookmark +### Search a book ``` -curl "http://localhost:8080/v1/bookmark?name=github" \ +curl "http://localhost:8080/v1/book?title=ozzy" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' ``` -### Show all bookmarks +### Show all books ``` -curl "http://localhost:8080/v1/bookmark" \ +curl "http://localhost:8080/v1/book" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' ``` ## CMD -### Search for a bookmark +### Search for a book ``` -./bin/search github +./bin/search ozzy ``` \ No newline at end of file diff --git a/api/handler/book.go b/api/handler/book.go new file mode 100644 index 0000000..71f38a2 --- /dev/null +++ b/api/handler/book.go @@ -0,0 +1,183 @@ +package handler + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/eminetto/clean-architecture-go-v2/domain" + + "github.com/eminetto/clean-architecture-go-v2/api/presenter" + + "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" + + "github.com/codegangsta/negroni" + "github.com/gorilla/mux" +) + +func listBooks(service book.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error reading books" + var data []*book.Book + var err error + title := r.URL.Query().Get("title") + switch { + case title == "": + data, err = service.List() + default: + data, err = service.Search(title) + } + w.Header().Set("Content-Type", "application/json") + if err != nil && err != domain.ErrNotFound { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + + if data == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(errorMessage)) + return + } + var toJ []*presenter.Book + for _, d := range data { + toJ = append(toJ, &presenter.Book{ + ID: d.ID, + Title: d.Title, + Author: d.Author, + Pages: d.Pages, + Quantity: d.Quantity, + }) + } + if err := json.NewEncoder(w).Encode(toJ); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + } + }) +} + +func createBook(service book.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error adding book" + var input struct { + Title string `json:"title"` + Author string `json:"author"` + Pages int `json:"pages"` + Quantity int `json:"quantity"` + } + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + b := &book.Book{ + ID: entity.NewID(), + Title: input.Title, + Author: input.Author, + Pages: input.Pages, + Quantity: input.Quantity, + CreatedAt: time.Now(), + } + b.ID, err = service.Create(b) + if err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + toJ := &presenter.Book{ + ID: b.ID, + Title: b.Title, + Author: b.Author, + Pages: b.Pages, + Quantity: b.Quantity, + } + + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(toJ); err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + }) +} + +func getBook(service book.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error reading book" + vars := mux.Vars(r) + id, err := entity.StringToID(vars["id"]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + data, err := service.Get(id) + w.Header().Set("Content-Type", "application/json") + if err != nil && err != domain.ErrNotFound { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + + if data == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(errorMessage)) + return + } + toJ := &presenter.Book{ + ID: data.ID, + Title: data.Title, + Author: data.Author, + Pages: data.Pages, + Quantity: data.Quantity, + } + if err := json.NewEncoder(w).Encode(toJ); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + } + }) +} + +func deleteBook(service book.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error removing bookmark" + vars := mux.Vars(r) + id, err := entity.StringToID(vars["id"]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + err = service.Delete(id) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + }) +} + +//MakeBookHandlers make url handlers +func MakeBookHandlers(r *mux.Router, n negroni.Negroni, service book.UseCase) { + r.Handle("/v1/book", n.With( + negroni.Wrap(listBooks(service)), + )).Methods("GET", "OPTIONS").Name("listBooks") + + r.Handle("/v1/book", n.With( + negroni.Wrap(createBook(service)), + )).Methods("POST", "OPTIONS").Name("createBook") + + r.Handle("/v1/book/{id}", n.With( + negroni.Wrap(getBook(service)), + )).Methods("GET", "OPTIONS").Name("getBook") + + r.Handle("/v1/book/{id}", n.With( + negroni.Wrap(deleteBook(service)), + )).Methods("DELETE", "OPTIONS").Name("deleteBook") +} diff --git a/api/handler/book_test.go b/api/handler/book_test.go new file mode 100644 index 0000000..b6bc12f --- /dev/null +++ b/api/handler/book_test.go @@ -0,0 +1,150 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "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/codegangsta/negroni" + "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock" + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func Test_BookIndex(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeBookHandlers(r, *n, service) + path, err := r.GetRoute("bookIndex").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/book", path) + b := book.NewFixtureBook() + service.EXPECT(). + List(). + Return([]*book.Book{b}, nil) + ts := httptest.NewServer(listBooks(service)) + defer ts.Close() + res, err := http.Get(ts.URL) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func Test_BookIndexNotFound(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + ts := httptest.NewServer(listBooks(service)) + defer ts.Close() + service.EXPECT(). + Search("book of books"). + Return(nil, domain.ErrNotFound) + res, err := http.Get(ts.URL + "?title=book+of+books") + assert.Nil(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) +} + +func Test_BookSearch(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + b := book.NewFixtureBook() + service.EXPECT(). + Search("ozzy"). + Return([]*book.Book{b}, nil) + ts := httptest.NewServer(listBooks(service)) + defer ts.Close() + res, err := http.Get(ts.URL + "?title=ozzy") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func Test_BookAdd(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeBookHandlers(r, *n, service) + path, err := r.GetRoute("bookAdd").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/book", path) + + service.EXPECT(). + Create(gomock.Any()). + Return(entity.NewID(), nil) + h := createBook(service) + + ts := httptest.NewServer(h) + defer ts.Close() + payload := fmt.Sprintf(`{ + "title": "I Am Ozzy", + "author": "Ozzy Osbourne", + "pages": 294, + "quantity":1 +}`) + resp, _ := http.Post(ts.URL+"/v1/book", "application/json", strings.NewReader(payload)) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var b *book.Book + json.NewDecoder(resp.Body).Decode(&b) + assert.Equal(t, "Ozzy Osbourne", b.Author) +} + +func Test_BookFind(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeBookHandlers(r, *n, service) + path, err := r.GetRoute("bookFind").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/book/{id}", path) + b := book.NewFixtureBook() + service.EXPECT(). + Get(b.ID). + Return(b, nil) + handler := getBook(service) + r.Handle("/v1/book/{id}", handler) + ts := httptest.NewServer(r) + defer ts.Close() + res, err := http.Get(ts.URL + "/v1/book/" + b.ID.String()) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + var d *book.Book + json.NewDecoder(res.Body).Decode(&d) + assert.NotNil(t, d) + assert.Equal(t, b.ID, d.ID) +} + +func TestBookRemove(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeBookHandlers(r, *n, service) + path, err := r.GetRoute("bookDelete").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/book/{id}", path) + b := book.NewFixtureBook() + service.EXPECT().Delete(b.ID).Return(nil) + handler := deleteBook(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/handler/bookmark.go b/api/handler/bookmark.go deleted file mode 100644 index 4a155a4..0000000 --- a/api/handler/bookmark.go +++ /dev/null @@ -1,153 +0,0 @@ -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 deleted file mode 100644 index fab4679..0000000 --- a/api/handler/bookmark_test.go +++ /dev/null @@ -1,173 +0,0 @@ -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/handler/user.go b/api/handler/user.go new file mode 100644 index 0000000..6043c36 --- /dev/null +++ b/api/handler/user.go @@ -0,0 +1,182 @@ +package handler + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + + "github.com/eminetto/clean-architecture-go-v2/domain" + + "github.com/eminetto/clean-architecture-go-v2/api/presenter" + + "github.com/eminetto/clean-architecture-go-v2/domain/entity" + + "github.com/codegangsta/negroni" + "github.com/gorilla/mux" +) + +func listUsers(service user.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error reading users" + var data []*user.User + var err error + name := r.URL.Query().Get("name") + switch { + case name == "": + data, err = service.List() + default: + data, err = service.Search(name) + } + w.Header().Set("Content-Type", "application/json") + if err != nil && err != domain.ErrNotFound { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + + if data == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(errorMessage)) + return + } + var toJ []*presenter.User + for _, d := range data { + toJ = append(toJ, &presenter.User{ + ID: d.ID, + Email: d.Email, + FirstName: d.FirstName, + LastName: d.LastName, + }) + } + if err := json.NewEncoder(w).Encode(toJ); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + } + }) +} + +func createUser(service user.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error adding user" + var input struct { + Email string `json:"email"` + Password string `json:"password"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + } + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + //TODO: validate data ;) + u := &user.User{ + ID: entity.NewID(), + Email: input.Email, + Password: input.Password, + FirstName: input.FirstName, + LastName: input.LastName, + CreatedAt: time.Now(), + } + u.ID, err = service.Create(u) + if err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + toJ := &presenter.User{ + ID: u.ID, + Email: u.Email, + FirstName: u.FirstName, + LastName: u.LastName, + } + + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(toJ); err != nil { + log.Println(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + }) +} + +func getUser(service user.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error reading user" + vars := mux.Vars(r) + id, err := entity.StringToID(vars["id"]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + data, err := service.Get(id) + w.Header().Set("Content-Type", "application/json") + if err != nil && err != domain.ErrNotFound { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + + if data == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(errorMessage)) + return + } + toJ := &presenter.User{ + ID: data.ID, + Email: data.Email, + FirstName: data.FirstName, + LastName: data.LastName, + } + if err := json.NewEncoder(w).Encode(toJ); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + } + }) +} + +func deleteUser(service user.UseCase) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errorMessage := "Error removing user" + vars := mux.Vars(r) + id, err := entity.StringToID(vars["id"]) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + err = service.Delete(id) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errorMessage)) + return + } + }) +} + +//MakeUserHandlers make url handlers +func MakeUserHandlers(r *mux.Router, n negroni.Negroni, service user.UseCase) { + r.Handle("/v1/user", n.With( + negroni.Wrap(listUsers(service)), + )).Methods("GET", "OPTIONS").Name("listUsers") + + r.Handle("/v1/user", n.With( + negroni.Wrap(createUser(service)), + )).Methods("POST", "OPTIONS").Name("createUser") + + r.Handle("/v1/user/{id}", n.With( + negroni.Wrap(getUser(service)), + )).Methods("GET", "OPTIONS").Name("getUser") + + r.Handle("/v1/user/{id}", n.With( + negroni.Wrap(deleteUser(service)), + )).Methods("DELETE", "OPTIONS").Name("deleteUser") +} diff --git a/api/handler/user_test.go b/api/handler/user_test.go new file mode 100644 index 0000000..8799438 --- /dev/null +++ b/api/handler/user_test.go @@ -0,0 +1,159 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/eminetto/clean-architecture-go-v2/api/presenter" + "github.com/eminetto/clean-architecture-go-v2/domain" + "github.com/eminetto/clean-architecture-go-v2/domain/entity" + + "github.com/codegangsta/negroni" + "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock" + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func Test_UserIndex(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeUserHandlers(r, *n, service) + path, err := r.GetRoute("userIndex").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/user", path) + u := user.NewFixtureUser() + service.EXPECT(). + List(). + Return([]*user.User{u}, nil) + ts := httptest.NewServer(userIndex(service)) + defer ts.Close() + res, err := http.Get(ts.URL) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func Test_UserIndexNotFound(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + ts := httptest.NewServer(userIndex(service)) + defer ts.Close() + service.EXPECT(). + Search("dio"). + Return(nil, domain.ErrNotFound) + res, err := http.Get(ts.URL + "?name=dio") + assert.Nil(t, err) + assert.Equal(t, http.StatusNotFound, res.StatusCode) +} + +func Test_UserSearch(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + u := user.NewFixtureUser() + service.EXPECT(). + Search("ozzy"). + Return([]*user.User{u}, nil) + ts := httptest.NewServer(userIndex(service)) + defer ts.Close() + res, err := http.Get(ts.URL + "?name=ozzy") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func Test_UserAdd(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeUserHandlers(r, *n, service) + path, err := r.GetRoute("userAdd").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/user", path) + + service.EXPECT(). + Create(gomock.Any()). + Return(entity.NewID(), nil) + h := userAdd(service) + + ts := httptest.NewServer(h) + defer ts.Close() + payload := fmt.Sprintf(`{ + "name": "ozzy", + "email": "ozzy@hell.com", + "password": "asasa", + "first_name":"Ozzy", + "last_name":"Osbourne" +}`) + resp, _ := http.Post(ts.URL+"/v1/user", "application/json", strings.NewReader(payload)) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var u *presenter.User + json.NewDecoder(resp.Body).Decode(&u) + assert.Equal(t, "Ozzy Osbourne", fmt.Sprintf("%s %s", u.FirstName, u.LastName)) +} + +func Test_UserFind(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + service := mock.NewMockUseCase(controller) + r := mux.NewRouter() + n := negroni.New() + MakeUserHandlers(r, *n, service) + path, err := r.GetRoute("userFind").GetPathTemplate() + assert.Nil(t, err) + assert.Equal(t, "/v1/user/{id}", path) + u := user.NewFixtureUser() + service.EXPECT(). + Get(u.ID). + Return(u, nil) + handler := userFind(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 index 5b69a8f..9459dd9 100644 --- a/api/main.go +++ b/api/main.go @@ -1,53 +1,62 @@ package main import ( - "github.com/prometheus/client_golang/prometheus/promhttp" + "database/sql" + "fmt" "log" "net/http" "os" "strconv" "time" + "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + + "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" + "github.com/prometheus/client_golang/prometheus/promhttp" + "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/eminetto/clean-architecture-go-v2/api/handler" + "github.com/eminetto/clean-architecture-go-v2/api/middleware" + "github.com/eminetto/clean-architecture-go-v2/config" + "github.com/eminetto/clean-architecture-go-v2/pkg/metric" + _ "github.com/go-sql-driver/mysql" "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) + + dataSourceName := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true", config.DB_USER, config.DB_PASSWORD, config.DB_HOST, config.DB_DATABASE) + db, err := sql.Open("mysql", dataSourceName) if err != nil { log.Fatal(err.Error()) } - defer session.Close() + defer db.Close() - r := mux.NewRouter() + bookRepo := book.NewMySQLRepoRepository(db) + bookService := book.NewService(bookRepo) - mPool := mgosession.NewPool(nil, session, config.MONGODB_CONNECTION_POOL) - defer mPool.Close() + userRepo := user.NewMySQLRepoRepository(db) + userService := user.NewService(userRepo) - bookmarkRepo := bookmark.NewMongoRepository(mPool, config.MONGODB_DATABASE) - bookmarkService := bookmark.NewService(bookmarkRepo) + //loanService := loan.NewService(userService, bookService) metricService, err := metric.NewPrometheusService() if err != nil { log.Fatal(err.Error()) } - + r := mux.NewRouter() //handlers n := negroni.New( negroni.HandlerFunc(middleware.Cors), negroni.HandlerFunc(middleware.Metrics(metricService)), negroni.NewLogger(), ) - //bookmark - handler.MakeBookmarkHandlers(r, *n, bookmarkService) + //book + handler.MakeBookHandlers(r, *n, bookService) + + //user + handler.MakeUserHandlers(r, *n, userService) http.Handle("/", r) http.Handle("/metrics", promhttp.Handler()) diff --git a/api/middleware/metrics.go b/api/middleware/metrics.go index 03bb8ca..4ea74e6 100755 --- a/api/middleware/metrics.go +++ b/api/middleware/metrics.go @@ -4,7 +4,7 @@ import ( "net/http" "strconv" - "github.com/eminetto/clean-architecture-go/pkg/metric" + "github.com/eminetto/clean-architecture-go-v2/pkg/metric" "github.com/codegangsta/negroni" ) diff --git a/api/presenter/book.go b/api/presenter/book.go new file mode 100644 index 0000000..257c7e5 --- /dev/null +++ b/api/presenter/book.go @@ -0,0 +1,14 @@ +package presenter + +import ( + "github.com/eminetto/clean-architecture-go-v2/domain/entity" +) + +//Book data +type Book struct { + ID entity.ID `json:"id"` + Title string `json:"title"` + Author string `json:"author"` + Pages int `json:"pages"` + Quantity int `json:"quantity"` +} diff --git a/api/presenter/user.go b/api/presenter/user.go new file mode 100644 index 0000000..05dfb3e --- /dev/null +++ b/api/presenter/user.go @@ -0,0 +1,13 @@ +package presenter + +import ( + "github.com/eminetto/clean-architecture-go-v2/domain/entity" +) + +//User data +type User struct { + ID entity.ID `json:"id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} diff --git a/domain/entity/book/repository_mysql.go b/domain/entity/book/repository_mysql.go index 80e59bd..7df9b01 100644 --- a/domain/entity/book/repository_mysql.go +++ b/domain/entity/book/repository_mysql.go @@ -84,7 +84,27 @@ func (r *MySQLRepo) Search(query string) ([]*Book, error) { //List users func (r *MySQLRepo) List() ([]*Book, error) { - return nil, nil + stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book`) + if err != nil { + return nil, err + } + var books []*Book + rows, err := stmt.Query() + if err != nil { + return nil, err + } + for rows.Next() { + var b Book + err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt) + if err != nil { + return nil, err + } + books = append(books, &b) + } + if len(books) == 0 { + return nil, domain.ErrNotFound + } + return books, nil } //Delete an user diff --git a/pkg/metric/prometheus.go b/pkg/metric/prometheus.go index 0694d5b..2362d73 100755 --- a/pkg/metric/prometheus.go +++ b/pkg/metric/prometheus.go @@ -1,9 +1,9 @@ package metric import ( + "github.com/eminetto/clean-architecture-go-v2/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/push" - "github.com/eminetto/clean-architecture-go/config" ) //Service implements UseCase interface