diff --git a/api/handlers.go b/api/handlers.go index 4aeefc8..bcca499 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2,107 +2,30 @@ package api import ( "context" - "encoding/base64" - "fmt" - "log" - "strings" "github.com/aws/aws-lambda-go/events" ) -type SimpleResponse struct { - Body string - StatusCode int -} - -// Testing handler for debugging -func TestRequestHandler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - - default_body := "Unknown request: " + request.HTTPMethod + "with path: " + request.Path - default_status := 404 - sr := SimpleResponse{Body: default_body, StatusCode: default_status} - - if request.HTTPMethod == "GET" { - switch request.Path { - case "/": - sr.Body = "Root hit" - sr.StatusCode = 200 - case "/search-recipes": - text := request.QueryStringParameters["text"] - sr.Body = "Search response hit with query: " + text - sr.StatusCode = 200 - default: - sr.Body = "Unknown path: " + request.Path - sr.StatusCode = 404 - } +func errorResponse(status int, message string) events.APIGatewayProxyResponse { + return events.APIGatewayProxyResponse{ + Body: message, + StatusCode: status, + Headers: map[string]string{"Content-Type": "text/plain"}, } - - return events.APIGatewayProxyResponse{Body: sr.Body, StatusCode: sr.StatusCode}, nil } -// Real handler for recipe App -func RecipeRequestHandler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { - - var requester Requester - - log.Printf("Path: %s", request.Path) - log.Printf("Method: %s", request.HTTPMethod) - - if request.HTTPMethod == "GET" { - switch request_path := request.Path; { - case request_path == "/": - requester = NewHtmlRequester(true, "") - case strings.HasPrefix(request_path, "/search-recipes"): - requester = NewHtmlRequester(false, request.QueryStringParameters["text"]) - case strings.HasPrefix(request_path, "/static"): - requester = TextRequester{} - case strings.HasPrefix(request_path, "/thumbnails"): - image_name, _ := strings.CutPrefix(request_path, "/") - requester = NewImageRequester(image_name) - default: - err_body := fmt.Sprintf("Unknown Path: %s", request.Path) - log.Print(err_body) - return events.APIGatewayProxyResponse{ - Body: err_body, - StatusCode: 404, - Headers: request.Headers, - }, nil - } - } else { - err_body := "Only expect GET requests" - log.Print(err_body) - return events.APIGatewayProxyResponse{ - Body: err_body, - StatusCode: 405, - Headers: request.Headers, - }, nil - } - - body, e := requester.RetrieveData() - if e != nil { - body_err := e.Error() - log.Print(body_err) - return events.APIGatewayProxyResponse{ - Body: body_err, - StatusCode: 405, - Headers: request.Headers, - }, nil +// This creates handles the actual lambda request and will deal with errors in this handler, any remaining uncaught errors +// should be caught by the lambda itself +func RequestHandler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + requester, err := RequesterFactory(request) + if err != nil { + return errorResponse(405, err.Error()), nil } - headers := map[string]string{"Content-Type": requester.ContentType()} - if requester.ContentType() == "image/jpeg" { - headers["Content-Length"] = fmt.Sprintf("%d", len(body)) - return events.APIGatewayProxyResponse{ - Body: base64.StdEncoding.EncodeToString(body), - StatusCode: 200, - Headers: headers, - IsBase64Encoded: true, - }, nil + resp, err := requester.Do(ctx, request) + if err != nil { + return errorResponse(500, err.Error()), nil } else { - return events.APIGatewayProxyResponse{ - Body: string(body), - StatusCode: 200, - Headers: headers, - }, nil + return resp, nil } } diff --git a/api/requesters.go b/api/requesters.go index 9a0fc1a..4428077 100644 --- a/api/requesters.go +++ b/api/requesters.go @@ -2,11 +2,17 @@ package api import ( "bytes" + "context" "embed" + "encoding/base64" + "errors" + "fmt" "html/template" "io/fs" + "log" "strings" + "github.com/aws/aws-lambda-go/events" "github.com/isichei/recipe-book/types" ) @@ -15,67 +21,104 @@ import ( var StaticResources embed.FS type Requester interface { - RetrieveData() ([]byte, error) - ContentType() string + Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) } -type HtmlRequester struct { - tmpl *template.Template - searchText string -} +func RequesterFactory(request events.APIGatewayProxyRequest) (Requester, error) { -func NewHtmlRequester(is_root bool, text string) HtmlRequester { - var h HtmlRequester + log.Printf("Path: %s", request.Path) + log.Printf("Method: %s", request.HTTPMethod) - if is_root { - h = HtmlRequester{ - template.Must(template.ParseFS(StaticResources, "templates/home.html", "templates/search_results.html")), - "", - } - } else { - h = HtmlRequester{ - template.Must(template.ParseFS(StaticResources, "templates/search_results.html")), - text, - } + if request.HTTPMethod != "GET" { + return nil, errors.New(fmt.Sprintf("API only accepts GET requests but got a %s request", request.HTTPMethod)) + } + var requester Requester + switch request_path := request.Path; { + case request_path == "/": + requester = homeRequester{} + case strings.HasPrefix(request_path, "/search-recipes"): + requester = searchRecipesRequester{} + case strings.HasPrefix(request_path, "/static"): + requester = staticRequester{} + case strings.HasPrefix(request_path, "/thumbnails"): + requester = imageRequester{} + default: + return nullRequester{}, errors.New(fmt.Sprintf("Recieved unexpected Path %s", request_path)) } - return h + return requester, nil } -func (h HtmlRequester) RetrieveData() ([]byte, error) { - var html bytes.Buffer +type nullRequester struct{} - e := h.tmpl.Execute(&html, searchRecipes(h.searchText)) - return html.Bytes(), e +func (nullRequester) Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{}, nil } -func (h HtmlRequester) ContentType() string { - return "text/html" -} +type homeRequester struct{} -type ImageRequester struct { - imagePath string +func (homeRequester) Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + h := template.Must(template.ParseFS(StaticResources, "templates/home.html", "templates/search_results.html")) + return htmlTemplateToResponse(h, searchRecipes("")) } -func NewImageRequester(imagePath string) ImageRequester { - return ImageRequester{imagePath} +type searchRecipesRequester struct{} + +func (searchRecipesRequester) Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + tmpl := template.Must(template.ParseFS(StaticResources, "templates/search_results.html")) + return htmlTemplateToResponse(tmpl, searchRecipes(request.QueryStringParameters["text"])) } -func (ir ImageRequester) RetrieveData() ([]byte, error) { - return fs.ReadFile(StaticResources, ir.imagePath) +type staticRequester struct{} + +func (staticRequester) Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + b, err := fs.ReadFile(StaticResources, "static/styles.css") + if err != nil { + return events.APIGatewayProxyResponse{}, err + } + headers := map[string]string{"Content-Type": "text/css"} + return events.APIGatewayProxyResponse{ + Body: string(b), + StatusCode: 200, + Headers: headers, + }, nil } -func (ir ImageRequester) ContentType() string { - return "image/jpeg" +type imageRequester struct{} + +func (imageRequester) Do(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + + image_name, _ := strings.CutPrefix(request.Path, "/") + b, err := fs.ReadFile(StaticResources, image_name) + if err != nil { + return events.APIGatewayProxyResponse{}, err + } + return imageToResponse(b), nil } -type TextRequester struct{} +func htmlTemplateToResponse(tmpl *template.Template, data any) (events.APIGatewayProxyResponse, error) { + var html bytes.Buffer + + e := tmpl.Execute(&html, data) -func (tr TextRequester) RetrieveData() ([]byte, error) { - return fs.ReadFile(StaticResources, "static/styles.css") + if e != nil { + return events.APIGatewayProxyResponse{}, e + } + headers := map[string]string{"Content-Type": "text/html"} + return events.APIGatewayProxyResponse{ + Body: html.String(), + StatusCode: 200, + Headers: headers, + }, nil } -func (tr TextRequester) ContentType() string { - return "text/css" +func imageToResponse(body []byte) events.APIGatewayProxyResponse { + headers := map[string]string{"Content-Type": "image/jpeg", "Content-Length": fmt.Sprintf("%d", len(body))} + return events.APIGatewayProxyResponse{ + Body: base64.StdEncoding.EncodeToString(body), + StatusCode: 200, + Headers: headers, + IsBase64Encoded: true, + } } // Todo move stuff around once lambdas are working as this duplicates storage package diff --git a/api/server.go b/api/server.go deleted file mode 100644 index 1a33c24..0000000 --- a/api/server.go +++ /dev/null @@ -1,75 +0,0 @@ -package api - -import ( - "html/template" - "log" - "net/http" - - "github.com/isichei/recipe-book/storage" -) - -type Server struct { - port string - dataStore storage.Storage -} - -func NewServer(listenPort string, dataStore storage.Storage) *Server { - return &Server{ - port: listenPort, - dataStore: dataStore, - } -} - -func (s *Server) Start() error { - - // serve static folder - static_fs := http.FileServer(http.Dir("./static")) - http.Handle("/static/", http.StripPrefix("/static/", static_fs)) - - // serve thumbnails folder - thumbnails_fs := http.FileServer(http.Dir("./thumbnails")) - http.Handle("/thumbnails/", http.StripPrefix("/thumbnails/", thumbnails_fs)) - - http.HandleFunc("/", s.rootHandler) - http.HandleFunc("/search-recipes", s.searchRecipesHandler) - - log.Println("Starting Recipe App at ", s.port) - return http.ListenAndServe(s.port, nil) -} - -// handler for root -func (s *Server) rootHandler(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL.String()) - - if r.Method != http.MethodGet { - http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) - return - } - - tmpl := template.Must(template.ParseFiles("templates/home.html", "templates/search_results.html")) - tmpl.Execute(w, s.dataStore.SearchRecipes("")) -} - -// handler for the search recipes -func (s *Server) searchRecipesHandler(w http.ResponseWriter, r *http.Request) { - - log.Println(r.URL.String()) - - if r.Method != http.MethodGet { - http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) - return - } - - // Parse the form data to retrieve the parameter value - err := r.ParseForm() - if err != nil { - http.Error(w, "Error parsing form data", http.StatusInternalServerError) - return - } - - recipeMetadata := s.dataStore.SearchRecipes(r.Form.Get("text")) - - tmpl := template.Must(template.ParseFiles("templates/search_results.html")) - - tmpl.Execute(w, recipeMetadata) -} diff --git a/main.go b/main.go index fd876ef..9e082ec 100644 --- a/main.go +++ b/main.go @@ -13,5 +13,5 @@ var staticResources embed.FS func main() { api.StaticResources = staticResources - lambda.Start(api.RecipeRequestHandler) + lambda.Start(api.RequestHandler) } diff --git a/main.tf b/main.tf index 63fa7fc..96ad44c 100644 --- a/main.tf +++ b/main.tf @@ -40,7 +40,7 @@ resource "aws_lambda_function" "recipe_app_lambda" { filename = data.archive_file.recipe_app_zip.output_path role = aws_iam_role.lambda_role.arn handler = "bootstrap" - runtime = "go1.x" + runtime = "provided.al2" timeout = 10 source_code_hash = filebase64sha256(data.archive_file.recipe_app_zip.output_path) } diff --git a/storage/storage.go b/storage/storage.go deleted file mode 100644 index 6d232ef..0000000 --- a/storage/storage.go +++ /dev/null @@ -1,47 +0,0 @@ -package storage - -import ( - "strings" - - "github.com/isichei/recipe-book/types" -) - -type Storage interface { - SearchRecipes(string) []types.RecipeMetadata -} - -type FakeStorage struct { - data []types.RecipeMetadata -} - -func NewFakeStorage() FakeStorage { - dhansak := types.RecipeMetadata{ - Uid: "chicken-dhansak-recipe", - Title: "Chicken Dhansak", - Description: "A chicken dhansak recipe from BBC good foods", - } - roast := types.RecipeMetadata{ - Uid: "christmas-roast-potatoes", - Title: "Jamie Oliver Roast Potatoes", - Description: "A jamie oliver roast potato recipe usually used at Christmas", - } - return FakeStorage{ - data: []types.RecipeMetadata{dhansak, roast}, - } -} - -func (store FakeStorage) SearchRecipes(text string) []types.RecipeMetadata { - - if text == "" { - return store.data - } else { - var filtered []types.RecipeMetadata - - for _, recipe := range store.data { - if strings.Contains(strings.ToLower(recipe.Description), strings.ToLower(text)) { - filtered = append(filtered, recipe) - } - } - return filtered - } -} diff --git a/storage/storage_test.go b/storage/storage_test.go deleted file mode 100644 index c97dc15..0000000 --- a/storage/storage_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package storage - -import ( - "testing" -) - -func TestFakeStorageMethod(t *testing.T) { - nfs := NewFakeStorage() - test_out := len(nfs.SearchRecipes("")) - if test_out != 2 { - t.Errorf("Expected 2 results got %d", test_out) - } - - test_out = len(nfs.SearchRecipes("chicken")) - if test_out != 1 { - t.Errorf("Expected 1 results got %d", test_out) - } -}