// test package server import ( "errors" "fmt" "log/slog" "net/http" "strconv" "git.runcible.io/learning/ratchet/internal/model" "git.runcible.io/learning/ratchet/internal/validator" "github.com/alexedwards/scs/v2" "github.com/go-playground/form/v4" ) // TODO function should accept and a pointer to an interface allowing for mocking in tests. func handleHome(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { // 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))) // Old way. We want default data so // data := templateData{ // Snippets: snippets, // } data := newTemplateData(r, sm) data.Snippets = snippets renderTemplate(w, r, tc, http.StatusOK, "home.go.tmpl", data) // // 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...) // ts, err := parseTemplateFiles(files...) // if err != nil { // serverError(w, r, err) // return // } // // Write template content to response body // err = ts.ExecuteTemplate(w, "base", data) // 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) // serverError(w, r, err) // } }) } func handleSnippetView(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, 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 } // Use the PopString() method to retrieve the value for the "flash" key. // PopString() also deletes the key and value from the session data, so it // acts like a one-time fetch. If there is no matching key in the session // data this will return the empty string. // See also GetInt, GetBool, GetBytes, GetTime etc. // NOW DONE IN TEMPLATE DATA FUNC // flash := sm.PopString(r.Context(), "flash") // files := []string{ // "./ui/html/base.go.tmpl", // "./ui/html/partials/nav.go.tmpl", // "./ui/html/pages/view.go.tmpl", // } // //ts, err := template.ParseFiles(files...) // ts, err := parseTemplateFiles(files...) // if err != nil { // serverError(w, r, err) // return // } // data := templateData{ // Snippet: snippet, // } // logger.Debug(fmt.Sprintf("created template: %s", ts.Name())) // Any data that you pass as the final parameter to ts.ExecuteTemplate() // is represented within your HTML templates by the . character (referred to as dot). // In this specific case, the underlying type of dot will be a models.Snippet struct. // When the underlying type of dot is a struct, you can render (or yield) the value // of any exported field in your templates by postfixing dot with the field name // field, we could yield the snippet title by writing {{.Title}} in our templates. // err = ts.ExecuteTemplate(w, "base", data) // if err != nil { // serverError(w, r, err) // } // data := templateData{ // Snippet: snippet, // } data := newTemplateData(r, sm) data.Snippet = snippet // data.Flash = flash renderTemplate(w, r, tc, http.StatusOK, "view.go.tmpl", data) }) } func handleSnippetCreateGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data := newTemplateData(r, sm) // Initialize a new snippetCreateForm instance and pass it to the template. // Notice how this is also a great opportunity to set any default or // 'initial' values for the form --- here we set the initial value for the // snippet expiry to 365 days. data.Form = snippetCreateForm{ Expires: 365, } renderTemplate(w, r, tc, http.StatusOK, "create.go.tmpl", data) }) } // snippetCreate handles display of the form used to create snippets // // curl -iL -d "" http://localhost:5001/snippet/create func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder *form.Decoder, sm *scs.SessionManager, 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") // Limit the request body size to 4096 bytes // by default it's 10mb unless enctype="multipart/form-data in which case // thier is no limit. You would do this however to limit the number of bytes // that r.ParseForm will process. In this case it is only the first 4096 bytes // reading beyond that will cause the MaxBytesReader to throw and error surfaced // in your call to r.ParseForm() // Also if the limit is reached the reader flags the server which will terminate // the under lying TCP connection. r.Body = http.MaxBytesReader(w, r.Body, 4096) // ParseForm is idempotent. Can be called many times. // First we call r.ParseForm() which adds any data in POST request bodies // to the r.PostForm map. This also works in the same way for PUT and PATCH // requests. If there are any errors, we use our app.ClientError() helper to // send a 400 Bad Request response to the user. // If you have a form that submits data using the HTTP method GET, rather than POST, // the form data will be included as URL query string parameters /foo/bar?title=value&content=value. // can be retrieved using r.URL.Query().Get() which will return the value or an empty string. // r.Form could be used, but it is clearer and more explicit to stick with r.URL.Query and r.PostForm // to access submitted data. err := r.ParseForm() if err != nil { clientError(w, http.StatusBadRequest) return } // initializes the struct with zero values. So &form is non-nil var form snippetCreateForm // AUTOMATIC FORM PROCESSING // When we call app.formDecoder.Decode() it requires a non-nil pointer as the target // decode destination. If we try to pass in something that isn’t a non-nil pointer, then // Decode() will return a form.InvalidDecoderError error. That error should be handled // differently form a 400 error since it's a server side problem (ie 5xx error) err = decodePostForm(r, formDecoder, &form) if err != nil { clientError(w, http.StatusBadRequest) return } // OLD WAY // The r.PostForm.Get() method always returns the form data as a *string*. // However, we're expecting our expires value to be a number, and want to // represent it in our Go code as an integer. So we need to manually convert // the form data to an integer using strconv.Atoi(), and we send a 400 Bad // Request response if the conversion fails. // expires, err := strconv.Atoi(r.PostForm.Get("expires")) // if err != nil { // clientError(w, http.StatusBadRequest) // return // } // Create an instance of the snippetCreateForm struct containing the values // from the form and an empty map for any validation errors. // form := snippetCreateForm{ // Title: r.PostForm.Get("title"), // Content: r.PostForm.Get("content"), // Expires: expires, // } // VALIDATION // THE OLD WAY // if strings.TrimSpace(form.Title) == "" { // form.FieldErrors["title"] = "field cannot be blank" // // we want to count the number of unicode code points not bytes in the string // } else if utf8.RuneCountInString(form.Title) > 100 { // form.FieldErrors["title"] = "This field cannot exceed 100 characters" // } // if strings.TrimSpace(form.Content) == "" { // form.FieldErrors["content"] = "field cannot be blank" // } // if expires != 1 && expires != 7 && expires != 365 { // form.FieldErrors["expires"] = "This field must equal 1,7, or 365" // } // If there are any validation errors, then re-display the create.tmpl template, // passing in the snippetCreateForm instance as dynamic data in the Form // field. Note that we use the HTTP status code 422 Unprocessable Entity // when sending the response to indicate that there was a validation error. // if len(form.FieldErrors) > 0 { // data := newTemplateData() // data.Form = form // renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data) // return // } // New VALIDATION form.CheckField(validator.NotBlank(form.Title), "title", "this field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "this field cannot exceed 100 characters") form.CheckField(validator.NotBlank(form.Content), "content", "this field cannot be blank") form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "this field cannot be blank") if !form.Valid() { data := newTemplateData(r, sm) data.Form = form renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data) return } // NOTE // // In the case of a form field with multiple values you’ll need to work with the // r.PostForm map directly. The underlying type of the r.PostForm map is url.Values, // which in turn has the underlying type map[string][]string. So, for fields with // multiple values you can loop over the underlying map to access them like so: // for i, item := range r.PostForm["items"] { // fmt.Fprintf(w, "%d: Item %s\n", i, item) // } id, err := snippetService.Insert(form.Title, form.Content, form.Expires) if err != nil { serverError(w, r, err) } logger.Info(fmt.Sprintf("Inserted record. id: %d", id)) // Use the Put() method to add a string value ("Snippet successfully // created!") and the corresponding key ("flash") to the session data. sm.Put(r.Context(), "flash", "Snippet successfully created!") http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }) }