diff --git a/products/data/product.go b/products/data/product.go index 20ce968..b200ca4 100644 --- a/products/data/product.go +++ b/products/data/product.go @@ -28,6 +28,18 @@ func AddProduct(p *models.Product) { productList = append(productList, p) } +// GetProductByID returns a single product which matches the id from the +// database. +// If a product is not found this function returns a ProductNotFound error +func GetProductByID(id int) (*models.Product, error) { + i, _, err := findProduct(id) + if err != nil { + return nil, ErrorProductNotFound + } + + return productList[i], nil +} + // UpdateProduct edits a Product identified by its id func UpdateProduct(id int, p *models.Product) error { idx, _, err := findProduct(id) diff --git a/products/handlers/delete.go b/products/handlers/delete.go index a4bfc83..8bc5dd1 100644 --- a/products/handlers/delete.go +++ b/products/handlers/delete.go @@ -9,12 +9,15 @@ import ( "github.com/rjNemo/go-micro/products/data" ) -// swagger:route DELETE /products/{id} products product -// Deletes a product +// swagger:route DELETE /products/{id} products deleteProduct +// Update a products details +// // responses: -// 200: productResponse +// 204: noContent +// 404: errorResponse +// 501: errorResponse -// DeleteProduct edit product identified by id +// DeleteProduct delete product from datastore identified by id func (p *Products) DeleteProduct(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, _ := strconv.Atoi(vars["id"]) @@ -31,4 +34,6 @@ func (p *Products) DeleteProduct(w http.ResponseWriter, r *http.Request) { http.Error(w, errMsg, http.StatusInternalServerError) return } + // write the no content success header + w.WriteHeader(http.StatusNoContent) } diff --git a/products/handlers/docs.go b/products/handlers/docs.go new file mode 100644 index 0000000..e9d3519 --- /dev/null +++ b/products/handlers/docs.go @@ -0,0 +1,61 @@ +// Package classification Product API +// +// Documentation for Product API +// +// Schemes: http +// BasePath: / +// Version: 1.0.0 +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// swagger:meta +package handlers + +import "github.com/rjNemo/go-micro/products/models" + +// list of products in the response. +// swagger:response productsResponse +type productsResponse struct { + // All products in the datastore + // in: body + Body []models.Product +} + +// product in the response. +// swagger:response productResponse +type productResponse struct { + // One product in the datastore + // in: body + Body models.Product +} + +// swagger:parameters deleteProduct updateProduct +type productIDParameter struct { + // The ID of a product in the database + // in: path + // required: true + ID int `json:"id"` +} + +// empty response +// swagger:response noContent +type productNoContent struct{} + +// Generic error message returned as a string +// swagger:response errorResponse +type errorResponse struct { + // Description of the error + // in: body + Body string +} + +// Validation errors defined as an array of strings +// swagger:response errorValidation +type errorValidation struct { + // Collection of the errors + // in: body + Body string +} diff --git a/products/handlers/get.go b/products/handlers/get.go index 07f2d17..ca966e8 100644 --- a/products/handlers/get.go +++ b/products/handlers/get.go @@ -8,11 +8,11 @@ import ( ) // swagger:route GET /products products listProducts -// Returns a list of products +// Return a list of products from the database // responses: -// 200: productsResponse +// 200: productsResponse -// GetProducts writes all products to response in JSON format +// GetProducts returns all products to response in JSON format func (p *Products) GetProducts(w http.ResponseWriter, r *http.Request) { p.logger.Println("Handle 'GET' request") // fetch products from the datastore @@ -25,3 +25,36 @@ func (p *Products) GetProducts(w http.ResponseWriter, r *http.Request) { return } } + +// swagger:route GET /products/{id} products getProduct +// Return a list of products from the database +// responses: +// 200: productResponse +// 404: errorResponse + +// GetOneProduct handles GET requests +func (p *Products) GetOneProduct(w http.ResponseWriter, r *http.Request) { + id := getProductID(r) + p.logger.Println("[DEBUG] get record id", id) + prod, err := data.GetProductByID(id) + + switch err { + case nil: + + case data.ErrorProductNotFound: + p.logger.Println("[ERROR] fetching product", err) + http.Error(w, err.Error(), http.StatusNotFound) + return + + default: + p.logger.Println("[ERROR] fetching product", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = prod.ToJSON(w) + if err != nil { + // we should never be here but log the error just incase + p.logger.Println("[ERROR] serializing product", err) + } +} diff --git a/products/handlers/middleware.go b/products/handlers/middleware.go new file mode 100644 index 0000000..2dae088 --- /dev/null +++ b/products/handlers/middleware.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "context" + "fmt" + "net/http" + + "github.com/rjNemo/go-micro/products/models" +) + +// ProductValidationMiddleware validates the data passed by the user +func (p *Products) ProductValidationMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create a new product + newProd := &models.Product{} + // deserialize JSON to product + err := newProd.FromJSON(r.Body) + if err != nil { + p.logger.Printf("Error deserializing %v", err) + errMsg := fmt.Sprintf("Unable to decode data: %s\n", err) + http.Error(w, errMsg, http.StatusBadRequest) + return + } + // validate the product + err = newProd.Validate() + if err != nil { + p.logger.Printf("Error deserializing %v", err) + errMsg := fmt.Sprintf("Validation error: %s\n", err) + http.Error(w, errMsg, http.StatusBadRequest) + return + } + + // add product to the context + ctx := context.WithValue(r.Context(), KeyProduct{}, newProd) + req := r.WithContext(ctx) + + // call the next handler + next.ServeHTTP(w, req) + }) +} diff --git a/products/handlers/post.go b/products/handlers/post.go index 799b713..5c49371 100644 --- a/products/handlers/post.go +++ b/products/handlers/post.go @@ -7,10 +7,13 @@ import ( "github.com/rjNemo/go-micro/products/models" ) -// swagger:route POST /products products product -// Creates a product +// swagger:route POST /products products createProduct +// Create a new product +// // responses: -// 201: productResponse +// 200: productResponse +// 422: errorValidation +// 501: errorResponse // AddProduct reads request body and creates new product func (p *Products) AddProduct(w http.ResponseWriter, r *http.Request) { diff --git a/products/handlers/products.go b/products/handlers/products.go index a85b9eb..669430a 100644 --- a/products/handlers/products.go +++ b/products/handlers/products.go @@ -1,26 +1,11 @@ -// Package classification Product API -// -// Documentation for Product API -// -// Schemes: http -// BasePath: / -// Version: 1.0.0 -// -// Consumes: -// - application/json -// -// Produces: -// - application/json -// swagger:meta package handlers import ( - "context" - "fmt" "log" "net/http" + "strconv" - "github.com/rjNemo/go-micro/products/models" + "github.com/gorilla/mux" ) // Products is a handler for Products API service @@ -28,22 +13,6 @@ type Products struct { logger *log.Logger } -// list of products in the response. For go-swagger -// swagger:response productsResponse -type productsResponse struct { - // All products in the datastore - // in: body - Body []models.Product -} - -// product in the response. For go-swagger -// swagger:response productResponse -type productResponse struct { - // One product in the datastore - // in: body - Body models.Product -} - // New creates a Products handler func New(logger *log.Logger) *Products { return &Products{logger: logger} @@ -52,33 +21,20 @@ func New(logger *log.Logger) *Products { // KeyProduct is a key used to pass validated product to handler type KeyProduct struct{} -// ProductValidationMiddleware validates the data passed by the user -func (p *Products) ProductValidationMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // create a new product - newProd := &models.Product{} - // deserialize JSON to product - err := newProd.FromJSON(r.Body) - if err != nil { - p.logger.Printf("Error deserializing %v", err) - errMsg := fmt.Sprintf("Unable to decode data: %s\n", err) - http.Error(w, errMsg, http.StatusBadRequest) - return - } - // validate the product - err = newProd.Validate() - if err != nil { - p.logger.Printf("Error deserializing %v", err) - errMsg := fmt.Sprintf("Validation error: %s\n", err) - http.Error(w, errMsg, http.StatusBadRequest) - return - } +// getProductID returns the product ID from the URL +// Panics if cannot convert the id into an integer +// this should never happen as the router ensures that +// this is a valid number +func getProductID(r *http.Request) int { + // parse the product id from the url + vars := mux.Vars(r) - // add product to the context - ctx := context.WithValue(r.Context(), KeyProduct{}, newProd) - req := r.WithContext(ctx) + // convert the id into an integer and return + id, err := strconv.Atoi(vars["id"]) + if err != nil { + // should never happen + panic(err) + } - // call the next handler - next.ServeHTTP(w, req) - }) + return id } diff --git a/products/handlers/put.go b/products/handlers/put.go index 5f1aee8..a6fab43 100644 --- a/products/handlers/put.go +++ b/products/handlers/put.go @@ -3,22 +3,22 @@ package handlers import ( "fmt" "net/http" - "strconv" - "github.com/gorilla/mux" "github.com/rjNemo/go-micro/products/data" "github.com/rjNemo/go-micro/products/models" ) -// swagger:route PUT /products/{id} products product -// Updates a product +// swagger:route PUT /products products updateProduct +// Update a products details +// // responses: -// 204: productResponse +// 204: noContent +// 404: errorResponse +// 422: errorValidation // UpdateProduct edit product identified by id func (p *Products) UpdateProduct(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id, _ := strconv.Atoi(vars["id"]) + id := getProductID(r) p.logger.Println("Handle 'PUT' request", id) // get product from the request @@ -35,4 +35,6 @@ func (p *Products) UpdateProduct(w http.ResponseWriter, r *http.Request) { http.Error(w, errMsg, http.StatusInternalServerError) return } + // write the no content success header + w.WriteHeader(http.StatusNoContent) } diff --git a/products/handlers/routes.go b/products/handlers/routes.go index fd2f401..ffc68dc 100644 --- a/products/handlers/routes.go +++ b/products/handlers/routes.go @@ -12,6 +12,7 @@ func (p *Products) RegisterRoutes(r *mux.Router) { // GET getRouter := r.Methods(http.MethodGet).Subrouter() getRouter.HandleFunc("/", p.GetProducts) + getRouter.HandleFunc("/{id:[0-9]+}", p.GetOneProduct) // POST postRouter := r.Methods(http.MethodPost).Subrouter() postRouter.HandleFunc("/", p.AddProduct) diff --git a/products/models/product.go b/products/models/product.go index 79203f0..9f7dceb 100644 --- a/products/models/product.go +++ b/products/models/product.go @@ -9,15 +9,36 @@ import ( ) // Product defines the structure of a product +// swagger:model type Product struct { - ID int `json:"id"` //TODO: use uuid - Name string `json:"name" validate:"required"` - Description string `json:"description"` - Price float32 `json:"price" validate:"gt=0"` // TODO: use int - SKU string `json:"sku" validate:"required,sku"` - CreatedOn string `json:"-"` - UpdatedOn string `json:"-"` - DeletedOn string `json:"-"` + // the id for this product + // + // required: true + // min: 1 + ID int `json:"id"` //TODO: use uuid + // the name for this poduct + // + // required: true + // max length: 255 + Name string `json:"name" validate:"required"` + // the description for this poduct + // + // required: false + // max length: 10000 + Description string `json:"description"` + // the price for the product + // + // required: true + // min: 0.01 + Price float32 `json:"price" validate:"gt=0"` // TODO: use int + // the SKU for the product + // + // required: true + // pattern: [a-z]+-[a-z]+-[a-z]+ + SKU string `json:"sku" validate:"required,sku"` + CreatedOn string `json:"-"` + UpdatedOn string `json:"-"` + DeletedOn string `json:"-"` } // FromJSON read JSON data to create a new product @@ -25,6 +46,11 @@ func (p *Product) FromJSON(r io.Reader) error { return json.NewDecoder(r).Decode(p) } +// ToJSON convert product to JSON +func (p *Product) ToJSON(w io.Writer) error { + return json.NewEncoder(w).Encode(p) +} + // Validate checks object validity func (p *Product) Validate() error { validate := validator.New() diff --git a/swagger.yaml b/swagger.yaml index f367f6c..d973d29 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -6,22 +6,37 @@ definitions: description: Product defines the structure of a product properties: description: + description: the description for this poduct + maxLength: 10000 type: string x-go-name: Description id: + description: the id for this product format: int64 + minimum: 1 type: integer x-go-name: ID name: + description: the name for this poduct + maxLength: 255 type: string x-go-name: Name price: + description: the price for the product format: float + minimum: 0.01 type: number x-go-name: Price sku: + description: the SKU for the product + pattern: '[a-z]+-[a-z]+-[a-z]+' type: string x-go-name: SKU + required: + - id + - name + - price + - sku type: object x-go-package: github.com/rjNemo/go-micro/products/models info: @@ -31,7 +46,7 @@ info: paths: /products: get: - description: Returns a list of products + description: Return a list of products from the database operationId: listProducts responses: "200": @@ -39,39 +54,83 @@ paths: tags: - products post: - description: Creates a product - operationId: product + description: Create a new product + operationId: createProduct responses: - "201": + "200": $ref: '#/responses/productResponse' + "422": + $ref: '#/responses/errorValidation' + "501": + $ref: '#/responses/errorResponse' + tags: + - products + put: + description: Update a products details + operationId: updateProduct + parameters: + - description: The ID of a product in the database + format: int64 + in: path + name: id + required: true + type: integer + x-go-name: ID + responses: + "204": + $ref: '#/responses/noContent' + "404": + $ref: '#/responses/errorResponse' + "422": + $ref: '#/responses/errorValidation' tags: - products /products/{id}: delete: - description: Deletes a product - operationId: product + description: Update a products details + operationId: deleteProduct + parameters: + - description: The ID of a product in the database + format: int64 + in: path + name: id + required: true + type: integer + x-go-name: ID + responses: + "204": + $ref: '#/responses/noContent' + "404": + $ref: '#/responses/errorResponse' + "501": + $ref: '#/responses/errorResponse' + tags: + - products + get: + description: Return a list of products from the database + operationId: getProduct responses: "200": $ref: '#/responses/productResponse' - tags: - - products - put: - description: Updates a product - operationId: product - responses: - "204": - $ref: '#/responses/productResponse' + "404": + $ref: '#/responses/errorResponse' tags: - products produces: - application/json responses: + errorResponse: + description: Generic error message returned as a string + errorValidation: + description: Validation errors defined as an array of strings + noContent: + description: empty response productResponse: - description: product in the response. For go-swagger + description: product in the response. schema: $ref: '#/definitions/Product' productsResponse: - description: list of products in the response. For go-swagger + description: list of products in the response. schema: items: $ref: '#/definitions/Product'