Skip to content

Commit

Permalink
docs: document NullableAttr and provide example usage
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrombley committed Jan 12, 2024
1 parent 2acfcfe commit 1e91421
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 8 deletions.
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,77 @@ func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
}
```

### Nullable attributes

Certain APIs may interpret the meaning of `null` attribute values as significantly
different from unspecified values (those that do not show up in the request).
The default use of the `omitempty` struct tag does not allow for sending
significant `null`s.

A type is provided for this purpose if needed: `NullableAttr[T]`. This type
provides an API for sending and receiving significant `null` values for
attribute values of any type.

In the example below, a payload is presented for a fictitious API that makes use
of significant `null` values. Once enabled, the `UnsettableTime` setting can
only be disabled by updating it to a `null` value.

The payload struct below makes use of a `NullableAttr` with an inner `time.Time`
to allow this behavior:

```go
type Settings struct {
ID int `jsonapi:"primary,videos"`
UnsettableTime jsonapi.NullableAttr[time.Time] `jsonapi:"attr,unsettable_time,rfc3339,omitempty"`
}
```

To enable the setting as described above, an instance of `time.Time` value is
sent to the API. This is done by using the exported
`NewNullableAttrWithValue[T]()` method:

```go
type Settings struct {
ID int `jsonapi:"primary,videos"`
UnsettableTime jsonapi.NullableAttr[time.Time] `jsonapi:"attr,unsettable_time,rfc3339,omitempty"`
}

s := Settings{
ID: 1,
UnsettableTime: jsonapi.NewNullableAttrWithValue[time.Time](time.Now()),
}
```

To disable the setting, a `null` value needs to be sent to the API. This is done
by using the exported `NewNullNullableAttr[T]()` method:

```go
s := Settings{
ID: 1,
UnsettableTime: jsonapi.NewNullNullableAttr[time.Time](),
}
```

Once a payload has been marshaled, the attribute value is flattened to a
primitive value:
```
"unsettable_time": "2021-01-01T02:07:14Z",
```

Significant nulls are also included and flattened, even when specifying `omitempty`:
```
"unsettable_time": null,
```

Once a payload is unmarshaled, the target attribute field is hydrated with
the value in the payload and can be retrieved with the `Get()` method:
```go
t, err := s.UnsettableTime.Get()
```

All other struct tags used in the attribute definition will be honored when
marshaling and unmarshaling non-null values for the inner type.

### Custom types

Custom types are supported for primitive types, only, as attributes. Examples,
Expand Down
22 changes: 22 additions & 0 deletions examples/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ func exerciseHandler() {
fmt.Println(buf.String())
fmt.Println("============== end raw jsonapi response =============")

// update
blog.UnsettableTime = jsonapi.NewNullableAttrWithValue[time.Time](time.Now())
in = bytes.NewBuffer(nil)
jsonapi.MarshalOnePayloadEmbedded(in, blog)

req, _ = http.NewRequest(http.MethodPatch, "/blogs", in)

req.Header.Set(headerAccept, jsonapi.MediaType)

w = httptest.NewRecorder()

fmt.Println("============ start update ===========")
http.DefaultServeMux.ServeHTTP(w, req)
fmt.Println("============ stop update ===========")

buf = bytes.NewBuffer(nil)
io.Copy(buf, w.Body)

fmt.Println("============ jsonapi response from update ===========")
fmt.Println(buf.String())
fmt.Println("============== end raw jsonapi response =============")

// echo
blogs := []interface{}{
fixtureBlogCreate(1),
Expand Down
4 changes: 3 additions & 1 deletion examples/fixtures.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import "time"
import (
"time"
)

func fixtureBlogCreate(i int) *Blog {
return &Blog{
Expand Down
25 changes: 25 additions & 0 deletions examples/handler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"net/http"
"strconv"

Expand All @@ -25,6 +26,8 @@ func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
methodHandler = h.createBlog
case http.MethodPatch:
methodHandler = h.updateBlog
case http.MethodPut:
methodHandler = h.echoBlogs
case http.MethodGet:
Expand Down Expand Up @@ -61,6 +64,28 @@ func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) {
}
}

func (h *ExampleHandler) updateBlog(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.update")

blog := new(Blog)

if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

fmt.Println(blog)

// ...do stuff with your blog...

w.WriteHeader(http.StatusCreated)
w.Header().Set(headerContentType, jsonapi.MediaType)

if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) {
jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list")
// ...fetch your blogs, filter, offset, limit, etc...
Expand Down
15 changes: 8 additions & 7 deletions examples/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (

// Blog is a model representing a blog site
type Blog struct {
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
ID int `jsonapi:"primary,blogs"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
UnsettableTime jsonapi.NullableAttr[time.Time] `jsonapi:"attr,unsettable_time,rfc3339,omitempty"`
ViewCount int `jsonapi:"attr,view_count"`
}

// Post is a model representing a post on a blog
Expand Down

0 comments on commit 1e91421

Please sign in to comment.