From e5ddfa120f9105977a54572c8d3d9f25fa7e2fb2 Mon Sep 17 00:00:00 2001 From: Maxime Delporte Date: Tue, 28 Oct 2025 11:04:18 +0100 Subject: [PATCH] Creating validator.go allowing us to validate received data from our endpoints. Updating errors.go adding failedValidationResponse method allowing us to deliver a StatusUnprocessableEntity error. Updating createMovieHandler with the use of our new validator package. --- cmd/api/errors.go | 5 +++ cmd/api/movies.go | 28 +++++++++++++- internal/validator/validator.go | 66 +++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 internal/validator/validator.go diff --git a/cmd/api/errors.go b/cmd/api/errors.go index 2c12cc8..8273418 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -51,3 +51,8 @@ func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http. func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { app.errorResponse(w, r, http.StatusBadRequest, err.Error()) } + +// The errors parameter here has the type map[string]string, which is exactly the same as the errors map contained in our Validator type. +func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) { + app.errorResponse(w, r, http.StatusUnprocessableEntity, errors) +} diff --git a/cmd/api/movies.go b/cmd/api/movies.go index 1c31d78..5f11530 100644 --- a/cmd/api/movies.go +++ b/cmd/api/movies.go @@ -3,6 +3,7 @@ package main import ( "fmt" "greenlight.craftr.fr/internal/data" + "greenlight.craftr.fr/internal/validator" "net/http" "time" ) @@ -19,12 +20,37 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques // Use the new readJSON() helper to decode the request body into the input struct. If this returns an error, we send the client the error message along with a 400 Bad Request status code, just like before. err := app.readJSON(w, r, &input) - if err != nil { app.badRequestResponse(w, r, err) return } + // Initialize a new Validator instance + v := validator.New() + + // Use the Check() method to execute our validation checks. This will add the provided key and error message to the errors maps if the check does not evaluate to true. For example, in the first line we "check that the title is not equal to an empty string". + v.Check(input.Title != "", "title", "must be provided") + v.Check(len(input.Title) <= 500, "title", "must not be more than 500 bytes long") + + v.Check(input.Year != 0, "year", "must be provided") + v.Check(input.Year >= 1888, "year", "must be greater than 1888") + v.Check(input.Year <= int32(time.Now().Year()), "year", "must not be in the future") + + v.Check(input.Runtime != 0, "runtime", "must be provided") + v.Check(input.Runtime > 0, "runtime", "must be a positive integer") + + v.Check(input.Genres != nil, "genres", "must be provided") + v.Check(len(input.Genres) >= 1, "genres", "must contain at least 1 genre") + v.Check(len(input.Genres) <= 5, "genres", "must not contain more than 5 genres") + // Not that we're using the Unique helper in the line below to check that all values in the input.Genres slice are unique + v.Check(validator.Unique(input.Genres), "genres", "must not contain duplicate values") + + // Use the Valid() method to see if any of the checks failed. If they did, then use the failedValidationResponse() helper to send a response to the client, passing in the v.Errors map. + if !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + // Dump the contents of the input struct in a HTTP response. fmt.Fprintf(w, "%+v\n", input) } diff --git a/internal/validator/validator.go b/internal/validator/validator.go new file mode 100644 index 0000000..5d35139 --- /dev/null +++ b/internal/validator/validator.go @@ -0,0 +1,66 @@ +package validator + +import ( + "regexp" + "slices" +) + +// Declare a regular expression for sanity checking the format of email addresses. This regular expression pattern is taken from https://html.spec.whatwg.org/#valid-e-mail-address. +/* +Regex pour l'e-mail à tester : +1. "^[a-zA-Z0-9.!#$%&'*+/=?^`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" +2. `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$` +*/ +var ( + EmailTX, _ = regexp.Compile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) +) + +// Validator : Contains a map of validation errors +type Validator struct { + Errors map[string]string +} + +// New is a helper which creates a new Validator instance with an empty errors map +func New() *Validator { + return &Validator{Errors: make(map[string]string)} +} + +// Valid returns true if the errors map doesn't contain any entries +func (v *Validator) Valid() bool { + return len(v.Errors) == 0 +} + +// AddError adds an error message to the map (so long as no entry already exists for the given key) +func (v *Validator) AddError(key, message string) { + if _, exists := v.Errors[key]; !exists { + v.Errors[key] = message + } +} + +// Check adds an error message to the map only if a validation check is not 'ok' +func (v *Validator) Check(ok bool, key, message string) { + if !ok { + v.AddError(key, message) + } +} + +// PermittedValue is a generic function which returns true if a specific value is in a list of permitted values +func PermittedValue[T comparable](value T, permittedValues ...T) bool { + return slices.Contains(permittedValues, value) +} + +// Matches returns true if a string value matches a specific regexp pattern +func Matches(value string, rx *regexp.Regexp) bool { + return rx.MatchString(value) +} + +// Unique is a generic function which returns true if all values in a slice are unique +func Unique[T comparable](values []T) bool { + uniqueValues := make(map[T]bool) + + for _, value := range values { + uniqueValues[value] = true + } + + return len(values) == len(uniqueValues) +}