feat: get movies, url parametes parsing and validating, sql exeme querying

This commit is contained in:
2026-03-18 13:52:32 +01:00
parent 03f11029a1
commit 6b170a2705
9 changed files with 265 additions and 8 deletions
+59
View File
@@ -7,10 +7,12 @@ import (
"io"
"maps"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
"greenlight.debuggingjon.dev/internal/validator"
)
// Define an envelope type.
@@ -116,3 +118,60 @@ func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst any
return nil
}
// The readString() helper returns a string value from the query string, or the provided
// default value if no matching key could be found.
func (app *application) readString(qs url.Values, key string, defaultValue string) string {
// Extract the value for a given key from the query string. If no key exists this
// will return the empty string "".
s := qs.Get(key)
// If no key exists (or the value is empty) then return the default value.
if s == "" {
return defaultValue
}
// Otherwise return the string.
return s
}
// The readCSV() helper reads a string value from the query string and then splits it
// into a slice on the comma character. If no matching key could be found, it returns
// the provided default value.
func (app *application) readCSV(qs url.Values, key string, defaultValue []string) []string {
// Extract the value from the query string.
csv := qs.Get(key)
// If no key exists (or the value is empty) then return the default value.
if csv == "" {
return defaultValue
}
// Otherwise parse the value into a []string slice and return it.
return strings.Split(csv, ",")
}
// The readInt() helper reads a string value from the query string and converts it to an
// integer before returning. If no matching key could be found it returns the provided
// default value. If the value couldn't be converted to an integer, then we record an
// error message in the provided Validator instance.
func (app *application) readInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
// Extract the value from the query string.
s := qs.Get(key)
// If no key exists (or the value is empty) then return the default value.
if s == "" {
return defaultValue
}
// Try to convert the value to an int. If this fails, add an error message to the
// validator instance and return the default value.
i, err := strconv.Atoi(s)
if err != nil {
v.AddError(key, "must be an integer value")
return defaultValue
}
// Otherwise, return the converted integer value.
return i
}
+56
View File
@@ -209,3 +209,59 @@ func (app *application) deleteMovieHandler(w http.ResponseWriter, r *http.Reques
app.serverErrorResponse(w, r, err)
}
}
func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) {
// To keep things consistent with our other handlers, we'll define an input struct
// to hold the expected values from the request query string.
var input struct {
Title string
Genres []string
data.Filters
}
// Initialize a new Validator instance.
v := validator.New()
// Call r.URL.Query() to get the url.Values map containing the query string data.
qs := r.URL.Query()
// Use our helpers to extract the title and genres query string values, falling back
// to defaults of an empty string and an empty slice respectively if they are not
// provided by the client.
input.Title = app.readString(qs, "title", "")
input.Genres = app.readCSV(qs, "genres", []string{})
// Get the page and page_size query string values as integers. Notice that we set
// the default page value to 1 and default page_size to 20, and that we pass the
// validator instance as the final argument here.
input.Page = app.readInt(qs, "page", 1, v)
input.PageSize = app.readInt(qs, "page_size", 20, v)
// Extract the sort query string value, falling back to "id" if it is not provided
// by the client (which will imply a ascending sort on movie ID).
input.Sort = app.readString(qs, "sort", "id")
// Add the supported sort values for this endpoint to the sort safelist.
input.SortSafelist = []string{"id", "title", "year", "runtime", "-id", "-title", "-year", "-runtime"}
// Execute the validation checks on the Filters struct and send a response
// containing the errors if necessary.
if data.ValidateFilters(v, input.Filters); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
// Call the GetAll() method to retrieve the movies, passing in the various filter
// parameters.
movies, err := app.models.Movies.GetAll(input.Title, input.Genres, input.Filters)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
// Send a JSON response containing the movie data.
err = app.writeJSON(w, http.StatusOK, envelope{"movies": movies}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ func (app *application) routes() *httprouter.Router {
// Likewise, convert the methodNotAllowedResponse() helper to a http.Handler and set
// it as the custom error handler for 405 Method Not Allowed responses.
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler)
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)