summaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
authorTucker Evans <tuckerevans24@gmail.com>2019-12-07 15:05:42 -0500
committerTucker Evans <tuckerevans24@gmail.com>2019-12-07 15:05:42 -0500
commitac33c29faf55b640066641d2c328d841fa3e593b (patch)
tree81d53524e0decc1e8b7e3c2fab59aed0135a056b /backend
parent33ff048f5efb30e767780f24424588af562ea400 (diff)
parentcafe1b4fd3cc02554f44ffbaa8467d867a6838cb (diff)
Merge commit 'cafe1b4fd3cc02554f44ffbaa8467d867a6838cb' into chrisundercoffer
Diffstat (limited to 'backend')
-rw-r--r--backend/main.go244
-rw-r--r--backend/readme.adoc197
-rw-r--r--backend/recipe.go44
-rw-r--r--backend/todo.txt4
4 files changed, 279 insertions, 210 deletions
diff --git a/backend/main.go b/backend/main.go
index 57e6ab2..0bd5ebf 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -10,27 +10,38 @@ import _ "github.com/lib/pq"
import "database/sql"
import "encoding/json"
-type APIError struct {
+type APIStatus struct {
Code int
Msg string
}
-type APIDataIds struct {
- Ids interface{}
+type APIResponse struct {
+ Status APIStatus
+ Data interface{}
}
-type APIDataRecipe struct {
- Recipe interface{}
+func MakeAPIResponse(status int, msg string, data interface{}) *APIResponse {
+ return &APIResponse{
+ Status: APIStatus{
+ Code: status,
+ Msg: msg,
+ },
+ Data: data,
+ }
}
-type APIResponseList struct {
- Status APIError
- Data []APIDataIds
-}
+func sendResponse(w http.ResponseWriter, code int, msg string, data interface{}) {
+ w.Header().Set("Access-Control-Allow-Origin", "*") //Enable CORS
+ w.Header().Set("Content-Type",
+ "application/json; charset=UTF-8")
+
+ w.WriteHeader(code)
+
+ resp := MakeAPIResponse(code, msg, data)
-type APIResponseItem struct {
- Status APIError
- Data []APIDataRecipe
+ if err := json.NewEncoder(w).Encode(resp); err != nil {
+ panic(err)
+ }
}
func RecipeList(w http.ResponseWriter, r *http.Request) {
@@ -48,19 +59,8 @@ func RecipeList(w http.ResponseWriter, r *http.Request) {
}
}
- resp := APIResponseList{
- Status: APIError{Code: 200, Msg: "Successful Request"},
- Data: make([]APIDataIds, 0),
- }
- resp.Data = append(resp.Data, APIDataIds{Ids: ids})
+ sendResponse(w, http.StatusOK, "Successful Request", ids)
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusOK)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
- return
} else if r.Method == "POST" {
var recipe *Recipe
@@ -77,109 +77,46 @@ func RecipeList(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(body, &recipe)
if err != nil {
fmt.Println(err)
- w.WriteHeader(http.StatusUnprocessableEntity)
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- resp := APIResponseItem{
- Status: APIError{
- Code: http.StatusUnprocessableEntity,
- Msg: "Invalid Recipe"},
- Data: make([]APIDataRecipe, 0),
- }
-
- err := json.NewEncoder(w).Encode(resp)
- if err != nil {
- panic(err)
- }
+ sendResponse(w, http.StatusUnprocessableEntity,
+ "Invalid Recipe", nil)
return
}
err = AddRecipeDB(recipe, db)
if err != nil {
fmt.Println(err)
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusBadRequest,
- Msg: "Recipe could not be added"},
- Data: make([]APIDataRecipe, 0),
- }
-
- resp.Data = append(resp.Data, APIDataRecipe{recipe})
-
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusBadRequest)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
+ sendResponse(w, http.StatusBadRequest,
+ "Recipe could not be added", recipe)
return
}
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusCreated,
- Msg: "Recipe added successfully"},
- Data: make([]APIDataRecipe, 0),
- }
+ sendResponse(w, http.StatusCreated, "Recipe added successfully",
+ recipe)
- resp.Data = append(resp.Data, APIDataRecipe{recipe})
+ } else {
+ sendResponse(w, http.StatusMethodNotAllowed, "Invalid method",
+ nil)
- w.Header().Set("Content-Type", "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusCreated)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
-
- return
- }
-
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusMethodNotAllowed,
- Msg: "Invalid method"},
- Data: nil,
- }
-
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusMethodNotAllowed)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
}
}
func SingleRecipe(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*") //Enable CORS
+
recipe_id, err := strconv.Atoi(r.URL.Path[len("/recipes/"):])
if err != nil {
fmt.Println("Not a valid ID")
return
}
if r.Method == "GET" {
- var status int
- var msg string
-
recipe := RecipeFromId(recipe_id, db)
if recipe == nil {
- status = http.StatusNotFound
- msg = "Recipe not Found"
+ sendResponse(w, http.StatusNotFound, "Recipe not Found",
+ nil)
} else {
- status = http.StatusOK
- msg = "Successful"
- }
-
- resp := APIResponseItem{
- Status: APIError{Code: status, Msg: msg},
- Data: make([]APIDataRecipe, 0),
- }
-
- if status == http.StatusOK {
- resp.Data = append(resp.Data, APIDataRecipe{recipe})
+ sendResponse(w, http.StatusOK, "Successful", recipe)
}
- w.Header().Set("Content-Type", "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusOK)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
-
- return
} else if r.Method == "POST" {
var status int
row := db.QueryRow(`SELECT id FROM recipes WHERE id = $1`,
@@ -192,18 +129,9 @@ func SingleRecipe(w http.ResponseWriter, r *http.Request) {
} else {
status = http.StatusConflict
}
- resp := APIResponseItem{
- Status: APIError{Code: status, Msg: "Cannot add to specific resource"},
- Data: nil,
- }
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(status)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
- return
+ sendResponse(w, status, "Cannot add to specific resource",
+ nil)
} else if r.Method == "PUT" {
var recipe *Recipe
@@ -220,17 +148,8 @@ func SingleRecipe(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(body, &recipe)
if err != nil {
fmt.Println(err)
- w.WriteHeader(http.StatusUnprocessableEntity)
- w.Header().Set("Content-Type", "application/json; charset=UTF-8")
- resp := APIResponseItem{
- Status: APIError{
- Code: http.StatusUnprocessableEntity,
- Msg: "Invalid Recipe"},
- Data: make([]APIDataRecipe, 0),
- }
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
+ sendResponse(w, http.StatusUnprocessableEntity,
+ "Invalid Recipe", nil)
return
}
@@ -239,38 +158,15 @@ func SingleRecipe(w http.ResponseWriter, r *http.Request) {
err = UpdateRecipeDB(recipe, db)
if err != nil {
fmt.Println(err)
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusBadRequest,
- Msg: "Recipe could not be updated"},
- Data: make([]APIDataRecipe, 0),
- }
+ sendResponse(w, http.StatusBadRequest,
+ "Recipe could not be updated", recipe)
- resp.Data = append(resp.Data, APIDataRecipe{recipe})
-
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusBadRequest)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
return
}
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusCreated,
- Msg: "Recipe added successfully"},
- Data: make([]APIDataRecipe, 0),
- }
-
- resp.Data = append(resp.Data, APIDataRecipe{recipe})
-
- w.Header().Set("Content-Type", "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusCreated)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
+ sendResponse(w, http.StatusCreated, "Recipe added successfully",
+ recipe)
- return
} else if r.Method == "DELETE" {
res, err := db.Exec(`DELETE FROM recipes where id = $1`,
@@ -279,42 +175,17 @@ func SingleRecipe(w http.ResponseWriter, r *http.Request) {
panic(err)
}
- var status int
- var msg string
if ra, _ := res.RowsAffected(); ra == 0 {
- status = http.StatusNotFound
- msg = "Recipe Not found"
+ sendResponse(w, http.StatusNotFound, "Recipe Not found",
+ nil)
} else {
- status = http.StatusOK
- msg = "Recipe Deleted Successfully"
+ sendResponse(w, http.StatusOK,
+ "Recipe Deleted Successfully", nil)
}
- resp := APIResponseItem{
- Status: APIError{Code: status, Msg: msg},
- Data: make([]APIDataRecipe, 0),
- }
-
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusOK)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
- }
-
- return
- }
-
- resp := APIResponseItem{
- Status: APIError{Code: http.StatusMethodNotAllowed,
- Msg: "Invalid method"},
- Data: nil,
- }
-
- w.Header().Set("Content-Type",
- "application/json; charset=UTF-8")
- w.WriteHeader(http.StatusMethodNotAllowed)
- if err := json.NewEncoder(w).Encode(resp); err != nil {
- panic(err)
+ } else {
+ sendResponse(w, http.StatusMethodNotAllowed, "Invalid method",
+ nil)
}
}
@@ -335,8 +206,13 @@ func main() {
dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable",
DB_USER, DB_PASSWORD, DB_NAME)
db, err = sql.Open("postgres", dbinfo)
- if err != nil || db.Ping() != nil {
- fmt.Println("Error connecting to database")
+ if err != nil {
+ panic(err)
+ }
+
+ err = db.Ping()
+ if err != nil {
+ panic(err)
}
http.HandleFunc("/recipes", RecipeList)
diff --git a/backend/readme.adoc b/backend/readme.adoc
new file mode 100644
index 0000000..0dec0b2
--- /dev/null
+++ b/backend/readme.adoc
@@ -0,0 +1,197 @@
+Backend API
+===========
+Tucker Evans
+v1.0, November 22, 2019
+
+This REST API allows you to access recipe information in our database with
+simple HTTP requests. There is currently no authentication/authorization of
+clients. It return recipes in JSON format together with some status information
+about the request.
+
+JSON format
+-----------
+The current implementation expects (and returns) recipes in the form:
+
+.Recipe JSON
+[source,json]
+----
+{
+ "Id": 0,
+ "Title": "Recipe Title",
+ "Desc": "Recipe Description",
+ "Photos": [
+ "photo_url_1",
+ "photo_url_2"
+ ],
+ "Serving_size": 0,
+ "Cook_time": 0,
+ "Rating": 0,
+ "Num_cooked": 0,
+ "Keywords": [
+ "keyword 1",
+ "keyword 2",
+ "keyword 3"
+ ],
+ "Ingredients": [
+ {
+ "Name": "Ingredient 1 Name",
+ "Amount": 1.0,
+ "Unit": "Ingredient Units"
+ },
+ ],
+ "Steps": [
+ {
+ "Num": 0,
+ "Desc": "Step Instructions/Description",
+ "Time": 0
+ }
+ ]
+}
+
+----
+[NOTE]
+`"Id"` is not required for a POST request, and will be ignored.
+
+[IMPORTANT]
+Keywords and Photo URLs are currently stored as pipe separated values, the
+parsing of which is not complete and as such there is a extra empty string
+(`""`) is appended to these lists in the response (it is not required in
+requests)
+
+.Response JSON
+[source,json]
+----
+{
+ "Status": {
+ "Code": 200,
+ "Msg": "Successful"
+ },
+ "Data": "<DATA>"
+}
+----
+[NOTE]
+Data will either be a Recipe object or a list of recipe ids (null is also a
+valid value).
+
+Status Codes
+~~~~~~~~~~~~
+Status codes are based on https://httpstatuses.com/[HTTP status codes].
+
+.Currently Used
+- 200 OK
+- 201 Created
+- 400 Bad Request
+- 404 Not Found
+- 405 Method Not Allowed
+- 409 Conflict
+- 422 Unprocessable Entity
+- _500 Internal Server Error_ (not yet implemented)
+
+The messages included in the status section are meant to be human readable
+descriptions of any error.
+
+Usage
+-----
+This api is currently availiable with a base URL of
+http://api.recipebuddy.xyz:8888.
+
+CRUD Interface
+~~~~~~~~~~~~~~
+
+NOTE: Examples are run with a database that contains 1 recipe (you can see the
+ initial contents of this recipe in the read example).
+
+Create
+^^^^^^
+Creating a recipe is done by sending a `POST` HTTP request to the location
+http://api.recipebuddy.xyz:8888/recipes[`/recipes`], with a body containing a
+recipe object in JSON form:
+[source,bash]
+----
+$ curl -X POST api.recipebuddy.xyz:8888/recipes -d '
+{
+ "Title":"Test Recipe 2",
+ "Desc":"This is a descripiton for the test recipe",
+ "Photos":["photo_url_1","photo_url_2"],
+ "Serving_size":0,
+ "Cook_time":60,
+ "Rating":5,
+ "Keywords":["keyword_1", "keyword_2","keyword_3"],
+ "Ingredients":[
+ {"Name":"INGR 1","Amount":2.5,"Unit":"cups"},
+ {"Name":"INGR 2","Amount":1,"Unit":"oz"}
+ ],
+ "Steps":[
+ {"Num":1,"Desc":"Step 1: Do this first","Time":10}
+ ]
+}'
+
+{"Status":{"Code":201,"Msg":"Recipe added successfully"},"Data":{"Id":2,"Title":"Test Recipe 2","Desc":"This is a descripiton for the test recipe","Photos":["photo_url_1","photo_url_2"],"Serving_size":0,"Cook_time":60,"Rating":5,"Num_cooked":0,"Keywords":["keyword_1","keyword_2","keyword_3"],"Ingredients":[{"Name":"INGR 1","Amount":2.5,"Unit":"cups"},{"Name":"INGR 2","Amount":1,"Unit":"oz"}],"Steps":[{"Num":1,"Desc":"Step 1: Do this first","Time":10}]}}
+----
+
+Read
+^^^^
+Reading a recipe is done by sending a `GET` HTTP request to the location
+http://api.recipebuddy.xyz:8888/recipes/0[`/recipes/{id}`], the HTTP body is ignored.
+
+[source,bash]
+----
+$ curl -X GET api.recipebuddy.xyz:8888/recipes/1
+
+{"Status":{"Code":200,"Msg":"Successful"},"Data":{"Id":1,"Title":"Test Recipe","Desc":"This is a descripiton for the test recipe","Photos":["photo_url_1","photo_url_2",""],"Serving_size":0,"Cook_time":60,"Rating":5,"Num_cooked":0,"Keywords":["keyword_1","keyword_2","keyword_3",""],"Ingredients":[{"Name":"INGR 1","Amount":2.5,"Unit":"cups"},{"Name":"INGR 2","Amount":1,"Unit":"oz"}],"Steps":[{"Num":1,"Desc":"Step 1: Do this first","Time":10}]}}
+----
+
+To access a list of all recipe ids in the database send a `GET` request to
+http://api.recipebuddy.xyz:8888/recipes[`/recipes`], the HTTP body is ignored.
+[source,bash]
+----
+curl -X GET api.recipebuddy.xyz:8888/recipes
+{"Status":{"Code":200,"Msg":"Successful Request"},"Data":[1,2]}
+----
+
+Update
+^^^^^^
+Updating a recipe is done by sending a `PUT` HTTP request to
+http://api.recipebuddy.xyz:8888/recipes/0[`recipes/{id}`], the HTTP body should be a
+complete recipe in JSON form.
+[source,bash]
+----
+$ curl -X PUT localhost:8888/recipes/1 -d '
+{
+ "Id": 1,
+ "Title":"Test Recipe 1",
+ "Desc":"This is a descripiton for the test recipe",
+ "Photos":[ "photo_url_1", "photo_url_2" ],
+ "Serving_size":0,
+ "Cook_time":60,
+ "Rating":5,
+ "Keywords":[ "keyword_1", "keyword_2", "keyword_3" ],
+ "Ingredients":[
+ { "Name":"INGR 1", "Amount":2.5, "Unit":"cups" },
+ { "Name":"INGR 2", "Amount":1, "Unit":"oz" }
+ ],
+ "Steps":[
+ { "Num":0, "Desc":"Step 1: Do this first", "Time":10 }
+ ]
+}'
+
+{"Status":{"Code":201,"Msg":"Recipe added successfully"},"Data":{"Id":1,"Title":"Test Recipe 1","Desc":"This is a descripiton for the test recipe","Photos":["photo_url_1","photo_url_2"],"Serving_size":0,"Cook_time":60,"Rating":5,"Num_cooked":0,"Keywords":["keyword_1","keyword_2","keyword_3"],"Ingredients":[{"Name":"INGR 1","Amount":2.5,"Unit":"cups"},{"Name":"INGR 2","Amount":1,"Unit":"oz"}],"Steps":[{"Num":0,"Desc":"Step 1: Do this first","Time":10}]}}
+
+----
+[WARNING]
+Any recipe information not included in the request will be removed from the
+database.
+
+Delete
+^^^^^^
+Deleting a recipe is done by sending a `DELETE` HTTP request to
+http://api.recipebuddy.xyz:8888/recipes/0[`recipes/{id}`], the HTTP body is ignored.
+[source,bash]
+----
+$ curl -X DELETE api.recipebuddy.xyz:8888/recipes/2
+{"Status":{"Code":200,"Msg":"Recipe Deleted Successfully"},"Data":null}
+$ curl -X GET api.recipebuddy.xyz:8888/recipes
+{"Status":{"Code":200,"Msg":"Successful Request"},"Data":[1]}
+----
+[WARNING]
+This is currently a *HARD* delete.
diff --git a/backend/recipe.go b/backend/recipe.go
index fed83ca..a3191c3 100644
--- a/backend/recipe.go
+++ b/backend/recipe.go
@@ -1,33 +1,32 @@
package main
import "database/sql"
-import "errors"
import "strings"
type Ingredient struct {
- Name string
- Amount float64
- Unit string
+ Name string `json:"name"`
+ Amount float64 `json:"amount"`
+ Unit string `json:"units"`
+ Type string `json:"type"`
}
type Step struct {
- Num int
- Desc string
- Time int
+ Desc string `json:"instructions"`
+ Time int `json:"timer"`
}
type Recipe struct {
- Id int
- Title string
- Desc string
- Photos []string
- Serving_size int
- Cook_time int
- Rating int
- Num_cooked int
- Keywords []string
- Ingredients []Ingredient
- Steps []Step
+ Id int `json:"id"`
+ Title string `json:"name"`
+ Desc string `json:"description"`
+ Photos []string `json:"photos"`
+ Serving_size int `json:"servingSize"`
+ Cook_time int `json:"cookTime"`
+ Rating int `json:"rating"`
+ Num_cooked int `json:"timesCooked"`
+ Keywords []string `json:"tags"`
+ Ingredients []Ingredient `json:"ingredients"`
+ Steps []Step `json:"steps"`
}
func MakeRecipe() *Recipe {
@@ -114,13 +113,12 @@ func RecipeFromId(id int, db *sql.DB) *Recipe {
var num, timer int
rows_steps, err := db.Query(`SELECT step, description, timer
- FROM steps WHERE recipe_id = $1`, id)
+ FROM steps WHERE recipe_id = $1 ORDER BY step`, id)
defer rows_steps.Close()
if err == nil {
for rows_steps.Next() {
rows_steps.Scan(&num, &desc, &timer)
step = Step{
- Num: num,
Desc: desc,
Time: timer,
}
@@ -189,7 +187,7 @@ func AddRecipeDB(r *Recipe, db *sql.DB) error {
res, err := tx.Exec(`INSERT INTO steps
(step, description, timer, recipe_id)
VALUES ($1, $2, $3, $4)`,
- step.Num,
+ i,
step.Desc,
step.Time,
id,
@@ -279,10 +277,6 @@ func UpdateRecipeDB(r *Recipe, db *sql.DB) error {
}
for i, step := range r.Steps {
- if step.Num != 0 {
- tx.Rollback()
- return errors.New("invalid json Recipe")
- }
_, err := tx.Exec(`INSERT INTO steps
(step, description, timer, recipe_id)
VALUES ($1, $2, $3, $4)
diff --git a/backend/todo.txt b/backend/todo.txt
index cb074a4..ae57ec0 100644
--- a/backend/todo.txt
+++ b/backend/todo.txt
@@ -1 +1,3 @@
-Add Error responses for incorrect methods
+Refactor Response creation
+Handle PSV parsing (rm empty string at end, or rm last pipe when creating item)
+Fix Update (steps != 0)