diff --git a/cmd/api/errors.go b/cmd/api/errors.go index 8df28d0..ed507d6 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -39,3 +39,7 @@ 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()) } + +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 467cd70..fae67b6 100644 --- a/cmd/api/movies.go +++ b/cmd/api/movies.go @@ -6,6 +6,7 @@ import ( "time" "gitea.local.lab/Lbenedar/greenlight/internal/data" + "gitea.local.lab/Lbenedar/greenlight/internal/validator" ) func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) { @@ -21,6 +22,20 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques app.badRequestResponse(w, r, err) return } + + movie := &data.Movie{ + Title: input.Title, + Year: input.Year, + Runtime: input.Runtime, + Genres: input.Genres, + } + + v := validator.New() + if data.ValidateMovie(v, movie); !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + fmt.Fprintf(w, "%+v\n", input) } diff --git a/internal/data/movies.go b/internal/data/movies.go index 040175c..1863222 100644 --- a/internal/data/movies.go +++ b/internal/data/movies.go @@ -1,6 +1,10 @@ package data -import "time" +import ( + "time" + + "gitea.local.lab/Lbenedar/greenlight/internal/validator" +) type Movie struct { ID int64 `json:"id"` @@ -11,3 +15,20 @@ type Movie struct { Genres []string `json:"genres,omitempty"` Version int32 `json:"version"` } + +func ValidateMovie(v *validator.Validator, movie *Movie) { + v.Check(movie.Title != "", "title", "must be provided") + v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long") + + v.Check(movie.Year != 0, "year", "must be provided") + v.Check(movie.Year >= 1988, "year", "must be greater than 1988") + v.Check(movie.Year <= int32(time.Now().Year()), "year", "must not be in the future") + + v.Check(movie.Runtime != 0, "runtime", "must be provided") + v.Check(movie.Runtime > 0, "runtime", "must be a positive integer") + + v.Check(movie.Genres != nil, "genres", "must be provided") + v.Check(len(movie.Genres) >= 1, "genres", "must contain at least 1 genre") + v.Check(len(movie.Genres) <= 5, "genres", "must not contain more than 5 genres") + v.Check(validator.Unique(movie.Genres), "genres", "must not contain duplicate values") +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go new file mode 100644 index 0000000..83294f1 --- /dev/null +++ b/internal/validator/validator.go @@ -0,0 +1,53 @@ +package validator + +import "regexp" + +var ( + EmailRX = regexp.MustCompile("^[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])?)*$") +) + +type Validator struct { + Errors map[string]string +} + +func New() *Validator { + return &Validator{Errors: make(map[string]string)} +} + +func (v *Validator) Valid() bool { + return len(v.Errors) == 0 +} + +func (v *Validator) AddError(key, message string) { + if _, exists := v.Errors[key]; !exists { + v.Errors[key] = message + } +} + +func (v *Validator) Check(ok bool, key, message string) { + if !ok { + v.AddError(key, message) + } +} + +func PermittedValue[T comparable](value T, permittedValues ...T) bool { + for i := range permittedValues { + if value == permittedValues[i] { + return true + } + } + return false +} + +func Matches(value string, rx *regexp.Regexp) bool { + return rx.MatchString(value) +} + +func Unique[T comparable](values []T) bool { + uniqueValues := make(map[T]bool) + + for _, value := range values { + uniqueValues[value] = true + } + return len(values) == len(uniqueValues) +}