forked from Mirrors/jsonapi
Skimata's null relationships + fixes (#62)
* 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.
This commit is contained in:
parent
bb88592460
commit
2cb19b89ed
|
@ -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 .
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
node.go
2
node.go
|
@ -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 {
|
||||
|
|
22
request.go
22
request.go
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -520,6 +533,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
}
|
||||
|
||||
fieldValue.Set(m)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
|
@ -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 {
|
||||
|
|
115
response.go
115
response.go
|
@ -25,8 +25,6 @@ var (
|
|||
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
|
||||
)
|
||||
|
||||
const iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
||||
|
||||
// MarshalOnePayload writes a jsonapi response with one, with related records
|
||||
// sideloaded, into "included" array. This method encodes a response for a
|
||||
// single record only. Hence, data will be a single record rather than an array
|
||||
|
@ -176,7 +174,8 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) {
|
||||
func visitModelNode(model interface{}, included *map[string]*Node,
|
||||
sideload bool) (*Node, error) {
|
||||
node := new(Node)
|
||||
|
||||
var er error
|
||||
|
@ -186,7 +185,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
|
||||
for i := 0; i < modelValue.NumField(); i++ {
|
||||
structField := modelValue.Type().Field(i)
|
||||
tag := structField.Tag.Get("jsonapi")
|
||||
tag := structField.Tag.Get(annotationJSONAPI)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
@ -194,7 +193,7 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
fieldValue := modelValue.Field(i)
|
||||
fieldType := modelType.Field(i)
|
||||
|
||||
args := strings.Split(tag, ",")
|
||||
args := strings.Split(tag, annotationSeperator)
|
||||
|
||||
if len(args) < 1 {
|
||||
er = ErrBadJSONAPIStructTag
|
||||
|
@ -203,13 +202,13 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
if annotation == "primary" {
|
||||
if annotation == annotationPrimary {
|
||||
v := fieldValue
|
||||
|
||||
// Deal with PTRS
|
||||
|
@ -253,20 +252,20 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
}
|
||||
|
||||
node.Type = args[1]
|
||||
} else if annotation == clientIDAnnotation {
|
||||
} else if annotation == annotationClientID {
|
||||
clientID := fieldValue.String()
|
||||
if clientID != "" {
|
||||
node.ClientID = clientID
|
||||
}
|
||||
} else if annotation == "attr" {
|
||||
} else if annotation == annotationAttribute {
|
||||
var omitEmpty, iso8601 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
switch arg {
|
||||
case "omitempty":
|
||||
case annotationOmitEmpty:
|
||||
omitEmpty = true
|
||||
case "iso8601":
|
||||
case annotationISO8601:
|
||||
iso8601 = true
|
||||
}
|
||||
}
|
||||
|
@ -325,10 +324,18 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
node.Attributes[args[1]] = fieldValue.Interface()
|
||||
}
|
||||
}
|
||||
} else if annotation == "relation" {
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
} else if annotation == annotationRelation {
|
||||
var omitEmpty bool
|
||||
|
||||
if (isSlice && fieldValue.Len() < 1) || (!isSlice && fieldValue.IsNil()) {
|
||||
//add support for 'omitempty' struct tag for marshaling as absent
|
||||
if len(args) > 2 {
|
||||
omitEmpty = args[2] == annotationOmitEmpty
|
||||
}
|
||||
|
||||
isSlice := fieldValue.Type().Kind() == reflect.Slice
|
||||
if omitEmpty &&
|
||||
(isSlice && fieldValue.Len() < 1 ||
|
||||
(!isSlice && fieldValue.IsNil())) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -337,53 +344,58 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
}
|
||||
|
||||
if isSlice {
|
||||
// to-many relationship
|
||||
relationship, err := visitModelNodeRelationships(
|
||||
args[1],
|
||||
fieldValue,
|
||||
included,
|
||||
sideload,
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
d := relationship.Data
|
||||
if sideload {
|
||||
var shallowNodes []*Node
|
||||
|
||||
for _, n := range d {
|
||||
appendIncluded(included, n)
|
||||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||
}
|
||||
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||
Data: shallowNodes,
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = relationship
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
if sideload {
|
||||
shallowNodes := []*Node{}
|
||||
for _, n := range relationship.Data {
|
||||
appendIncluded(included, n)
|
||||
shallowNodes = append(shallowNodes, toShallowNode(n))
|
||||
}
|
||||
|
||||
node.Relationships[args[1]] = &RelationshipManyNode{
|
||||
Data: shallowNodes,
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = relationship
|
||||
}
|
||||
} else {
|
||||
// to-one relationships
|
||||
|
||||
// Handle null relationship case
|
||||
if fieldValue.IsNil() {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: nil}
|
||||
continue
|
||||
}
|
||||
|
||||
relationship, err := visitModelNode(
|
||||
fieldValue.Interface(),
|
||||
included,
|
||||
sideload)
|
||||
if err == nil {
|
||||
if sideload {
|
||||
appendIncluded(included, relationship)
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: toShallowNode(relationship),
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: relationship,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sideload,
|
||||
)
|
||||
if err != nil {
|
||||
er = err
|
||||
break
|
||||
}
|
||||
|
||||
if sideload {
|
||||
appendIncluded(included, relationship)
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{
|
||||
Data: toShallowNode(relationship),
|
||||
}
|
||||
} else {
|
||||
node.Relationships[args[1]] = &RelationshipOneNode{Data: relationship}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -395,7 +407,6 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
if er != nil {
|
||||
return nil, er
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
|
@ -406,15 +417,13 @@ func toShallowNode(node *Node) *Node {
|
|||
}
|
||||
}
|
||||
|
||||
func visitModelNodeRelationships(relationName string, models reflect.Value, included *map[string]*Node, sideload bool) (*RelationshipManyNode, error) {
|
||||
var nodes []*Node
|
||||
|
||||
if models.Len() == 0 {
|
||||
nodes = make([]*Node, 0)
|
||||
}
|
||||
func visitModelNodeRelationships(relationName string, models reflect.Value,
|
||||
included *map[string]*Node, sideload bool) (*RelationshipManyNode, error) {
|
||||
nodes := []*Node{}
|
||||
|
||||
for i := 0; i < models.Len(); i++ {
|
||||
n := models.Index(i).Interface()
|
||||
|
||||
node, err := visitModelNode(n, included, sideload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
121
response_test.go
121
response_test.go
|
@ -47,6 +47,127 @@ type Book struct {
|
|||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) {
|
||||
blog := &Blog{}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalOnePayload(out, blog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{})
|
||||
|
||||
// Verifiy the "posts" relation was an empty array
|
||||
posts, ok := relationships["posts"]
|
||||
if !ok {
|
||||
t.Fatal("Was expecting the data.relationships.posts key/value to have been present")
|
||||
}
|
||||
postsMap, ok := posts.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("data.relationships.posts was not a map")
|
||||
}
|
||||
postsData, ok := postsMap["data"]
|
||||
if !ok {
|
||||
t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present")
|
||||
}
|
||||
postsDataSlice, ok := postsData.([]interface{})
|
||||
if !ok {
|
||||
t.Fatal("data.relationships.posts.data was not a slice []")
|
||||
}
|
||||
if len(postsDataSlice) != 0 {
|
||||
t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []")
|
||||
}
|
||||
|
||||
// Verifiy the "current_post" was a null
|
||||
currentPost, postExists := relationships["current_post"]
|
||||
if !postExists {
|
||||
t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted")
|
||||
}
|
||||
currentPostMap, ok := currentPost.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("data.relationships.current_post was not a map")
|
||||
}
|
||||
currentPostData, ok := currentPostMap["data"]
|
||||
if !ok {
|
||||
t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present")
|
||||
}
|
||||
if currentPostData != nil {
|
||||
t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) {
|
||||
type BlogOptionalPosts struct {
|
||||
ID int `jsonapi:"primary,blogs"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Posts []*Post `jsonapi:"relation,posts,omitempty"`
|
||||
CurrentPost *Post `jsonapi:"relation,current_post,omitempty"`
|
||||
}
|
||||
|
||||
blog := &BlogOptionalPosts{ID: 999}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalOnePayload(out, blog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := jsonData["data"].(map[string]interface{})
|
||||
|
||||
// Verify relationship was NOT set
|
||||
if val, exists := payload["relationships"]; exists {
|
||||
t.Fatalf("Was expecting the data.relationships key/value to have been empty - it was not and had a value of %v", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) {
|
||||
type BlogOptionalPosts struct {
|
||||
ID int `jsonapi:"primary,blogs"`
|
||||
Title string `jsonapi:"attr,title"`
|
||||
Posts []*Post `jsonapi:"relation,posts,omitempty"`
|
||||
CurrentPost *Post `jsonapi:"relation,current_post,omitempty"`
|
||||
}
|
||||
|
||||
blog := &BlogOptionalPosts{
|
||||
ID: 999,
|
||||
CurrentPost: &Post{
|
||||
ID: 123,
|
||||
},
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalOnePayload(out, blog); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var jsonData map[string]interface{}
|
||||
if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload := jsonData["data"].(map[string]interface{})
|
||||
|
||||
// Verify relationship was set
|
||||
if _, exists := payload["relationships"]; !exists {
|
||||
t.Fatal("Was expecting the data.relationships key/value to have NOT been empty")
|
||||
}
|
||||
|
||||
relationships := payload["relationships"].(map[string]interface{})
|
||||
|
||||
// Verify the relationship was not omitted, and is not null
|
||||
if val, exists := relationships["current_post"]; !exists {
|
||||
t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted")
|
||||
} else if val.(map[string]interface{})["data"] == nil {
|
||||
t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null")
|
||||
}
|
||||
}
|
||||
|
||||
type Timestamp struct {
|
||||
ID int `jsonapi:"primary,timestamps"`
|
||||
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
|
||||
|
|
Loading…
Reference in New Issue