Reorganize types

main
Drew Bednar 1 week ago
parent ea0138e827
commit 04489650ad

@ -3,6 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"git.runcible.io/learning/pulley/internal/data"
) )
// The logError() method is a generic helper for logging an error message along // The logError() method is a generic helper for logging an error message along
@ -20,8 +22,7 @@ func (app *application) logError(r *http.Request, err error) {
// type for the message parameter, rather than just a string type, as this gives us // type for the message parameter, rather than just a string type, as this gives us
// more flexibility over the values that we can include in the response. // more flexibility over the values that we can include in the response.
func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) { func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) {
env := envelope{"error": message} env := data.ErrorMessageResponse{ErrorMessage: message}
err := app.writeJSON(w, status, env, nil) err := app.writeJSON(w, status, env, nil)
if err != nil { if err != nil {
app.logError(r, err) app.logError(r, err)

@ -36,7 +36,7 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques
} }
// use intermediate struct for decoding, use Movie struct for validation // use intermediate struct for decoding, use Movie struct for validation
m := &data.Movie{ movie := &data.Movie{
Title: input.Title, Title: input.Title,
Year: input.Year, Year: input.Year,
Runtime: input.Runtime, Runtime: input.Runtime,
@ -45,24 +45,24 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques
v := validator.New() v := validator.New()
if data.ValidateMovie(v, m); !v.Valid() { if data.ValidateMovie(v, movie); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors) app.failedValidationResponse(w, r, v.Errors)
} }
// We can implement a timeout context if we would like here // We can implement a timeout context if we would like here
ctx := r.Context() ctx := r.Context()
err = app.models.Movies.Insert(ctx, m) err = app.models.Movies.Insert(ctx, movie)
if err != nil { if err != nil {
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
} }
// Include location headers in HTTP response for newly created resource // Include location headers in HTTP response for newly created resource
headers := make(http.Header) headers := make(http.Header)
headers.Set("Location", fmt.Sprintf("/v1/movies/%d", m.ID)) headers.Set("Location", fmt.Sprintf("/v1/movies/%d", movie.ID))
// Write a JSON 201 Response with movie data in the response body and Location header // Write a JSON 201 Response with movie data in the response body and Location header
err = app.writeJSON(w, http.StatusCreated, envelope{"movie": m}, headers) err = app.writeJSON(w, http.StatusCreated, data.MovieResponse{Movie: movie}, headers)
if err != nil { if err != nil {
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
} }
@ -112,7 +112,7 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
} }
} }
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) err = app.writeJSON(w, http.StatusOK, data.MovieResponse{Movie: movie}, nil)
if err != nil { if err != nil {
//http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
@ -192,7 +192,7 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques
return return
} }
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil) err = app.writeJSON(w, http.StatusOK, data.MovieResponse{Movie: movie}, nil)
if err != nil { if err != nil {
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
} }
@ -235,7 +235,7 @@ func (app *application) deleteMovieHandler(w http.ResponseWriter, r *http.Reques
// are machines then status code is alright. // are machines then status code is alright.
//w.WriteHeader(http.StatusAccepted) //w.WriteHeader(http.StatusAccepted)
err = app.writeJSON(w, http.StatusOK, envelope{"message": "movie successfully deleted"}, nil) err = app.writeJSON(w, http.StatusOK, data.GenericMessageResponse{Message: "movie successfully deleted"}, nil)
if err != nil { if err != nil {
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
} }
@ -274,7 +274,9 @@ func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request
app.serverErrorResponse(w, r, err) app.serverErrorResponse(w, r, err)
return return
} }
app.writeJSON(w, 200, envelope{"movies": movies, "metadata": metadata}, nil)
// app.writeJSON(w, 200, envelope{"movies": movies, "metadata": metadata}, nil)
app.writeJSON(w, 200, data.ListMoviesResponse{Metadata: metadata, Movies: movies}, nil)
} }
func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request) { func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
@ -282,13 +284,13 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques
// js := `{"status": "available", "environment": %q, "version": %q}` // js := `{"status": "available", "environment": %q, "version": %q}`
// js = fmt.Sprintf(js, app.config.env, Version) // js = fmt.Sprintf(js, app.config.env, Version)
// w.Write([]byte(js)) // w.Write([]byte(js))
env := envelope{ // env := envelope{
"status": "available", // "status": "available",
"system_info": map[string]string{ // "system_info": map[string]string{
"environment": app.config.Env, // "environment": app.config.Env,
"version": Version, // "version": Version,
}, // },
} // }
// js, err := json.Marshal(data) // js, err := json.Marshal(data)
// if err != nil { // if err != nil {
@ -301,7 +303,7 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques
// js = append(js, '\n') // js = append(js, '\n')
// w.Header().Set("Content-Type", "application/json") // w.Header().Set("Content-Type", "application/json")
// w.Write(js) // w.Write(js)
err := app.writeJSON(w, 200, env, nil) err := app.writeJSON(w, 200, data.HealthCheckResponse{Status: "available", SystemInfo: data.SystemInfo{Environment: app.config.Env, Version: Version}}, nil)
if err != nil { if err != nil {
//app.logger.Error(err.Error()) //app.logger.Error(err.Error())
//http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)

@ -45,13 +45,20 @@ func TestHttpListHandlers(t *testing.T) {
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NilError(t, err) assert.NilError(t, err)
/// If you use an anonymous struct be sure to use exported fields
content := make(map[string][]data.Movie) // the encoding/json package can only work with exported fields
// content := new(struct {
// Metadata data.Metadata
// Movies []data.Movie
// })
content := new(data.ListMoviesResponse)
err = json.Unmarshal(body, &content) err = json.Unmarshal(body, &content)
assert.NilError(t, err) assert.NilError(t, err)
t.Logf("Values: %v", content)
assert.Equal(t, len(content["movies"]), 1) assert.Equal(t, len(content.Movies), 1)
assert.MovieEqual(t, content["movies"][0], movies[0]) assert.MovieEqual(t, *content.Movies[0], movies[0])
assert.Equal(t, content.Metadata.CurrentPage, 1)
assert.Equal(t, content.Metadata.TotalRecords, 1)
}) })
} }

@ -344,14 +344,15 @@ func TestListMovieHandler(t *testing.T) {
defer resp.Body.Close() defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
assert.NilError(t, err) assert.NilError(t, err)
jsonContent := make(map[string][]data.Movie) // jsonContent := make(map[string][]data.Movie)
jsonContent := new(data.ListMoviesResponse)
err = json.Unmarshal(body, &jsonContent) err = json.Unmarshal(body, &jsonContent)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, len(jsonContent["movies"]), 2) assert.Equal(t, len(jsonContent.Movies), 2)
for i := range jsonContent["movies"] { for i := range jsonContent.Movies {
assert.MovieEqual(t, jsonContent["movies"][i], movies[i]) assert.MovieEqual(t, *jsonContent.Movies[i], movies[i])
} }
err = mockPool.ExpectationsWereMet() err = mockPool.ExpectationsWereMet()

@ -18,7 +18,9 @@ import (
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
type envelope map[string]any // We moved from a generic map envelope to specific types in internal/dto.go
// type envelope map[string]any
type envelope any
// This was the original function signature without enveloping // This was the original function signature without enveloping
// func (app *application) writeJSON(w http.ResponseWriter, status int, data any, headers http.Header) error { // func (app *application) writeJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {

@ -0,0 +1,28 @@
package data
type ListMoviesResponse struct {
Metadata Metadata `json:"metadata"`
Movies []*Movie `json:"movies"`
}
type MovieResponse struct {
Movie *Movie `json:"movie"`
}
type SystemInfo struct {
Environment string `json:"environment"`
Version string `json:"version"`
}
type HealthCheckResponse struct {
Status string `json:"status"`
SystemInfo SystemInfo `json:"system_info"`
}
type GenericMessageResponse struct {
Message string `json:"message"`
}
type ErrorMessageResponse struct {
ErrorMessage any `json:"error"`
}
Loading…
Cancel
Save