validator

main
Drew Bednar 2 months ago
parent ec2c95efb6
commit a75659f653

@ -3,3 +3,5 @@ module git.runcible.io/learning/ratchet
go 1.23.3
require github.com/mattn/go-sqlite3 v1.14.24
require github.com/go-playground/form/v4 v4.2.1 // indirect

@ -1,2 +1,5 @@
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form/v4 v4.2.1 h1:HjdRDKO0fftVMU5epjPW2SOREcZ6/wLUzEobqUGJuPw=
github.com/go-playground/form/v4 v4.2.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=

@ -1,13 +1,20 @@
// test
package server
import "git.runcible.io/learning/ratchet/internal/validator"
// Define a snippetCreateForm struct to represent the form data and validation
// errors for the form fields. Note that all the struct fields are deliberately
// exported (i.e. start with a capital letter). This is because struct fields
// must be exported in order to be read by the html/template package when
// rendering the template.
//
// Remove the explicit FieldErrors struct field and instead embed the Validator
// struct. Embedding this means that our snippetCreateForm "inherits" all the
// fields and methods of our Validator struct (including the FieldErrors field).
type snippetCreateForm struct {
Title string
Content string
Expires int
FieldErrors map[string]string
Title string
Content string
Expires int
validator.Validator
}

@ -1,3 +1,4 @@
// test
package server
import (
@ -6,10 +7,9 @@ import (
"log/slog"
"net/http"
"strconv"
"strings"
"unicode/utf8"
"git.runcible.io/learning/ratchet/internal/model"
"git.runcible.io/learning/ratchet/internal/validator"
)
// TODO function should accept and a pointer to an interface allowing for mocking in tests.
@ -187,32 +187,44 @@ func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, snippetServ
// 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,
FieldErrors: map[string]string{},
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
}
// VALIDATION
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"
}
// 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 {
// 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)

@ -0,0 +1,54 @@
// See also https://www.alexedwards.net/blog/validation-snippets-for-go
// for more validation snippets
package validator
import (
"slices"
"strings"
"unicode/utf8"
)
type Validator struct {
FieldErrors map[string]string
}
// Valid() returns true if the FieldErrors map doesn't contain any entries.
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
}
// AddFieldError() adds an error message to the FieldErrors map (so long as no
// entry already exists for the given key).
func (v *Validator) AddFieldError(key, message string) {
if v.FieldErrors == nil {
v.FieldErrors = make(map[string]string)
}
if _, exists := v.FieldErrors[key]; !exists {
v.FieldErrors[key] = message
}
}
// CheckField() adds an error message to the FieldErrors map only if a
// validation check is not 'ok'.
func (v *Validator) CheckField(ok bool, key, message string) {
if !ok {
v.AddFieldError(key, message)
}
}
// NotBlank() returns true if a value is not an empty string.
func NotBlank(value string) bool {
return strings.TrimSpace(value) != ""
}
// MaxChars() returns true if a value contains no more than n characters.
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
// PermittedValue() returns true if a value is in a list of specific permitted
// values.
func PermittedValue[T comparable](value T, permittedValues ...T) bool {
return slices.Contains(permittedValues, value)
}
Loading…
Cancel
Save