diff --git a/cmd/api/errors.go b/cmd/api/errors.go index b1cd733..fd650bb 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -3,6 +3,8 @@ package main import ( "fmt" "net/http" + + "git.runcible.io/learning/pulley/internal/data" ) // 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 // 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) { - env := envelope{"error": message} - + env := data.ErrorMessageResponse{ErrorMessage: message} err := app.writeJSON(w, status, env, nil) if err != nil { app.logError(r, err) diff --git a/cmd/api/handlers.go b/cmd/api/handlers.go index 7a7176f..07cd2bf 100644 --- a/cmd/api/handlers.go +++ b/cmd/api/handlers.go @@ -36,7 +36,7 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques } // use intermediate struct for decoding, use Movie struct for validation - m := &data.Movie{ + movie := &data.Movie{ Title: input.Title, Year: input.Year, Runtime: input.Runtime, @@ -45,24 +45,24 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques v := validator.New() - if data.ValidateMovie(v, m); !v.Valid() { + if data.ValidateMovie(v, movie); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) } // We can implement a timeout context if we would like here ctx := r.Context() - err = app.models.Movies.Insert(ctx, m) + err = app.models.Movies.Insert(ctx, movie) if err != nil { app.serverErrorResponse(w, r, err) } // Include location headers in HTTP response for newly created resource 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 - err = app.writeJSON(w, http.StatusCreated, envelope{"movie": m}, headers) + err = app.writeJSON(w, http.StatusCreated, data.MovieResponse{Movie: movie}, headers) if err != nil { 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 { //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) app.serverErrorResponse(w, r, err) @@ -192,7 +192,7 @@ func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Reques 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 { 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. //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 { app.serverErrorResponse(w, r, err) } @@ -274,7 +274,9 @@ func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request app.serverErrorResponse(w, r, err) 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) { @@ -282,13 +284,13 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques // js := `{"status": "available", "environment": %q, "version": %q}` // js = fmt.Sprintf(js, app.config.env, Version) // w.Write([]byte(js)) - env := envelope{ - "status": "available", - "system_info": map[string]string{ - "environment": app.config.Env, - "version": Version, - }, - } + // env := envelope{ + // "status": "available", + // "system_info": map[string]string{ + // "environment": app.config.Env, + // "version": Version, + // }, + // } // js, err := json.Marshal(data) // if err != nil { @@ -301,7 +303,7 @@ func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Reques // js = append(js, '\n') // w.Header().Set("Content-Type", "application/json") // 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 { //app.logger.Error(err.Error()) //http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError) diff --git a/cmd/api/handlers_integration_test.go b/cmd/api/handlers_integration_test.go index e0a9ace..53dd797 100644 --- a/cmd/api/handlers_integration_test.go +++ b/cmd/api/handlers_integration_test.go @@ -45,13 +45,20 @@ func TestHttpListHandlers(t *testing.T) { defer resp.Body.Close() body, err := io.ReadAll(resp.Body) assert.NilError(t, err) - - content := make(map[string][]data.Movie) + /// If you use an anonymous struct be sure to use exported fields + // 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) assert.NilError(t, err) - - assert.Equal(t, len(content["movies"]), 1) - assert.MovieEqual(t, content["movies"][0], movies[0]) + t.Logf("Values: %v", content) + assert.Equal(t, len(content.Movies), 1) + assert.MovieEqual(t, *content.Movies[0], movies[0]) + assert.Equal(t, content.Metadata.CurrentPage, 1) + assert.Equal(t, content.Metadata.TotalRecords, 1) }) } diff --git a/cmd/api/handlers_test.go b/cmd/api/handlers_test.go index 2ffca7a..7c3dd03 100644 --- a/cmd/api/handlers_test.go +++ b/cmd/api/handlers_test.go @@ -344,14 +344,15 @@ func TestListMovieHandler(t *testing.T) { defer resp.Body.Close() body, err := io.ReadAll(resp.Body) 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) assert.NilError(t, err) - assert.Equal(t, len(jsonContent["movies"]), 2) - for i := range jsonContent["movies"] { - assert.MovieEqual(t, jsonContent["movies"][i], movies[i]) + assert.Equal(t, len(jsonContent.Movies), 2) + for i := range jsonContent.Movies { + assert.MovieEqual(t, *jsonContent.Movies[i], movies[i]) } err = mockPool.ExpectationsWereMet() diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index 1d5d42a..1ae0c76 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -18,7 +18,9 @@ import ( "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 // func (app *application) writeJSON(w http.ResponseWriter, status int, data any, headers http.Header) error { diff --git a/internal/data/dto.go b/internal/data/dto.go new file mode 100644 index 0000000..923a18a --- /dev/null +++ b/internal/data/dto.go @@ -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"` +}