Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,36 @@ type GenericInterface struct {
Data interface{} `jsonapi:"attr,interface"`
}

type Organization struct {
ID int `jsonapi:"primary,organizations"`
ClientID string `jsonapi:"client-id"`
Name string `jsonapi:"attr,title"`
DefaultProject *Project `jsonapi:"relation,default_project"`
CreatedAt time.Time `jsonapi:"attr,created_at"`

Links Links `jsonapi:"links,omitempty"`
}

type Project struct {
ID int `jsonapi:"primary,projects"`
ClientID string `jsonapi:"client-id"`
Name string `jsonapi:"attr,name"`
Organization *Organization `jsonapi:"relation,organization"`

Links Links `jsonapi:"links,omitempty"`
}

type Blog struct {
ID int `jsonapi:"primary,blogs"`
ClientID string `jsonapi:"client-id"`
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"`
ClientID string `jsonapi:"client-id"`
Title string `jsonapi:"attr,title"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
Organization *Organization `jsonapi:"relation,organization"`
Project *Project `jsonapi:"relation,project"`

Links Links `jsonapi:"links,omitempty"`
}
Expand Down
38 changes: 29 additions & 9 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl
return ErrUnsupportedPtrType{rf, t, structField}
}

type includedNode struct {
node *Node
model *reflect.Value
}

// UnmarshalPayload converts an io into a struct instance using jsonapi tags on
// struct fields. This method supports single request payloads only, at the
// moment. Bulk creates and updates are not supported yet.
Expand Down Expand Up @@ -94,16 +99,16 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl
// model interface{} should be a pointer to a struct.
func UnmarshalPayload(in io.Reader, model interface{}) error {
payload := new(OnePayload)
includedMap := make(map[string]*includedNode)

if err := json.NewDecoder(in).Decode(payload); err != nil {
return err
}

if payload.Included != nil {
includedMap := make(map[string]*Node)
for _, included := range payload.Included {
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
includedMap[key] = included
includedMap[key] = &includedNode{included, nil}
}

return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap)
Expand All @@ -120,13 +125,13 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
return nil, err
}

models := []interface{}{} // will be populated from the "data"
includedMap := map[string]*Node{} // will be populate from the "included"
models := []interface{}{} // will be populated from the "data"
includedMap := map[string]*includedNode{} // will be populate from the "included"

if payload.Included != nil {
for _, included := range payload.Included {
key := fmt.Sprintf("%s,%s", included.Type, included.ID)
includedMap[key] = included
includedMap[key] = &includedNode{included, nil}
}
}

Expand Down Expand Up @@ -263,7 +268,7 @@ func getStructTags(field reflect.StructField) ([]string, error) {

// unmarshalNodeMaybeChoice populates a model that may or may not be
// a choice type struct that corresponds to a polyrelation or relation
func unmarshalNodeMaybeChoice(m *reflect.Value, data *Node, annotation string, choiceTypeMapping map[string]structFieldIndex, included *map[string]*Node) error {
func unmarshalNodeMaybeChoice(m *reflect.Value, data *Node, annotation string, choiceTypeMapping map[string]structFieldIndex, included *map[string]*includedNode) error {
// This will hold either the value of the choice type model or the actual
// model, depending on annotation
var actualModel = *m
Expand Down Expand Up @@ -300,7 +305,7 @@ func unmarshalNodeMaybeChoice(m *reflect.Value, data *Node, annotation string, c
return nil
}

func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*includedNode) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type())
Expand Down Expand Up @@ -509,6 +514,21 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
// model, depending on annotation
m := reflect.New(fieldValue.Type().Elem())

includedKey := fmt.Sprintf("%s,%s", relationship.Data.Type, relationship.Data.ID)
if included != nil && (*included)[includedKey] != nil {
if (*included)[includedKey].model != nil {
fieldValue.Set(*(*included)[includedKey].model)
} else {
(*included)[includedKey].model = &m
err := unmarshalNodeMaybeChoice(&m, (*included)[includedKey].node, annotation, choiceMapping, included)
if err != nil {
er = err
break
}
fieldValue.Set(m)
}
continue
}
err = unmarshalNodeMaybeChoice(&m, relationship.Data, annotation, choiceMapping, included)
if err != nil {
er = err
Expand Down Expand Up @@ -565,11 +585,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
return er
}

func fullNode(n *Node, included *map[string]*Node) *Node {
func fullNode(n *Node, included *map[string]*includedNode) *Node {
includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID)

if included != nil && (*included)[includedKey] != nil {
return (*included)[includedKey]
return (*included)[includedKey].node
}

return n
Expand Down
134 changes: 134 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,41 @@ func TestUnmarshalRelationships(t *testing.T) {
}
}

func TestUnmarshalMany_relationships_with_circular_inclusion(t *testing.T) {
data := samplePayloadWithCircularInclusion()
payload, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(payload)
model := reflect.TypeOf(new(Blog))

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

result_1 := out[0].(*Blog)

if result_1.Project != result_1.Organization.DefaultProject {
t.Errorf("expected blog.project (%p) to hold the same pointer as blog.organization.default-project (%p) ", result_1.Project, result_1.Organization.DefaultProject)
}

if result_1.Organization != result_1.Project.Organization {
t.Errorf("expected blog.organization (%p) to hold the same pointer as blog.project.organization (%p)", result_1.Organization, result_1.Project.Organization)
}

result_2 := out[1].(*Blog)

if result_2.Project != result_2.Organization.DefaultProject {
t.Errorf("expected blog.project (%p) to hold the same pointer as blog.organization.default-project (%p) ", result_2.Project, result_2.Organization.DefaultProject)
}

if result_2.Organization != result_2.Project.Organization {
t.Errorf("expected blog.organization (%p) to hold the same pointer as blog.project.organization (%p)", result_2.Organization, result_2.Project.Organization)
}
}

func Test_UnmarshalPayload_polymorphicRelations(t *testing.T) {
in := bytes.NewReader([]byte(`{
"data": {
Expand Down Expand Up @@ -1378,6 +1413,105 @@ func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) {
}
}

func samplePayloadWithCircularInclusion() *ManyPayload {
payload := &ManyPayload{
Data: []*Node{
{
Type: "blogs",
ClientID: "1",
ID: "1",
Attributes: map[string]interface{}{
"title": "Foo",
"current_post_id": 1,
"created_at": 1436216820,
"view_count": 1000,
},
Relationships: map[string]interface{}{
"project": &RelationshipOneNode{
Data: &Node{
Type: "projects",
ClientID: "1",
ID: "1",
},
},
"organization": &RelationshipOneNode{
Data: &Node{
Type: "organizations",
ClientID: "1",
ID: "1",
},
},
},
},
{
Type: "blogs",
ClientID: "2",
ID: "2",
Attributes: map[string]interface{}{
"title": "Foo2",
"current_post_id": 1,
"created_at": 1436216820,
"view_count": 1000,
},
Relationships: map[string]interface{}{
"project": &RelationshipOneNode{
Data: &Node{
Type: "projects",
ClientID: "1",
ID: "1",
},
},
"organization": &RelationshipOneNode{
Data: &Node{
Type: "organizations",
ClientID: "1",
ID: "1",
},
},
},
},
},
Included: []*Node{
{
Type: "projects",
ClientID: "1",
ID: "1",
Attributes: map[string]interface{}{
"name": "Bar",
},
Relationships: map[string]interface{}{
"organization": &RelationshipOneNode{
Data: &Node{
Type: "organizations",
ClientID: "1",
ID: "1",
},
},
},
},
{
Type: "organizations",
ClientID: "1",
ID: "1",
Attributes: map[string]interface{}{
"name": "Baz",
},
Relationships: map[string]interface{}{
"default_project": &RelationshipOneNode{
Data: &Node{
Type: "projects",
ClientID: "1",
ID: "1",
},
},
},
},
},
}

return payload
}

func samplePayloadWithoutIncluded() map[string]interface{} {
return map[string]interface{}{
"data": map[string]interface{}{
Expand Down
Loading