diff --git a/controllers/library.go b/controllers/library.go index 1b05601..873c39c 100644 --- a/controllers/library.go +++ b/controllers/library.go @@ -6,80 +6,131 @@ import ( "fmt" "net/http" "strconv" + "strings" "cloud.google.com/go/datastore" "github.com/go-martini/martini" - "google.golang.org/api/iterator" "acme-books/models" ) -type LibraryController struct{} - -func (lc LibraryController) GetByKey(params martini.Params, w http.ResponseWriter) { - ctx := context.Background() - client, _ := datastore.NewClient(ctx, "acme-books") +type LibraryController struct { + bi *models.BookInt +} - defer client.Close() +func NewLibrary(client *datastore.Client, ctx context.Context, books []models.Book) (*LibraryController, error) { + bi := models.NewBookInt(client, ctx) + lc := LibraryController{bi} + if err := lc.bootstrapBooks(books); err != nil { + fmt.Println("Problem bootstrapping library: ", err) + return nil, err + } + return &lc, nil +} - id, err := strconv.Atoi(params["id"]) +func (lc LibraryController) bootstrapBooks(books []models.Book) error { + return lc.bi.PutBooks(books) +} - if err != nil { - fmt.Println(err) +func (lc LibraryController) Get(w http.ResponseWriter, params martini.Params) { + if id, err := strconv.Atoi(params["id"]); err != nil { + fmt.Println("Bad id: ", err) w.WriteHeader(http.StatusBadRequest) return + } else if book, err := lc.bi.GetBookByKey(int64(id)); err != nil { + fmt.Println("Error getting book: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else if jsonStr, err := json.MarshalIndent(book, "", " "); err != nil { + fmt.Println("Error serializing: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusOK) + w.Write(jsonStr) } +} + +func (lc LibraryController) List(w http.ResponseWriter, r *http.Request) { + var output []models.Book + var err error - var book models.Book - key := datastore.IDKey("Book", int64(id), nil) + r.ParseForm() - err = client.Get(ctx, key, &book) + title := r.Form.Get("title") + author := r.Form.Get("author") + borrowed := r.Form.Get("borrowed") - if err != nil { - fmt.Println(err) - w.WriteHeader(http.StatusInternalServerError) + switch sortBy := r.Form.Get("sort"); sortBy { + case "author", "title", "id", "": + output, err = lc.bi.GetBooks(title, author, borrowed, strings.Title(sortBy)) + default: + fmt.Println("Unknown sorting field: ", sortBy) + w.WriteHeader(http.StatusBadRequest) return } - - jsonStr, err := json.MarshalIndent(book, "", " ") - if err != nil { - fmt.Println(err) + fmt.Println("Error getting books: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else if jsonStr, err := json.MarshalIndent(output, "", " "); err != nil { + fmt.Println("Error serializing: ", err) w.WriteHeader(http.StatusInternalServerError) return + } else { + w.WriteHeader(http.StatusOK) + w.Write(jsonStr) } - - w.WriteHeader(http.StatusOK) - w.Write(jsonStr) } -func (lc LibraryController) ListAll(r *http.Request, w http.ResponseWriter) { - ctx := context.Background() - client, _ := datastore.NewClient(ctx, "acme-books") - - defer client.Close() - - var output []models.Book +// Return a handler to borrow or return a book. TODO: sanitize inputs +func (lc LibraryController) BorrowOrReturn(borrow bool) martini.Handler { + return func(w http.ResponseWriter, params martini.Params) { + var book models.Book - it := client.Run(ctx, datastore.NewQuery("Book")) - for { - var b models.Book - _, err := it.Next(&b) - if err == iterator.Done { - fmt.Println(err) - break + state := "return" + if borrow { + state = "borrow" } - output = append(output, b) + if id, err := strconv.Atoi(params["id"]); err != nil { + fmt.Println("Bad id: ", err) + w.WriteHeader(http.StatusBadRequest) + return + } else if book, err = lc.bi.GetBookByKey(int64(id)); err == datastore.ErrNoSuchEntity { + fmt.Println("Book not found: ", id) + w.WriteHeader(http.StatusBadRequest) + return + } else if err != nil { + fmt.Println("Error getting book: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else if book.Borrowed == borrow { + fmt.Printf("Book already %sed: %d\n", state, id) + w.WriteHeader(http.StatusBadRequest) + return + } + book.Borrowed = borrow + if err := lc.bi.PutBook(book); err != nil { + fmt.Printf("Error %sing book: %s\n", state, err) + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Printf("Book %sed: %d\n", state, book.Id) + w.WriteHeader(http.StatusNoContent) } +} - jsonStr, err := json.MarshalIndent(output, "", " ") - - if err != nil { - fmt.Println(err) +func (lc LibraryController) AddBook(w http.ResponseWriter, book models.Book) { + if newbook, err := lc.bi.NewBook(book); err != nil { + fmt.Println("Error adding book: ", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else if jsonStr, err := json.MarshalIndent(newbook, "", " "); err != nil { + fmt.Println("Error serializing: ", err) w.WriteHeader(http.StatusInternalServerError) return + } else { + w.WriteHeader(http.StatusOK) + w.Write(jsonStr) } - - w.WriteHeader(http.StatusOK) - w.Write(jsonStr) } diff --git a/go.mod b/go.mod index 9c14db8..2e28741 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,6 @@ require ( github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab github.com/joho/godotenv v1.4.0 + github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf // indirect google.golang.org/api v0.60.0 ) diff --git a/go.sum b/go.sum index 856c7ae..b68bd4a 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf h1:6YSkbjZVghliN7zwJC/U3QQG+OVXOrij3qQ8sxfPIMg= +github.com/martini-contrib/binding v0.0.0-20160701174519-05d3e151b6cf/go.mod h1:aCggxkm1kuifLw/LEQUbz91N1ZM6PhV7dz03xPQduZA= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/main.go b/main.go index 40f0b27..7e79cec 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,22 @@ package main import ( - "context" - "fmt" "log" "os" - "cloud.google.com/go/datastore" - - "acme-books/models" "acme-books/server" "github.com/joho/godotenv" ) -func main() { +func init() { err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file! Did you forget to run `gcloud beta emulators datastore env-init > .env`") } +} - bootstrapBooks() - +func main() { host := getEnvWithDefault("HOST", "localhost") port := getEnvWithDefault("PORT", "3030") @@ -34,27 +29,3 @@ func getEnvWithDefault(key, fallback string) string { } return fallback } - -func bootstrapBooks() { - ctx := context.Background() - client, _ := datastore.NewClient(ctx, "acme-books") - - defer client.Close() - - books := []models.Book{ - {Id: 1, Author: "George Orwell", Title: "1984", Borrowed: false}, - {Id: 2, Author: "George Orwell", Title: "Animal Farm", Borrowed: false}, - {Id: 3, Author: "Robert Jordan", Title: "Eye of the world", Borrowed: false}, - {Id: 4, Author: "Various", Title: "Collins Dictionary", Borrowed: false}, - } - - var keys []*datastore.Key - - for _, book := range books { - keys = append(keys, datastore.IDKey("Book", book.Id, nil)) - } - - if _, err := client.PutMulti(ctx, keys, books); err != nil { - fmt.Println(err) - } -} diff --git a/models/book.go b/models/book.go index 531d740..a3c52cc 100644 --- a/models/book.go +++ b/models/book.go @@ -1,8 +1,90 @@ package models +import ( + "context" + "strconv" + + "cloud.google.com/go/datastore" +) + type Book struct { Id int64 - Title string `json:"title"` - Author string `json:"writer"` + Title string `json:"title" binding:"required"` + Author string `json:"writer" binding:"required"` Borrowed bool `json:"borrowed"` } + +type BookInt struct { + client *datastore.Client + ctx context.Context +} + +func NewBookInt(client *datastore.Client, ctx context.Context) *BookInt { + return &BookInt{client, ctx} +} + +func (bi BookInt) GetBookByKey(id int64) (book Book, err error) { + key := datastore.IDKey("Book", id, nil) + err = bi.client.Get(bi.ctx, key, &book) + return book, err +} + +func (bi BookInt) GetBooks(title, author, borrowed, sort string) ([]Book, error) { + var books []Book + query := datastore.NewQuery("Book") + + if title != "" { + query = query.Filter("Title =", title) + } + + if author != "" { + query = query.Filter("Author =", author) + } + + if borrowed != "" { + if b, err := strconv.ParseBool(borrowed); err != nil { + return books, err + + } else { + query = query.Filter("Borrowed =", b) + } + } + + if sort == "" { + sort = "Id" + } + + query = query.Order(sort) + + _, err := bi.client.GetAll(bi.ctx, query, &books) + return books, err +} + +func (bi BookInt) PutBooks(books []Book) error { + var keys []*datastore.Key + + for _, book := range books { + keys = append(keys, datastore.IDKey("Book", book.Id, nil)) + } + + _, err := bi.client.PutMulti(bi.ctx, keys, books) + return err +} + +func (bi BookInt) PutBook(book Book) error { + return bi.PutBooks([]Book{book}) +} + +func (bi BookInt) NewBook(book Book) (Book, error) { + q := datastore.NewQuery("Book") + if n, err := bi.client.Count(bi.ctx, q); err != nil { + return Book{}, err + } else { + book.Id = int64(n) + 1 + } + if err := bi.PutBook(book); err != nil { + return Book{}, err + } else { + return book, nil + } +} diff --git a/server/router.go b/server/router.go index 70f082c..f2ef5f8 100644 --- a/server/router.go +++ b/server/router.go @@ -2,17 +2,20 @@ package server import ( "github.com/go-martini/martini" + "github.com/martini-contrib/binding" "acme-books/controllers" + "acme-books/models" ) -func NewRouter() *martini.ClassicMartini { - libraryController := new(controllers.LibraryController) - +func NewRouter(library *controllers.LibraryController) *martini.ClassicMartini { router := martini.Classic() - router.Get("/books", libraryController.ListAll) - router.Get("/books/:id", libraryController.GetByKey) + router.Get("/books", library.List) + router.Get("/books/:id", library.Get) + router.Put("/:id/borrow", library.BorrowOrReturn(true)) + router.Put("/:id/return", library.BorrowOrReturn(false)) + router.Post("/book", binding.Bind(models.Book{}), library.AddBook) return router } diff --git a/server/server.go b/server/server.go index 404bf4a..dbb8c60 100644 --- a/server/server.go +++ b/server/server.go @@ -1,6 +1,30 @@ package server +import ( + "acme-books/controllers" + "acme-books/models" + "context" + "log" + + "cloud.google.com/go/datastore" +) + +var books = []models.Book{ + {Id: 1, Author: "George Orwell", Title: "1984", Borrowed: false}, + {Id: 2, Author: "George Orwell", Title: "Animal Farm", Borrowed: false}, + {Id: 3, Author: "Robert Jordan", Title: "Eye of the world", Borrowed: false}, + {Id: 4, Author: "Various", Title: "Collins Dictionary", Borrowed: false}, +} + func Init(host string, port string) { - r := NewRouter() - r.RunOnAddr(host + ":" + port) + ctx := context.Background() + if client, err := datastore.NewClient(ctx, "acme-books"); err != nil { + log.Fatalf("Error creating datastore client: %s", err) + } else { + defer client.Close() + if library, err := controllers.NewLibrary(client, ctx, books); err == nil { + r := NewRouter(library) + r.RunOnAddr(host + ":" + port) + } + } }