Skip to content

Commit

Permalink
Skimata's null relationships + fixes (google#62)
Browse files Browse the repository at this point in the history
* Add support to nullify relationship; http://jsonapi.org/format/#document-resource-object-linkage

* Fixed: [null] is not valid as an empty relationship.

* add support for 'omitempty' on relationships; default behavior of marshalling empty/nil relations (i.e. w/o 'omitempty' tag) marshals with null data relation

* cleanup whitespace

* Added go 1.7 to test versions; fixed the marshaling of empty relations to return an empty array rather than a null/nil. Added a more robust test case for the marshaling of non omitted relations.

* Cleanup.

* Added a comment to UnmarshalMany

* Document the ‘omitempty’ annotation on a relation.

* Add common JSON API values as exported jsonapi pkg constants.
  • Loading branch information
aren55555 authored and shwoodard committed Jan 21, 2017
1 parent bb88592 commit 2cb19b8
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 61 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
language: go
go:
- 1.4.3
- 1.5.3
- 1.4
- 1.5
- 1.6
- 1.7
- tip
script: go test -v .
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,16 @@ field when `count` has a value of `0`). Lastly, the spec indicates that
#### `relation`

```
`jsonapi:"relation,<key name in relationships hash>"`
`jsonapi:"relation,<key name in relationships hash>,<optional: omitempty>"`
```

Relations are struct fields that represent a one-to-one or one-to-many
relationship with other structs. JSON API will traverse the graph of
relationships and marshal or unmarshal records. The first argument must
be, `relation`, and the second should be the name of the relationship,
used as the key in the `relationships` hash for the record.
used as the key in the `relationships` hash for the record. The optional
third argument is `omitempty` - if present will prevent non existent to-one and
to-many from being serialized.

## Methods Reference

Expand Down
55 changes: 55 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package jsonapi

const (
// StructTag annotation strings
annotationJSONAPI = "jsonapi"
annotationPrimary = "primary"
annotationClientID = "client-id"
annotationAttribute = "attr"
annotationRelation = "relation"
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationSeperator = ","

iso8601TimeFormat = "2006-01-02T15:04:05Z"

// MediaType is the identifier for the JSON API media type
//
// see http://jsonapi.org/format/#document-structure
MediaType = "application/vnd.api+json"

// Pagination Constants
//
// http://jsonapi.org/format/#fetching-pagination

// KeyFirstPage is the key to the links object whose value contains a link to
// the first page of data
KeyFirstPage = "first"
// KeyLastPage is the key to the links object whose value contains a link to
// the last page of data
KeyLastPage = "last"
// KeyPreviousPage is the key to the links object whose value contains a link
// to the previous page of data
KeyPreviousPage = "prev"
// KeyNextPage is the key to the links object whose value contains a link to
// the next page of data
KeyNextPage = "next"

// QueryParamPageNumber is a JSON API query parameter used in a page based
// pagination strategy in conjunction with QueryParamPageSize
QueryParamPageNumber = "page[number]"
// QueryParamPageSize is a JSON API query parameter used in a page based
// pagination strategy in conjunction with QueryParamPageNumber
QueryParamPageSize = "page[size]"

// QueryParamPageOffset is a JSON API query parameter used in an offset based
// pagination strategy in conjunction with QueryParamPageLimit
QueryParamPageOffset = "page[offset]"
// QueryParamPageLimit is a JSON API query parameter used in an offset based
// pagination strategy in conjunction with QueryParamPageOffset
QueryParamPageLimit = "page[limit]"

// QueryParamPageCursor is a JSON API query parameter used with a cursor-based
// strategy
QueryParamPageCursor = "page[cursor]"
)
2 changes: 0 additions & 2 deletions node.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package jsonapi

const clientIDAnnotation = "client-id"

// OnePayload is used to represent a generic JSON API payload where a single
// resource (Node) was included as an {} in the "data" key
type OnePayload struct {
Expand Down
22 changes: 18 additions & 4 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ func UnmarshalPayload(in io.Reader, model interface{}) error {
return unmarshalNode(payload.Data, reflect.ValueOf(model), nil)
}

// UnmarshalManyPayload converts an io into a set of struct instances using
// jsonapi tags on the type's struct fields.
func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
payload := new(ManyPayload)

Expand Down Expand Up @@ -155,8 +157,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)

annotation := args[0]

if (annotation == clientIDAnnotation && len(args) != 1) ||
(annotation != clientIDAnnotation && len(args) < 2) {
if (annotation == annotationClientID && len(args) != 1) ||
(annotation != annotationClientID && len(args) < 2) {
er = ErrBadJSONAPIStructTag
break
}
Expand Down Expand Up @@ -244,7 +246,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}

assign(fieldValue, idValue)
} else if annotation == clientIDAnnotation {
} else if annotation == annotationClientID {
if data.ClientID == "" {
continue
}
Expand Down Expand Up @@ -472,6 +474,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}

if isSlice {
// to-many relationship
relationship := new(RelationshipManyNode)

buf := bytes.NewBuffer(nil)
Expand Down Expand Up @@ -499,6 +502,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)

fieldValue.Set(models)
} else {
// to-one relationships
relationship := new(RelationshipOneNode)

buf := bytes.NewBuffer(nil)
Expand All @@ -508,8 +512,17 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
)
json.NewDecoder(buf).Decode(relationship)

m := reflect.New(fieldValue.Type().Elem())
/*
http://jsonapi.org/format/#document-resource-object-relationships
http://jsonapi.org/format/#document-resource-object-linkage
relationship can have a data node set to null (e.g. to disassociate the relationship)
so unmarshal and set fieldValue only if data obj is not null
*/
if relationship.Data == nil {
continue
}

m := reflect.New(fieldValue.Type().Elem())
if err := unmarshalNode(
fullNode(relationship.Data, included),
m,
Expand All @@ -520,6 +533,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
}

fieldValue.Set(m)

}

} else {
Expand Down
66 changes: 66 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,72 @@ func TestUnmarshalRelationships(t *testing.T) {
}
}

func TestUnmarshalNullRelationship(t *testing.T) {
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "posts",
"id": "1",
"attributes": map[string]interface{}{
"body": "Hello",
"title": "World",
},
"relationships": map[string]interface{}{
"latest_comment": map[string]interface{}{
"data": nil, // empty to-one relationship
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}

in := bytes.NewReader(data)
out := new(Post)

if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}

if out.LatestComment != nil {
t.Fatalf("Latest Comment was not set to nil")
}
}

func TestUnmarshalNullRelationshipInSlice(t *testing.T) {
sample := map[string]interface{}{
"data": map[string]interface{}{
"type": "posts",
"id": "1",
"attributes": map[string]interface{}{
"body": "Hello",
"title": "World",
},
"relationships": map[string]interface{}{
"comments": map[string]interface{}{
"data": []interface{}{}, // empty to-many relationships
},
},
},
}
data, err := json.Marshal(sample)
if err != nil {
t.Fatal(err)
}

in := bytes.NewReader(data)
out := new(Post)

if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}

if len(out.Comments) != 0 {
t.Fatalf("Wrong number of comments; Comments should be empty")
}
}

func TestUnmarshalNestedRelationships(t *testing.T) {
out, err := unmarshalSamplePayload()
if err != nil {
Expand Down
Loading

0 comments on commit 2cb19b8

Please sign in to comment.