Adding enveloping responses
continuous-integration/drone/push Build is passing Details

main
Drew Bednar 4 weeks ago
parent 0ef484a7d4
commit a79876bdda

@ -17,4 +17,66 @@ Uses [CleanURLs](https://en.wikipedia.org/wiki/Clean_URL)
| POST | /v1/movies | createMovieHandler | Create a new movie |
| GET | /v1/movies/:id | showMovieHandler | Show the details of a specific movie |
| PUT | /v1/movies/:id | editMovieHandler | Update the details of a specific movie |
| DELETE | /v1/movies/:id | deleteMovieHandler | Delete a specific movie |
| DELETE | /v1/movies/:id | deleteMovieHandler | Delete a specific movie |
## Go JSON Mapping Reference
| Go type | JSON type |
|-----------------------------------------------|----------------------------|
| bool | JSON boolean |
| string | JSON string |
| int*, uint*, float*, rune | JSON number |
| arrays, non-nil slices | JSON array |
| structs, non-nil maps | JSON object |
| nil pointers, interface values, slices, maps | JSON null |
| chan, func, complex* | Not supported |
| time.Time | RFC3339-format JSON string |
| []byte | Base64-encoded JSON string |
- Go `time.Time` values (which are actually a struct behind the scenes) will be encoded as a JSON string in RFC 3339 format like "2020-11-08T06:27:59+01:00", rather than as a JSON object.
- A `[]byte` slice will be encoded as a base64-encoded JSON string, rather than as a JSON array. So, for example, a byte slice of `[]byte{'h','e','l','l','o'}` would appear as "aGVsbG8=" in the JSON output. The base64 encoding uses padding and the standard character set.
- Encoding of nested objects is supported. So, for example, if you have a slice of structs in Go that will encode to an array of objects in JSON.
- Channels, functions and complex number types cannot be encoded. If you try to do so, youll get a json.`UnsupportedTypeError` error at runtime.
- Any pointer values will encode as the value pointed to.
- Struct fields MUST be exported for them to be serialize
- If a struct field doesnt have an explicit value set, then the JSON-encoding of the zero value for the field will appear in the output.
- Its also possible to control the visibility of individual struct fields in the JSON by using the `omitzero` and `-` struct tag directives.
- The `-` (hyphen) directive can be used when you never want a particular struct field to appear in the JSON output. This is useful for fields that contain internal system information that isnt relevant to your users, or sensitive information that you dont want to expose (like the hash of a password)
- In contrast, the omitzero directive hides a field in the JSON output if and only if the value is the zero value for the field type. If you want to use omitzero and not change the key name then you can leave it blank in the struct tag — like this: `json:",omitzero"`. Notice that the leading comma is still required.
- An older `omiteby creating a custom envelope map with the underlying type map[string]any.mpty` struct field( Go <= 1.23) has some "stranger" behaviors that overlap with omitzero.
- `omitempty` will not omit structs, even if all the struct fields have their zero value.
- `omitempty` will not omit time.Time types, even if they have their zero value. Note that this is because the time.Time type is actually a struct behind-the-scenes, so this is really just a special case of the bullet point above.
- `omitempty` will not omit arrays, even if they have their zero value.
- `omitempty` will omit empty slices and maps (that is, initialized slices and maps with length zero) as well as nil slices and maps.
- A final, less-frequently-used, struct tag directive is `string`. You can use this on individual struct fields to force the data to be represented as a string in the JSON output.
- Note that the string directive will only work on struct fields which have `int*`, `uint*`, `float*` or `bool` types. For any other type of struct field it will have no effect.
- Check out the [appendix chapter](file:///home/toor/Desktop/lets-go-further-v1.24.1/lets-go-further.html/21.
04-json-encoding-nuances.html) for additional nuances on encoding types.
The pattern `err := json.NewEncoder(w).Encode(data)` looks elegant but creates issues when needing to coditionally set headers in response to an error. The performance difference between `json.Marshal` and `json.Encoder` are so small(tiny more mem and 1 extra alloc) that it's just cleaner to use `json.Marshal`.
### Enveloping Respsonses
```json
{
"movie": {
"id": 123,
"title": "Casablanca",
"runtime": 102,
"genres": [
"drama",
"romance",
"war"
],
"version":1
}
}
```
Enveloping response data like this isnt strictly necessary, and whether you choose to do so is partly a matter of style and taste. But there are a few tangible benefits:
1. Including a key name (like "movie") at the top-level of the JSON helps make the response more self-documenting. For any humans who see the response out of context, it is a bit easier to understand what the data relates to.
2. It reduces the risk of errors on the client side, because its harder to accidentally process one response thinking that it is something different. To get at the data, a client must explicitly reference it via the "movie" key.
3. If we always envelope the data returned by our API, then we mitigate a security vulnerability in older browsers which can arise if you return a JSON array as a response.

@ -4,7 +4,9 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"git.runcible.io/learning/pulley/internal/data"
"github.com/julienschmidt/httprouter"
)
@ -17,6 +19,8 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
// When httprouter is parsing a request, any interpolated URL parameters will be
// stored in the request context. We can use the ParamsFromContext() function to
// retrieve a slice containing these parameter names and values.
// TODO refactor id retrieval to an app.readIDParam receiver
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
@ -25,9 +29,21 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
http.NotFound(w, r)
return
}
app.logger.Debug((fmt.Sprintf("Using id: %d", id)))
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "show the details of movie %d\n", id)
movie := data.Movie{
ID: id,
CreatedAt: time.Now(),
Title: "Eisenhorn",
Runtime: 102,
Genres: []string{"sci-fi", "action"},
Version: 1,
}
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, 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)
}
}
func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
@ -35,10 +51,12 @@ 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))
data := map[string]string{
"status": "available",
"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)
@ -52,7 +70,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, data, nil)
err := app.writeJSON(w, 200, env, 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)

@ -38,11 +38,11 @@ func TestHealthRoute(t *testing.T) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NilError(t, err)
jsonContent := make(map[string]string)
jsonContent := make(map[string]any)
json.Unmarshal(body, &jsonContent)
assert.Equal(t, jsonContent["environment"], "test")
assert.Equal(t, jsonContent["status"], "available")
}
func TestCreateMovieHandler(t *testing.T) {

@ -5,9 +5,20 @@ import (
"net/http"
)
func (app *application) writeJSON(w http.ResponseWriter, status int, data any, headers http.Header) error {
type envelope map[string]any
// 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 envelope, headers http.Header) error {
// TODO set a flag for wether to indent or not. Indenting cost CPU time, more memory, and more allocs
// approximately 65% more time 30% more mem.
//js, err := json.Marshal(data)
// Use the json.MarshalIndent() function so that whitespace is added to the encoded
// JSON. Here we use no line prefix ("") and tab indents ("\t") for each element.
js, err := json.MarshalIndent(data, "", "\t")
js, err := json.Marshal(data)
if err != nil {
return err
}

@ -0,0 +1,15 @@
package data
import "time"
// MUST export fields to serialize them
type Movie struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"-"` // omits always
Title string `json:"title"`
Year int32 `json:"year,omitzero"`
Runtime int32 `json:"runtime,omitzero"`
Genres []string `json:"genres,omitzero"`
Version int32 `json:"version"`
}
Loading…
Cancel
Save