package main import ( "errors" "fmt" "net/http" "strconv" "greenlight.debuggingjon.dev/internal/data" "greenlight.debuggingjon.dev/internal/validator" ) func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) { var input struct { Title string `json:"title"` Year int32 `json:"year"` Runtime data.Runtime `json:"runtime"` Genres []string `json:"genres"` } err := app.readJSON(w, r, &input) if err != nil { app.badRequestResponse(w, r, err) return } // Copy the values from the input struct to a new Movie struct. movie := &data.Movie{ Title: input.Title, Year: input.Year, Runtime: input.Runtime, Genres: input.Genres, } // Initialize a new Validator. v := validator.New() // Call the ValidateMovie() function and return a response containing the errors if // any of the checks fail. if data.ValidateMovie(v, movie); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } // Call the Insert() method on our movies model, passing in a pointer to the // validated movie struct. This will create a record in the database and update the // movie struct with the system-generated information.” err = app.models.Movies.Insert(movie) if err != nil { app.serverErrorResponse(w, r, err) return } // When sending a HTTP response, we want to include a Location header to let the // client know which URL they can find the newly-created resource at. We make an // empty http.Header map and then use the Set() method to add a new Location header, // interpolating the system-generated ID for our new movie in the URL. headers := make(http.Header) headers.Set("Location", fmt.Sprintf("/v1/movies/%d", movie.ID)) // Write a JSON response with a 201 Created status code, the movie data in the // response body, and the Location header. err = app.writeJSON(w, http.StatusCreated, envelope{"movie": movie}, headers) if err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } // Call the Get() method to fetch the data for a specific movie. We also need to // use the errors.Is() function to check if it returns a data.ErrRecordNotFound // error, in which case we send a 404 Not Found response to the client. movie, err := app.models.Movies.Get(id) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.notFoundResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) if err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) { id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } // Retrieve the movie record as normal. movie, err := app.models.Movies.Get(id) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.notFoundResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } // If the request contains a X-Expected-Version header, verify that the movie // version in the database matches the expected version specified in the header. if r.Header.Get("X-Expected-Version") != "" { if strconv.FormatInt(int64(movie.Version), 32) != r.Header.Get("X-Expected-Version") { app.editConflictResponse(w, r) return } } // Use pointers for the Title, Year and Runtime fields. var input struct { Title *string `json:"title"` Year *int32 `json:"year"` Runtime *data.Runtime `json:"runtime"` Genres []string `json:"genres"` } // Decode the JSON as normal. err = app.readJSON(w, r, &input) if err != nil { app.badRequestResponse(w, r, err) return } // If the input.Title value is nil then we know that no corresponding "title" key/ // value pair was provided in the JSON request body. So we move on and leave the // movie record unchanged. Otherwise, we update the movie record with the new title // value. Importantly, because input.Title is a now a pointer to a string, we need // to dereference the pointer using the * operator to get the underlying value // before assigning it to our movie record. if input.Title != nil { movie.Title = *input.Title } // We also do the same for the other fields in the input struct. if input.Year != nil { movie.Year = *input.Year } if input.Runtime != nil { movie.Runtime = *input.Runtime } if input.Genres != nil { movie.Genres = input.Genres // Note that we don't need to dereference a slice. } v := validator.New() if data.ValidateMovie(v, movie); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } // Intercept any ErrEditConflict error and call the new editConflictResponse() // helper. err = app.models.Movies.Update(movie) if err != nil { switch { case errors.Is(err, data.ErrEditConflict): app.editConflictResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) if err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deleteMovieHandler(w http.ResponseWriter, r *http.Request) { // Extract the movie ID from the URL. id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } // Delete the movie from the database, sending a 404 Not Found response to the // client if there isn't a matching record. err = app.models.Movies.Delete(id) if err != nil { switch { case errors.Is(err, data.ErrRecordNotFound): app.notFoundResponse(w, r) default: app.serverErrorResponse(w, r, err) } return } // Return a 200 OK status code along with a success message. err = app.writeJSON(w, http.StatusOK, envelope{"message": "movie successfully deleted"}, nil) if err != nil { 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) } }