From 43b7e2d986db0dac38e782ae6e81d747a5e36a66 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Fri, 24 Jan 2025 15:34:27 -0500 Subject: [PATCH] Making some decisions and refactoring --- internal/{domain/user => model}/user.go | 2 +- internal/{domain/user => model}/user_test.go | 2 +- internal/server/base_server.go | 149 ------------------- internal/server/handlers.go | 140 +++++++++++++++++ internal/server/helpers.go | 8 +- internal/server/routes.go | 28 ++++ internal/server/server.go | 47 ++++++ 7 files changed, 222 insertions(+), 154 deletions(-) rename internal/{domain/user => model}/user.go (99%) rename internal/{domain/user => model}/user_test.go (97%) delete mode 100644 internal/server/base_server.go create mode 100644 internal/server/handlers.go create mode 100644 internal/server/routes.go create mode 100644 internal/server/server.go diff --git a/internal/domain/user/user.go b/internal/model/user.go similarity index 99% rename from internal/domain/user/user.go rename to internal/model/user.go index 90b991b..ae3b28f 100644 --- a/internal/domain/user/user.go +++ b/internal/model/user.go @@ -1,4 +1,4 @@ -package user +package model import ( "context" diff --git a/internal/domain/user/user_test.go b/internal/model/user_test.go similarity index 97% rename from internal/domain/user/user_test.go rename to internal/model/user_test.go index c3c70e8..05559a4 100644 --- a/internal/domain/user/user_test.go +++ b/internal/model/user_test.go @@ -1,4 +1,4 @@ -package user +package model import ( "testing" diff --git a/internal/server/base_server.go b/internal/server/base_server.go deleted file mode 100644 index fcc836e..0000000 --- a/internal/server/base_server.go +++ /dev/null @@ -1,149 +0,0 @@ -package server - -import ( - "database/sql" - "errors" - "fmt" - "log/slog" - "net/http" - "strconv" - - "git.runcible.io/learning/ratchet/internal/domain/user" - "git.runcible.io/learning/ratchet/internal/model" -) - -type RatchetServer struct { - http.Handler - - logger *slog.Logger - //Services used by HTTP routes - snippetService *model.SnippetService - UserService user.UserService -} - -func NewRatchetServer(logger *slog.Logger, db *sql.DB) *RatchetServer { - rs := new(RatchetServer) - rs.logger = logger - rs.snippetService = &model.SnippetService{DB: db} - // TODO implement middleware that disables directory listings - fileServer := http.FileServer(http.Dir("./ui/static/")) - router := http.NewServeMux() - - // Subtree pattern for static assets - router.Handle("GET /static/", http.StripPrefix("/static/", fileServer)) - - // /{$} is used to prevent subtree path patterns from acting like a wildcard - // resulting in this route requiring an exact match on "/" only - // You can only include one HTTP method in a route pattern if you choose - // GET will match GET & HEAD http request methods - router.HandleFunc("GET /{$}", rs.home) - router.HandleFunc("GET /snippet/view/{id}", rs.snippetView) - router.HandleFunc("GET /snippet/create", rs.snippetCreate) - - // FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to - // the passed function - router.HandleFunc("POST /snippet/create", rs.snippetCreatePost) - - // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver. - rs.Handler = router - return rs -} - -func (rs *RatchetServer) home(w http.ResponseWriter, r *http.Request) { - // TODO middleware should be able to print out these lines for all routes - rs.logger.Info("request received", "method", "GET", "path", "/") - - w.Header().Add("Server", "Go") - - // Retrieve Snippets from DB - snippets, err := rs.snippetService.Lastest() - if err != err { - rs.serverError(w, r, err) - return - } - - rs.logger.Debug(fmt.Sprintf("%d snippets retrieved", len(snippets))) - - for _, snippet := range snippets { - fmt.Fprintf(w, "%+v\n", snippet) - } - - // // Initialize a slice containing the paths to the two files. It's important - // // to note that the file containing our base template must be the *first* - // // file in the slice. - // files := []string{ - // "./ui/html/base.go.tmpl", - // "./ui/html/partials/nav.go.tmpl", - // "./ui/html/pages/home.go.tmpl", - // } - - // // read template file into template set. - // ts, err := template.ParseFiles(files...) - // if err != nil { - // rs.serverError(w, r, err) - // return - // } - // // Write template content to response body - // err = ts.ExecuteTemplate(w, "base", nil) - // if err != nil { - // // This is the older more verbose way of doing what RatchetServer.serverError does - // // rs.logger.Error(err.Error()) - // // http.Error(w, "Internal Server Error", http.StatusInternalServerError) - // rs.serverError(w, r, err) - // } -} - -func (rs *RatchetServer) snippetView(w http.ResponseWriter, r *http.Request) { - - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil || id < 1 { - // http.NotFound(w, r) - rs.clientError(w, http.StatusNotFound) - return - } - - // Set a new cache-control header. If an existing "Cache-Control" header exists - // it will be overwritten. - // w.Header().Set("Cache-Control", "public, max-age=31536000") - - snippet, err := rs.snippetService.Get(id) - if err != nil { - rs.logger.Debug(fmt.Sprintf("Failed to retrieve an active record with id: %d", id)) - if errors.Is(err, model.ErrNoRecord) { - rs.clientError(w, http.StatusNotFound) - } else { - rs.serverError(w, r, err) - } - return - } - - // Write the snippet data as a plain-text HTTP response body. - fmt.Fprintf(w, "%+v", snippet) -} - -// snippetCreate handles display of the form used to create snippets -func (rs *RatchetServer) snippetCreate(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Create snippet form...")) -} - -// snippetCreate handles display of the form used to create snippets -// -// curl -iL -d "" http://localhost:5000/snippet/create -func (rs *RatchetServer) snippetCreatePost(w http.ResponseWriter, r *http.Request) { - // example of a custom header. Must be done before calling WriteHeader - // or they will fail to take effect. - w.Header().Add("Server", "Dirp") - // Create some variables holding dummy data. We'll remove these later on - // during the build. - title := "O snail" - content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa" - expires := 7 - - id, err := rs.snippetService.Insert(title, content, expires) - - if err != nil { - rs.serverError(w, r, err) - } - - http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) -} diff --git a/internal/server/handlers.go b/internal/server/handlers.go new file mode 100644 index 0000000..3bca7a3 --- /dev/null +++ b/internal/server/handlers.go @@ -0,0 +1,140 @@ +package server + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + "strconv" + + "git.runcible.io/learning/ratchet/internal/model" +) + +// TODO function should accept and a pointer to an interface allowing for mocking in tests. +func handleHome(logger *slog.Logger, snippetService *model.SnippetService) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + logger.Info("request received", "method", "GET", "path", "/") + // Just and example of adding a header + w.Header().Add("Server", "Go") + // Retrieve Snippets from DB + snippets, err := snippetService.Lastest() + if err != err { + serverError(w, r, err) + return + } + + logger.Debug(fmt.Sprintf("%d snippets retrieved", len(snippets))) + + for _, snippet := range snippets { + fmt.Fprintf(w, "%+v\n", snippet) + } + // // Initialize a slice containing the paths to the two files. It's important + // // to note that the file containing our base template must be the *first* + // // file in the slice. + // files := []string{ + // "./ui/html/base.go.tmpl", + // "./ui/html/partials/nav.go.tmpl", + // "./ui/html/pages/home.go.tmpl", + // } + + // // read template file into template set. + // ts, err := template.ParseFiles(files...) + // if err != nil { + // rs.serverError(w, r, err) + // return + // } + // // Write template content to response body + // err = ts.ExecuteTemplate(w, "base", nil) + // if err != nil { + // // This is the older more verbose way of doing what RatchetServer.serverError does + // // rs.logger.Error(err.Error()) + // // http.Error(w, "Internal Server Error", http.StatusInternalServerError) + // rs.serverError(w, r, err) + // } + }) +} + +func handleSnippetView(logger *slog.Logger, snippetService *model.SnippetService) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil || id < 1 { + clientError(w, http.StatusNotFound) + return + } + // Set a new cache-control header. If an existing "Cache-Control" header exists + // it will be overwritten. + // w.Header().Set("Cache-Control", "public, max-age=31536000") + + snippet, err := snippetService.Get(id) + if err != nil { + logger.Debug(fmt.Sprintf("Failed to retrieve an active record with id: %d", id)) + if errors.Is(err, model.ErrNoRecord) { + clientError(w, http.StatusNotFound) + } else { + serverError(w, r, err) + } + return + } + + // Write the snippet data as a plain-text HTTP response body. + fmt.Fprintf(w, "%+v", snippet) + + }) +} + +// // snippetCreate handles display of the form used to create snippets +// func (rs *RatchetServer) snippetCreate(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("Create snippet form...")) +// } + +func handleSnippetCreateGet() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Create snippet form..")) + }) +} + +// snippetCreate handles display of the form used to create snippets +// +// curl -iL -d "" http://localhost:5001/snippet/create +func handleSnippetCreatePost(logger *slog.Logger, snippetService *model.SnippetService) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // example of a custom header. Must be done before calling WriteHeader + // or they will fail to take effect. + w.Header().Add("Server", "Dirp") + // Create some variables holding dummy data. We'll remove these later on + // during the build. + title := "O snail" + content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa" + expires := 7 + + id, err := snippetService.Insert(title, content, expires) + if err != nil { + serverError(w, r, err) + } + logger.Info(fmt.Sprintf("Inserted record. id: %d", id)) + + http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) + }) +} + +// func (w http.ResponseWriter, r *http.Request) { +// // example of a custom header. Must be done before calling WriteHeader +// // or they will fail to take effect. +// w.Header().Add("Server", "Dirp") +// // Create some variables holding dummy data. We'll remove these later on +// // during the build. +// title := "O snail" +// content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa" +// expires := 7 + +// id, err := rs.snippetService.Insert(title, content, expires) + +// if err != nil { +// rs.serverError(w, r, err) +// } + +// http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) +// } diff --git a/internal/server/helpers.go b/internal/server/helpers.go index 2c235e4..979e3e5 100644 --- a/internal/server/helpers.go +++ b/internal/server/helpers.go @@ -1,6 +1,7 @@ package server import ( + "log/slog" "net/http" "runtime/debug" ) @@ -8,7 +9,8 @@ import ( // serverError helper writes a log entry at Error level (including the request // method and URI as attributes), then sends a generic 500 Internal Server Error // response to the user. -func (rs *RatchetServer) serverError(w http.ResponseWriter, r *http.Request, err error) { +func serverError(w http.ResponseWriter, r *http.Request, err error) { + logger := slog.Default() var ( method = r.Method uri = r.URL.RequestURI() @@ -17,13 +19,13 @@ func (rs *RatchetServer) serverError(w http.ResponseWriter, r *http.Request, err trace = string(debug.Stack()) ) - rs.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) + logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } // clientError helper sends a specific status code and corresponding description // to the user. We'll use this later in the book to send responses like 400 "Bad // Request" when there's a problem with the request that the user sent -func (rs *RatchetServer) clientError(w http.ResponseWriter, status int) { +func clientError(w http.ResponseWriter, status int) { http.Error(w, http.StatusText(status), status) } diff --git a/internal/server/routes.go b/internal/server/routes.go new file mode 100644 index 0000000..b472bf1 --- /dev/null +++ b/internal/server/routes.go @@ -0,0 +1,28 @@ +package server + +import ( + "database/sql" + "log/slog" + "net/http" + + "git.runcible.io/learning/ratchet/internal/model" +) + +func addRoutes(mux *http.ServeMux, + logger *slog.Logger, + db *sql.DB, + snippetService *model.SnippetService) http.Handler { + + // /{$} is used to prevent subtree path patterns from acting like a wildcard + // resulting in this route requiring an exact match on "/" only + // You can only include one HTTP method in a route pattern if you choose + // GET will match GET & HEAD http request methods + mux.Handle("GET /{$}", handleHome(logger, snippetService)) + mux.Handle("GET /snippet/view/{id}", handleSnippetView(logger, snippetService)) + mux.Handle("GET /snippet/create", handleSnippetCreateGet()) + mux.Handle("POST /snippet/create", handleSnippetCreatePost(logger, snippetService)) + // mux.Handle("/something", handleSomething(logger, config)) + // mux.Handle("/healthz", handleHealthzPlease(logger)) + // mux.Handle("/", http.NotFoundHandler()) + return mux +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..2f5f312 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,47 @@ +package server + +import ( + "database/sql" + "log/slog" + "net/http" + + "git.runcible.io/learning/ratchet/internal/model" +) + +type RatchetServer struct { + http.Handler + + logger *slog.Logger + //Services used by HTTP routes + snippetService *model.SnippetService + UserService model.UserService +} + +func NewRatchetServer(logger *slog.Logger, db *sql.DB) *RatchetServer { + rs := new(RatchetServer) + rs.logger = logger + rs.snippetService = &model.SnippetService{DB: db} + // TODO implement middleware that disables directory listings + fileServer := http.FileServer(http.Dir("./ui/static/")) + router := http.NewServeMux() + + // Subtree pattern for static assets + router.Handle("GET /static/", http.StripPrefix("/static/", fileServer)) + + // /{$} is used to prevent subtree path patterns from acting like a wildcard + // resulting in this route requiring an exact match on "/" only + // You can only include one HTTP method in a route pattern if you choose + // GET will match GET & HEAD http request methods + // router.HandleFunc("GET /{$}", rs.home) + // router.HandleFunc("GET /snippet/view/{id}", rs.snippetView) + // router.HandleFunc("GET /snippet/create", rs.snippetCreate) + + // FYI The http.HandlerFunc() adapter works by automatically adding a ServeHTTP() method to + // the passed function + // router.HandleFunc("POST /snippet/create", rs.snippetCreatePost) + + // Mux Router implements the Handler interface. AKA it has a ServeHTTP receiver. + // SEE we can really clean things up by moving this into routes.go and handlers.go + rs.Handler = addRoutes(router, rs.logger, db, rs.snippetService) + return rs +}