You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
9.7 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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/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, 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()
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, 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
}
// 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()
data.Snippet = snippet
renderTemplate(w, r, tc, http.StatusOK, "view.go.tmpl", data)
})
}
func handleSnippetCreateGet(tc *TemplateCache) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := newTemplateData()
// 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, 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
}
var form snippetCreateForm
// AUTOMATIC FORM PROCESSING
err = formDecoder.Decode(&form, r.PostForm)
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()
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 youll 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))
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
})
}