From faca55f86624cd8fa02c184dfc2f61ac3cdaa639 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sun, 9 Nov 2025 14:34:37 -0500 Subject: [PATCH] Adding filter and refactoring functions for new import graph --- cmd/api/handlers.go | 26 +++++++++++++++++++++++-- cmd/api/helpers.go | 34 +++++++++++++++++++++++++++++++++ internal/data/filters.go | 18 +++++++++++++++++ internal/data/movies.go | 18 +++++++++++++++-- internal/validator/validator.go | 17 ----------------- 5 files changed, 92 insertions(+), 21 deletions(-) create mode 100644 internal/data/filters.go diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 9c2eb84..e5ddad9 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -45,7 +45,7 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques v := validator.New() - if validator.ValidateMovie(v, m); !v.Valid() { + if data.ValidateMovie(v, m); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) } @@ -177,7 +177,7 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques v := validator.New() - if validator.ValidateMovie(v, movie); !v.Valid() { + if data.ValidateMovie(v, movie); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } @@ -248,6 +248,28 @@ func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request app.logger.Debug("query params", "qp", qparams) } + //holds values from input string + var input struct { + Title string + Genres []string + data.Filters + } + + v := validator.New() + input.Title = app.readString(qparams, "title", "") + input.Genres = app.readCSV(qparams, "genres", []string{}) + input.Page = app.readInt(qparams, "page", 1, v) + input.PageSize = app.readInt(qparams, "page_size", 20, v) + input.Sort = app.readString(qparams, "sort", "id") + input.Filters.SortSafelist = []string{"id", "title", "year", "runtime", "-id", "-title", "-year", "-runtime"} + + data.ValidateFilters(v, input.Filters) + + if !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + movies, err := app.models.Movies.List(r.Context()) if err != nil { app.serverErrorResponse(w, r, err) diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index 56db86c..1d5d42a 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -10,9 +10,11 @@ import ( "fmt" "io" "net/http" + "net/url" "strconv" "strings" + "git.runcible.io/learning/pulley/internal/validator" "github.com/julienschmidt/httprouter" ) @@ -156,3 +158,35 @@ func (a *application) readIDParam(r *http.Request) (int64, error) { return id, err } + +func (a *application) readString(qs url.Values, key string, defaultValue string) string { + s := qs.Get(key) + if s == "" { + return defaultValue + } + return s +} + +func (a *application) readInt(qs url.Values, key string, defaultInt int, v *validator.Validator) int { + s := qs.Get(key) + if s == "" { + return defaultInt + } + + i, err := strconv.Atoi(s) + if err != nil { + v.AddError(key, "must be an integer value") + return defaultInt + } + return i + +} + +func (a *application) readCSV(qs url.Values, key string, defaultValue []string) []string { + s := qs.Get(key) + if s == "" { + return defaultValue + } + + return strings.Split(s, ",") +} diff --git a/internal/data/filters.go b/internal/data/filters.go new file mode 100644 index 0000000..0ce7690 --- /dev/null +++ b/internal/data/filters.go @@ -0,0 +1,18 @@ +package data + +import "git.runcible.io/learning/pulley/internal/validator" + +type Filters struct { + Page int + PageSize int + Sort string + SortSafelist []string +} + +func ValidateFilters(v *validator.Validator, f Filters) { + v.Check(f.Page > 0, "page", "must be greater than zero") + v.Check(f.Page < 10_000_000, "page", "must be a maximum of 10 million") + v.Check(f.PageSize > 0, "page_size", "must be greater than zero") + v.Check(f.PageSize <= 100, "page_size", "must be a maximum of 100") + v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort value") +} diff --git a/internal/data/movies.go b/internal/data/movies.go index 7d638b0..c14a94d 100644 --- a/internal/data/movies.go +++ b/internal/data/movies.go @@ -5,6 +5,7 @@ import ( "errors" "time" + "git.runcible.io/learning/pulley/internal/validator" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" @@ -52,8 +53,6 @@ func (m MovieModel) Get(ctx context.Context, id int64) (*Movie, error) { return nil, ErrRecordNotFound } - panic("OMG!") - query := ` SELECT id, created_at, title, year, runtime, genres, version FROM movies @@ -207,3 +206,18 @@ func (m MovieModel) List(ctx context.Context) ([]*Movie, error) { return movies, nil } + +func ValidateMovie(v *validator.Validator, m *Movie) { + + v.Check(m.Title != "", "title", "must be provided") + v.Check(len(m.Title) <= 500, "title", "must not be more than 500 bytes long") + v.Check(m.Year != 0, "year", "must be provided") + v.Check(m.Year >= 1888, "year", "must be greater than 1888") + v.Check(m.Year <= int32(time.Now().Year()), "year", "must not be in the future") + v.Check(m.Runtime != 0, "runtime", "must be provided") + v.Check(m.Runtime > 0, "runtime", "must be a positive integer") + v.Check(m.Genres != nil, "genres", "must be provided") + v.Check(len(m.Genres) >= 1, "genres", "must contain one genre") + v.Check(len(m.Genres) <= 5, "genres", "must not contain more than 5 genres") + v.Check(validator.Unique(m.Genres), "genres", "must not contain duplicate values") +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index efe4460..b694e1f 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -1,10 +1,8 @@ package validator import ( - "git.runcible.io/learning/pulley/internal/data" "regexp" "slices" - "time" ) // Declare a regular expression for sanity checking the format of email addresses (we'll @@ -63,18 +61,3 @@ func Unique[T comparable](values []T) bool { return len(values) == len(uniqueValues) } - -func ValidateMovie(v *Validator, m *data.Movie) { - - v.Check(m.Title != "", "title", "must be provided") - v.Check(len(m.Title) <= 500, "title", "must not be more than 500 bytes long") - v.Check(m.Year != 0, "year", "must be provided") - v.Check(m.Year >= 1888, "year", "must be greater than 1888") - v.Check(m.Year <= int32(time.Now().Year()), "year", "must not be in the future") - v.Check(m.Runtime != 0, "runtime", "must be provided") - v.Check(m.Runtime > 0, "runtime", "must be a positive integer") - v.Check(m.Genres != nil, "genres", "must be provided") - v.Check(len(m.Genres) >= 1, "genres", "must contain one genre") - v.Check(len(m.Genres) <= 5, "genres", "must not contain more than 5 genres") - v.Check(Unique(m.Genres), "genres", "must not contain duplicate values") -}