diff --git a/constants.go b/constants.go index 23288d3..d5cd0ea 100644 --- a/constants.go +++ b/constants.go @@ -10,6 +10,7 @@ const ( annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationSeperator = "," + annotationIgnore = "-" iso8601TimeFormat = "2006-01-02T15:04:05Z" diff --git a/embeded_structs_test.go b/embeded_structs_test.go new file mode 100644 index 0000000..17381ca --- /dev/null +++ b/embeded_structs_test.go @@ -0,0 +1,1000 @@ +package jsonapi + +import ( + "bytes" + "encoding/json" + "reflect" + "testing" +) + +func TestMergeNode(t *testing.T) { + parent := &Node{ + Type: "Good", + ID: "99", + Attributes: map[string]interface{}{"fizz": "buzz"}, + } + + child := &Node{ + Type: "Better", + ClientID: "1111", + Attributes: map[string]interface{}{"timbuk": 2}, + } + + expected := &Node{ + Type: "Better", + ID: "99", + ClientID: "1111", + Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2}, + } + + parent.merge(child) + + if !reflect.DeepEqual(expected, parent) { + t.Errorf("Got %+v Expected %+v", parent, expected) + } +} + +func TestIsEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := isEmbeddedStruct(test.input) + if res != test.expectedRes { + t.Errorf( + "Scenario -> %s\nGot -> %v\nExpected -> %v\n", + test.scenario, res, test.expectedRes) + } + } +} + +func TestShouldIgnoreField(t *testing.T) { + type test struct { + scenario string + input string + expectedRes bool + } + + tests := []test{ + test{ + scenario: "opt-out", + input: annotationIgnore, + expectedRes: true, + }, + test{ + scenario: "no tag", + input: "", + expectedRes: false, + }, + test{ + scenario: "wrong tag", + input: "wrong,tag", + expectedRes: false, + }, + } + + for _, test := range tests { + res := shouldIgnoreField(test.input) + if res != test.expectedRes { + t.Errorf( + "Scenario -> %s\nGot -> %v\nExpected -> %v\n", + test.scenario, res, test.expectedRes) + } + } +} + +func TestIsValidEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "opt-out", + input: reflect.StructField{ + Anonymous: true, + Tag: "jsonapi:\"-\"", + Type: structType, + }, + expectedRes: false, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := (isEmbeddedStruct(test.input) && + !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI))) + if res != test.expectedRes { + t.Errorf( + "Scenario -> %s\nGot -> %v\nExpected -> %v\n", + test.scenario, res, test.expectedRes) + } + } +} + +// TestEmbeddedUnmarshalOrder tests the behavior of the marshaler/unmarshaler of +// embedded structs when a struct has an embedded struct w/ competing +// attributes, the top-level attributes take precedence it compares the behavior +// against the standard json package +func TestEmbeddedUnmarshalOrder(t *testing.T) { + type Bar struct { + Name int `jsonapi:"attr,Name"` + } + + type Foo struct { + Bar + ID string `jsonapi:"primary,foos"` + Name string `jsonapi:"attr,Name"` + } + + f := &Foo{ + ID: "1", + Name: "foo", + Bar: Bar{ + Name: 5, + }, + } + + // marshal f (Foo) using jsonapi marshaler + jsonAPIData := bytes.NewBuffer(nil) + if err := MarshalPayload(jsonAPIData, f); err != nil { + t.Fatal(err) + } + + // marshal f (Foo) using json marshaler + jsonData, err := json.Marshal(f) + if err != nil { + t.Fatal(err) + } + + // convert bytes to map[string]interface{} so that we can do a semantic JSON + // comparison + var jsonAPIVal, jsonVal map[string]interface{} + if err = json.Unmarshal(jsonAPIData.Bytes(), &jsonAPIVal); err != nil { + t.Fatal(err) + } + if err = json.Unmarshal(jsonData, &jsonVal); err != nil { + t.Fatal(err) + } + + // get to the jsonapi attribute map + jDataMap, ok := jsonAPIVal["data"].(map[string]interface{}) + if !ok { + t.Fatal("Could not parse `data`") + } + jAttrMap, ok := jDataMap["attributes"].(map[string]interface{}) + if !ok { + t.Fatal("Could not parse `attributes`") + } + + // compare + if !reflect.DeepEqual(jAttrMap["Name"], jsonVal["Name"]) { + t.Errorf("Got\n%s\nExpected\n%s\n", jAttrMap["Name"], jsonVal["Name"]) + } +} + +// TestEmbeddedMarshalOrder tests the behavior of the marshaler/unmarshaler of +// embedded structs when a struct has an embedded struct w/ competing +// attributes, the top-level attributes take precedence it compares the +// behavior against the standard json package +func TestEmbeddedMarshalOrder(t *testing.T) { + type Bar struct { + Name int `jsonapi:"attr,Name"` + } + + type Foo struct { + Bar + ID string `jsonapi:"primary,foos"` + Name string `jsonapi:"attr,Name"` + } + + // get a jsonapi payload w/ Name attribute of an int type + payloadWithInt, err := json.Marshal(&OnePayload{ + Data: &Node{ + Type: "foos", + ID: "1", + Attributes: map[string]interface{}{ + "Name": 5, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + // get a jsonapi payload w/ Name attribute of an string type + payloadWithString, err := json.Marshal(&OnePayload{ + Data: &Node{ + Type: "foos", + ID: "1", + Attributes: map[string]interface{}{ + "Name": "foo", + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + // unmarshal payloadWithInt to f (Foo) using jsonapi unmarshaler; expecting an error + f := &Foo{} + if err = UnmarshalPayload(bytes.NewReader(payloadWithInt), f); err == nil { + t.Errorf("expected an error: int value of 5 should attempt to map to Foo.Name (string) and error") + } + + // unmarshal payloadWithString to f (Foo) using jsonapi unmarshaler; expecting no error + f = &Foo{} + if err = UnmarshalPayload(bytes.NewReader(payloadWithString), f); err != nil { + t.Error(err) + } + if f.Name != "foo" { + t.Errorf("Got\n%s\nExpected\n%s\n", "foo", f.Name) + } + + // get a json payload w/ Name attribute of an int type + bWithInt, err := json.Marshal(map[string]interface{}{ + "Name": 5, + }) + if err != nil { + t.Fatal(err) + } + + // get a json payload w/ Name attribute of an string type + bWithString, err := json.Marshal(map[string]interface{}{ + "Name": "foo", + }) + if err != nil { + t.Fatal(err) + } + + // unmarshal bWithInt to f (Foo) using json unmarshaler; expecting an error + f = &Foo{} + if err := json.Unmarshal(bWithInt, f); err == nil { + t.Errorf("expected an error: int value of 5 should attempt to map to Foo.Name (string) and error") + } + // unmarshal bWithString to f (Foo) using json unmarshaler; expecting no error + f = &Foo{} + if err := json.Unmarshal(bWithString, f); err != nil { + t.Error(err) + } + if f.Name != "foo" { + t.Errorf("Got\n%s\nExpected\n%s\n", "foo", f.Name) + } +} + +func TestMarshalUnmarshalCompositeStruct(t *testing.T) { + type Thing struct { + ID int `jsonapi:"primary,things"` + Fizz string `jsonapi:"attr,fizz"` + Buzz int `jsonapi:"attr,buzz"` + } + + type Model struct { + Thing + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + type test struct { + name string + payload *OnePayload + dst, expected interface{} + } + + scenarios := []test{} + + scenarios = append(scenarios, test{ + name: "Model embeds Thing, models have no annotation overlaps", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "things", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "buzz": 99, + "fizz": "fizzy", + "foo": "fooey", + }, + }, + }, + expected: &Model{ + Foo: "fooey", + Bar: "barry", + Bat: "batty", + Thing: Thing{ + ID: 1, + Fizz: "fizzy", + Buzz: 99, + }, + }, + }) + + { + type Model struct { + Thing + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + Buzz int `jsonapi:"attr,buzz"` // overrides Thing.Buzz + } + + scenarios = append(scenarios, test{ + name: "Model embeds Thing, overlap Buzz attribute", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "things", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "buzz": 99, + "fizz": "fizzy", + "foo": "fooey", + }, + }, + }, + expected: &Model{ + Foo: "fooey", + Bar: "barry", + Bat: "batty", + Buzz: 99, + Thing: Thing{ + ID: 1, + Fizz: "fizzy", + }, + }, + }) + } + + { + type Model struct { + Thing + ModelID int `jsonapi:"primary,models"` //overrides Thing.ID due to primary annotation + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + Buzz int `jsonapi:"attr,buzz"` // overrides Thing.Buzz + } + + scenarios = append(scenarios, test{ + name: "Model embeds Thing, attribute, and primary annotation overlap", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "buzz": 99, + "fizz": "fizzy", + "foo": "fooey", + }, + }, + }, + expected: &Model{ + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + Buzz: 99, + Thing: Thing{ + Fizz: "fizzy", + }, + }, + }) + } + + { + type Model struct { + Thing `jsonapi:"-"` + ModelID int `jsonapi:"primary,models"` + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + Buzz int `jsonapi:"attr,buzz"` + } + + scenarios = append(scenarios, test{ + name: "Model embeds Thing, but is annotated w/ ignore", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "buzz": 99, + "foo": "fooey", + }, + }, + }, + expected: &Model{ + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + Buzz: 99, + }, + }) + } + { + type Model struct { + *Thing + ModelID int `jsonapi:"primary,models"` + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + scenarios = append(scenarios, test{ + name: "Model embeds pointer of Thing; Thing is initialized in advance", + dst: &Model{Thing: &Thing{}}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "foo": "fooey", + "buzz": 99, + "fizz": "fizzy", + }, + }, + }, + expected: &Model{ + Thing: &Thing{ + Fizz: "fizzy", + Buzz: 99, + }, + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + }, + }) + } + { + type Model struct { + *Thing + ModelID int `jsonapi:"primary,models"` + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + scenarios = append(scenarios, test{ + name: "Model embeds pointer of Thing; Thing is initialized w/ Unmarshal", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "foo": "fooey", + "buzz": 99, + "fizz": "fizzy", + }, + }, + }, + expected: &Model{ + Thing: &Thing{ + Fizz: "fizzy", + Buzz: 99, + }, + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + }, + }) + } + { + type Model struct { + *Thing + ModelID int `jsonapi:"primary,models"` + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + scenarios = append(scenarios, test{ + name: "Model embeds pointer of Thing; jsonapi model doesn't assign anything to Thing; *Thing is nil", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "foo": "fooey", + }, + }, + }, + expected: &Model{ + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + }, + }) + } + + { + type Model struct { + *Thing + ModelID int `jsonapi:"primary,models"` + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + scenarios = append(scenarios, test{ + name: "Model embeds pointer of Thing; *Thing is nil", + dst: &Model{}, + payload: &OnePayload{ + Data: &Node{ + Type: "models", + ID: "1", + Attributes: map[string]interface{}{ + "bar": "barry", + "bat": "batty", + "foo": "fooey", + }, + }, + }, + expected: &Model{ + ModelID: 1, + Foo: "fooey", + Bar: "barry", + Bat: "batty", + }, + }) + } + for _, scenario := range scenarios { + t.Logf("running scenario: %s\n", scenario.name) + + // get the expected model and marshal to jsonapi + buf := bytes.NewBuffer(nil) + if err := MarshalPayload(buf, scenario.expected); err != nil { + t.Fatal(err) + } + + // get the node model representation and marshal to jsonapi + payload, err := json.Marshal(scenario.payload) + if err != nil { + t.Fatal(err) + } + + // assert that we're starting w/ the same payload + isJSONEqual, err := isJSONEqual(payload, buf.Bytes()) + if err != nil { + t.Fatal(err) + } + if !isJSONEqual { + t.Errorf("Got\n%s\nExpected\n%s\n", buf.Bytes(), payload) + } + + // run jsonapi unmarshal + if err := UnmarshalPayload(bytes.NewReader(payload), scenario.dst); err != nil { + t.Fatal(err) + } + + // assert decoded and expected models are equal + if !reflect.DeepEqual(scenario.expected, scenario.dst) { + t.Errorf("Got\n%#v\nExpected\n%#v\n", scenario.dst, scenario.expected) + } + } +} + +func TestMarshal_duplicatePrimaryAnnotationFromEmbeddedStructs(t *testing.T) { + type Outer struct { + ID string `jsonapi:"primary,outer"` + Comment + *Post + } + + o := Outer{ + ID: "outer", + Comment: Comment{ID: 1}, + Post: &Post{ID: 5}, + } + var payloadData map[string]interface{} + + // Test the standard libraries JSON handling of dup (ID) fields - it uses + // the Outer's ID + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if e, a := o.ID, payloadData["ID"]; e != a { + t.Fatalf("Was expecting ID to be %v, got %v", e, a) + } + + // Test the JSONAPI lib handling of dup (ID) fields + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + id := data["id"].(string) + if e, a := o.ID, id; e != a { + t.Fatalf("Was expecting ID to be %v, got %v", e, a) + } +} + +func TestMarshal_duplicateAttributeAnnotationFromEmbeddedStructs(t *testing.T) { + type Foo struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Bar struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Outer struct { + ID uint `json:"id" jsonapi:"primary,outer"` + Foo + Bar + } + o := Outer{ + ID: 1, + Foo: Foo{Count: 1}, + Bar: Bar{Count: 2}, + } + + var payloadData map[string]interface{} + + // The standard JSON lib will not serialize either embedded struct's fields if + // a duplicate is encountered + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if _, found := payloadData["count"]; found { + t.Fatalf("Was not expecting to find the `count` key in the JSON") + } + + // Test the JSONAPI lib handling of dup (attr) fields + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + if _, found := data["attributes"]; found { + t.Fatal("Was not expecting to find any `attributes` in the JSON API") + } +} + +func TestMarshal_duplicateAttributeAnnotationFromEmbeddedStructsPtrs(t *testing.T) { + type Foo struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Bar struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Outer struct { + ID uint `json:"id" jsonapi:"primary,outer"` + *Foo + *Bar + } + o := Outer{ + ID: 1, + Foo: &Foo{Count: 1}, + Bar: &Bar{Count: 2}, + } + + var payloadData map[string]interface{} + + // The standard JSON lib will not serialize either embedded struct's fields if + // a duplicate is encountered + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if _, found := payloadData["count"]; found { + t.Fatalf("Was not expecting to find the `count` key in the JSON") + } + + // Test the JSONAPI lib handling of dup (attr) fields + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + if _, found := data["attributes"]; found { + t.Fatal("Was not expecting to find any `attributes` in the JSON API") + } +} + +func TestMarshal_duplicateAttributeAnnotationFromEmbeddedStructsMixed(t *testing.T) { + type Foo struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Bar struct { + Count uint `json:"count" jsonapi:"attr,count"` + } + type Outer struct { + ID uint `json:"id" jsonapi:"primary,outer"` + *Foo + Bar + } + o := Outer{ + ID: 1, + Foo: &Foo{Count: 1}, + Bar: Bar{Count: 2}, + } + + var payloadData map[string]interface{} + + // The standard JSON lib will not serialize either embedded struct's fields if + // a duplicate is encountered + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if _, found := payloadData["count"]; found { + t.Fatalf("Was not expecting to find the `count` key in the JSON") + } + + // Test the JSONAPI lib handling of dup (attr) fields; it should serialize + // neither + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + if _, found := data["attributes"]; found { + t.Fatal("Was not expecting to find any `attributes` in the JSON API") + } +} + +func TestMarshal_duplicateFieldFromEmbeddedStructs_serializationNameDiffers(t *testing.T) { + type Foo struct { + Count uint `json:"foo-count" jsonapi:"attr,foo-count"` + } + type Bar struct { + Count uint `json:"bar-count" jsonapi:"attr,bar-count"` + } + type Outer struct { + ID uint `json:"id" jsonapi:"primary,outer"` + Foo + Bar + } + o := Outer{ + ID: 1, + Foo: Foo{Count: 1}, + Bar: Bar{Count: 2}, + } + + var payloadData map[string]interface{} + + // The standard JSON lib will both the fields since their annotation name + // differs + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + fooJSON, fooFound := payloadData["foo-count"] + if !fooFound { + t.Fatal("Was expecting to find the `foo-count` key in the JSON") + } + if e, a := o.Foo.Count, fooJSON.(float64); e != uint(a) { + t.Fatalf("Was expecting the `foo-count` value to be %v, got %v", e, a) + } + barJSON, barFound := payloadData["bar-count"] + if !barFound { + t.Fatal("Was expecting to find the `bar-count` key in the JSON") + } + if e, a := o.Bar.Count, barJSON.(float64); e != uint(a) { + t.Fatalf("Was expecting the `bar-count` value to be %v, got %v", e, a) + } + + // Test the JSONAPI lib handling; it should serialize both + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + attributes := data["attributes"].(map[string]interface{}) + fooJSONAPI, fooFound := attributes["foo-count"] + if !fooFound { + t.Fatal("Was expecting to find the `foo-count` attribute in the JSON API") + } + if e, a := o.Foo.Count, fooJSONAPI.(float64); e != uint(e) { + t.Fatalf("Was expecting the `foo-count` attrobute to be %v, got %v", e, a) + } + barJSONAPI, barFound := attributes["bar-count"] + if !barFound { + t.Fatal("Was expecting to find the `bar-count` attribute in the JSON API") + } + if e, a := o.Bar.Count, barJSONAPI.(float64); e != uint(e) { + t.Fatalf("Was expecting the `bar-count` attrobute to be %v, got %v", e, a) + } +} + +func TestMarshal_embeddedStruct_providesDuplicateAttr(t *testing.T) { + type Foo struct { + Number uint `json:"count" jsonapi:"attr,count"` + } + type Outer struct { + Foo + ID uint `json:"id" jsonapi:"primary,outer"` + Count uint `json:"count" jsonapi:"attr,count"` + } + o := Outer{ + ID: 1, + Count: 1, + Foo: Foo{Number: 5}, + } + var payloadData map[string]interface{} + + // The standard JSON lib will take the count annotated field from the Outer + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if e, a := o.Count, payloadData["count"].(float64); e != uint(a) { + t.Fatalf("Was expecting a JSON `count` of %v, got %v", e, a) + } + + // In JSON API the handling should be that the Outer annotated count field is + // serialized into `attributes` + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + attributes := data["attributes"].(map[string]interface{}) + if e, a := o.Count, attributes["count"].(float64); e != uint(a) { + t.Fatalf("Was expecting a JSON API `count` attribute of %v, got %v", e, a) + } +} + +func TestMarshal_embeddedStructPtr_providesDuplicateAttr(t *testing.T) { + type Foo struct { + Number uint `json:"count" jsonapi:"attr,count"` + } + type Outer struct { + *Foo + ID uint `json:"id" jsonapi:"primary,outer"` + Count uint `json:"count" jsonapi:"attr,count"` + } + o := Outer{ + ID: 1, + Count: 1, + Foo: &Foo{Number: 5}, + } + var payloadData map[string]interface{} + + // The standard JSON lib will take the count annotated field from the Outer + jsonData, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonData, &payloadData); err != nil { + t.Fatal(err) + } + if e, a := o.Count, payloadData["count"].(float64); e != uint(a) { + t.Fatalf("Was expecting a JSON `count` of %v, got %v", e, a) + } + + // In JSON API the handling should be that the Outer annotated count field is + // serialized into `attributes` + jsonAPIData := new(bytes.Buffer) + if err := MarshalPayload(jsonAPIData, &o); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(jsonAPIData.Bytes(), &payloadData); err != nil { + t.Fatal(err) + } + data := payloadData["data"].(map[string]interface{}) + attributes := data["attributes"].(map[string]interface{}) + if e, a := o.Count, attributes["count"].(float64); e != uint(a) { + t.Fatalf("Was expecting a JSON API `count` attribute of %v, got %v", e, a) + } +} + +// TODO: test permutation of relations with embedded structs diff --git a/errors.go b/errors.go index ed7fa9f..9696e86 100644 --- a/errors.go +++ b/errors.go @@ -23,6 +23,14 @@ type ErrorsPayload struct { Errors []*ErrorObject `json:"errors"` } +// ErrorObjectLinks is an implementation of the JSON API error links payload. +// +// For more information on the JSON API spec's error links objects, see: http://jsonapi.org/format/#error-objects +type ErrorObjectLinks struct { + // About is a link that leads to further details about this particular occurrence of the problem. + About string `json:"about"` +} + // ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. // // The main idea behind this struct is that you can use it directly in your code as an error type @@ -45,6 +53,9 @@ type ErrorObject struct { // Code is an application-specific error code, expressed as a string value. Code string `json:"code,omitempty"` + // Links is an implementation of the JSON API error links payload. + Links *ErrorObjectLinks `json:"links,omitempty"` + // Meta is an object containing non-standard meta-information about the error. Meta *map[string]interface{} `json:"meta,omitempty"` } diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..6dfdd8f --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,88 @@ +package jsonapi + +import ( + "encoding/json" + "reflect" + "time" +) + +func isJSONEqual(b1, b2 []byte) (bool, error) { + var i1, i2 interface{} + var result bool + var err error + if err = json.Unmarshal(b1, &i1); err != nil { + return result, err + } + if err = json.Unmarshal(b2, &i2); err != nil { + return result, err + } + result = reflect.DeepEqual(i1, i2) + return result, err +} + +func testBlog() *Blog { + return &Blog{ + ID: 5, + Title: "Title 1", + CreatedAt: time.Now(), + Posts: []*Post{ + &Post{ + ID: 1, + Title: "Foo", + Body: "Bar", + Comments: []*Comment{ + &Comment{ + ID: 1, + Body: "foo", + }, + &Comment{ + ID: 2, + Body: "bar", + }, + }, + LatestComment: &Comment{ + ID: 1, + Body: "foo", + }, + }, + &Post{ + ID: 2, + Title: "Fuubar", + Body: "Bas", + Comments: []*Comment{ + &Comment{ + ID: 1, + Body: "foo", + }, + &Comment{ + ID: 3, + Body: "bas", + }, + }, + LatestComment: &Comment{ + ID: 1, + Body: "foo", + }, + }, + }, + CurrentPost: &Post{ + ID: 1, + Title: "Foo", + Body: "Bar", + Comments: []*Comment{ + &Comment{ + ID: 1, + Body: "foo", + }, + &Comment{ + ID: 2, + Body: "bar", + }, + }, + LatestComment: &Comment{ + ID: 1, + Body: "foo", + }, + }, + } +} diff --git a/models_test.go b/models_test.go index a53dd61..925189e 100644 --- a/models_test.go +++ b/models_test.go @@ -155,3 +155,22 @@ func (bc *BadComment) JSONAPILinks() *Links { "self": []string{"invalid", "should error"}, } } + +// Embedded Struct Models +type Engine struct { + NumberOfCylinders uint `jsonapi:"attr,cylinders"` + HorsePower uint `jsonapi:"attr,hp"` +} + +type BlockHeater struct { + Watts uint `jsonapi:"attr,watts"` +} + +type Vehicle struct { + ID uint `json:"id" jsonapi:"primary,car"` + Make string `jsonapi:"attr,make"` + Model string `jsonapi:"attr,model"` + Year uint `jsonapi:"attr,year"` + Engine // every car must have an engine + *BlockHeater // not every car will have a block heater +} diff --git a/node.go b/node.go index a58488c..73b7d59 100644 --- a/node.go +++ b/node.go @@ -44,6 +44,38 @@ type Node struct { Meta *Meta `json:"meta,omitempty"` } +func (n *Node) merge(node *Node) { + if node.Type != "" { + n.Type = node.Type + } + + if node.ID != "" { + n.ID = node.ID + } + + if node.ClientID != "" { + n.ClientID = node.ClientID + } + + if n.Attributes == nil && node.Attributes != nil { + n.Attributes = make(map[string]interface{}) + } + for k, v := range node.Attributes { + n.Attributes[k] = v + } + + if n.Relationships == nil && node.Relationships != nil { + n.Relationships = make(map[string]interface{}) + } + for k, v := range node.Relationships { + n.Relationships[k] = v + } + + if node.Links != nil { + n.Links = node.Links + } +} + // RelationshipOneNode is used to represent a generic has one JSON API relation type RelationshipOneNode struct { Data *Node `json:"data"` @@ -119,3 +151,35 @@ type RelationshipMetable interface { // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipMeta(relation string) *Meta } + +// derefs the arg, and clones the map-type attributes +// note: maps are reference types, so they need an explicit copy. +func deepCopyNode(n *Node) *Node { + if n == nil { + return n + } + + copyMap := func(m map[string]interface{}) map[string]interface{} { + if m == nil { + return m + } + cp := make(map[string]interface{}) + for k, v := range m { + cp[k] = v + } + return cp + } + + copy := *n + copy.Attributes = copyMap(copy.Attributes) + copy.Relationships = copyMap(copy.Relationships) + if copy.Links != nil { + tmp := Links(copyMap(map[string]interface{}(*copy.Links))) + copy.Links = &tmp + } + if copy.Meta != nil { + tmp := Meta(copyMap(map[string]interface{}(*copy.Meta))) + copy.Meta = &tmp + } + return © +} diff --git a/request.go b/request.go index fe29706..e962943 100644 --- a/request.go +++ b/request.go @@ -117,6 +117,11 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { return models, nil } +// unmarshalNode handles embedded struct models from top to down. +// it loops through the struct fields, handles attributes/relations at that level first +// the handling the embedded structs are done last, so that you get the expected composition behavior +// data (*Node) attributes are cleared on each success. +// relations/sideloaded models use deeply copied Nodes (since those sideloaded models can be referenced in multiple relations) func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { defer func() { if r := recover(); r != nil { @@ -127,416 +132,520 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) modelValue := model.Elem() modelType := model.Type().Elem() - var er error + type embedded struct { + structField, model reflect.Value + } + embeddeds := []*embedded{} for i := 0; i < modelValue.NumField(); i++ { fieldType := modelType.Field(i) - tag := fieldType.Tag.Get("jsonapi") - if tag == "" { + fieldValue := modelValue.Field(i) + tag := fieldType.Tag.Get(annotationJSONAPI) + + // handle explicit ignore annotation + if shouldIgnoreField(tag) { continue } - fieldValue := modelValue.Field(i) - - args := strings.Split(tag, ",") + // handles embedded structs + if isEmbeddedStruct(fieldType) { + embeddeds = append(embeddeds, + &embedded{ + model: reflect.ValueOf(fieldValue.Addr().Interface()), + structField: fieldValue, + }, + ) + continue + } - if len(args) < 1 { - er = ErrBadJSONAPIStructTag - break + // handles pointers to embedded structs + if isEmbeddedStructPtr(fieldType) { + embeddeds = append(embeddeds, + &embedded{ + model: reflect.ValueOf(fieldValue.Interface()), + structField: fieldValue, + }, + ) + continue } - annotation := args[0] + // handle tagless; after handling embedded structs (which could be tagless) + if tag == "" { + continue + } - if (annotation == annotationClientID && len(args) != 1) || - (annotation != annotationClientID && len(args) < 2) { - er = ErrBadJSONAPIStructTag - break + args := strings.Split(tag, annotationSeperator) + // require atleast 1 + if len(args) < 1 { + return ErrBadJSONAPIStructTag } - if annotation == annotationPrimary { - if data.ID == "" { - continue + // args[0] == annotation + switch args[0] { + case annotationClientID: + if err := handleClientIDUnmarshal(data, args, fieldValue); err != nil { + return err } - - // Check the JSON API Type - if data.Type != args[1] { - er = fmt.Errorf( - "Trying to Unmarshal an object of type %#v, but %#v does not match", - data.Type, - args[1], - ) - break + case annotationPrimary: + if err := handlePrimaryUnmarshal(data, args, fieldType, fieldValue); err != nil { + return err } - - // ID will have to be transmitted as astring per the JSON API spec - v := reflect.ValueOf(data.ID) - - // Deal with PTRS - var kind reflect.Kind - if fieldValue.Kind() == reflect.Ptr { - kind = fieldType.Type.Elem().Kind() - } else { - kind = fieldType.Type.Kind() + case annotationAttribute: + if err := handleAttributeUnmarshal(data, args, fieldType, fieldValue); err != nil { + return err } - - // Handle String case - if kind == reflect.String { - assign(fieldValue, v) - continue + case annotationRelation: + if err := handleRelationUnmarshal(data, args, fieldValue, included); err != nil { + return err } + default: + return fmt.Errorf(unsuportedStructTagMsg, args[0]) + } + } - // Value was not a string... only other supported type was a numeric, - // which would have been sent as a float value. - floatValue, err := strconv.ParseFloat(data.ID, 64) - if err != nil { - // Could not convert the value in the "id" attr to a float - er = ErrBadJSONAPIID - break + // handle embedded last + for _, em := range embeddeds { + // if nil, need to construct and rollback accordingly + if em.model.IsNil() { + copy := deepCopyNode(data) + tmp := reflect.New(em.model.Type().Elem()) + if err := unmarshalNode(copy, tmp, included); err != nil { + return err } - // Convert the numeric float to one of the supported ID numeric types - // (int[8,16,32,64] or uint[8,16,32,64]) - var idValue reflect.Value - switch kind { - case reflect.Int: - n := int(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int8: - n := int8(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int16: - n := int16(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int32: - n := int32(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int64: - n := int64(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint: - n := uint(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint8: - n := uint8(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint16: - n := uint16(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint32: - n := uint32(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint64: - n := uint64(floatValue) - idValue = reflect.ValueOf(&n) - default: - // We had a JSON float (numeric), but our field was not one of the - // allowed numeric types - er = ErrBadJSONAPIID - break + // had changes; assign value to struct field, replace orig node (data) w/ mutated copy + if !reflect.DeepEqual(copy, data) { + assign(em.structField, tmp) + data = copy } - - assign(fieldValue, idValue) - } else if annotation == annotationClientID { - if data.ClientID == "" { - continue + } else { + // handle non-nil scenarios + if err := unmarshalNode(data, em.model, included); err != nil { + return err } + } + } - fieldValue.Set(reflect.ValueOf(data.ClientID)) - } else if annotation == annotationAttribute { - attributes := data.Attributes - if attributes == nil || len(data.Attributes) == 0 { - continue - } + return nil +} - var iso8601 bool +func handleClientIDUnmarshal(data *Node, args []string, fieldValue reflect.Value) error { + if len(args) != 1 { + return ErrBadJSONAPIStructTag + } - if len(args) > 2 { - for _, arg := range args[2:] { - if arg == annotationISO8601 { - iso8601 = true - } - } - } + if data.ClientID == "" { + return nil + } - val := attributes[args[1]] + // set value and clear clientID to denote it's already been processed + fieldValue.Set(reflect.ValueOf(data.ClientID)) + data.ClientID = "" - // continue if the attribute was not included in the request - if val == nil { - continue - } + return nil +} - v := reflect.ValueOf(val) +func handlePrimaryUnmarshal(data *Node, args []string, fieldType reflect.StructField, fieldValue reflect.Value) error { + if len(args) < 2 { + return ErrBadJSONAPIStructTag + } - // Handle field of type time.Time - if fieldValue.Type() == reflect.TypeOf(time.Time{}) { - if iso8601 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { - er = ErrInvalidISO8601 - break - } + if data.ID == "" { + return nil + } - t, err := time.Parse(iso8601TimeFormat, tm) - if err != nil { - er = ErrInvalidISO8601 - break - } + // Check the JSON API Type + if data.Type != args[1] { + return fmt.Errorf( + "Trying to Unmarshal an object of type %#v, but %#v does not match", + data.Type, + args[1], + ) + } - fieldValue.Set(reflect.ValueOf(t)) + // Deal with PTRS + var kind reflect.Kind + if fieldValue.Kind() == reflect.Ptr { + kind = fieldType.Type.Elem().Kind() + } else { + kind = fieldType.Type.Kind() + } - continue - } + var idValue reflect.Value - var at int64 + // Handle String case + if kind == reflect.String { + // ID will have to be transmitted as a string per the JSON API spec + idValue = reflect.ValueOf(data.ID) + } else { + // Value was not a string... only other supported type was a numeric, + // which would have been sent as a float value. + floatValue, err := strconv.ParseFloat(data.ID, 64) + if err != nil { + // Could not convert the value in the "id" attr to a float + return ErrBadJSONAPIID + } - if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) - } else if v.Kind() == reflect.Int { - at = v.Int() - } else { - return ErrInvalidTime - } + // Convert the numeric float to one of the supported ID numeric types + // (int[8,16,32,64] or uint[8,16,32,64]) + switch kind { + case reflect.Int: + n := int(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int8: + n := int8(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int16: + n := int16(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int32: + n := int32(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Int64: + n := int64(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint: + n := uint(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint8: + n := uint8(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint16: + n := uint16(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint32: + n := uint32(floatValue) + idValue = reflect.ValueOf(&n) + case reflect.Uint64: + n := uint64(floatValue) + idValue = reflect.ValueOf(&n) + default: + // We had a JSON float (numeric), but our field was not one of the + // allowed numeric types + return ErrBadJSONAPIID + } + } - t := time.Unix(at, 0) + // set value and clear ID to denote it's already been processed + assign(fieldValue, idValue) + data.ID = "" - fieldValue.Set(reflect.ValueOf(t)) + return nil +} - continue - } +func handleRelationUnmarshal(data *Node, args []string, fieldValue reflect.Value, included *map[string]*Node) error { + if len(args) < 2 { + return ErrBadJSONAPIStructTag + } - if fieldValue.Type() == reflect.TypeOf([]string{}) { - values := make([]string, v.Len()) - for i := 0; i < v.Len(); i++ { - values[i] = v.Index(i).Interface().(string) - } + if data.Relationships == nil || data.Relationships[args[1]] == nil { + return nil + } - fieldValue.Set(reflect.ValueOf(values)) + // to-one relationships + handler := handleToOneRelationUnmarshal + isSlice := fieldValue.Type().Kind() == reflect.Slice + if isSlice { + // to-many relationship + handler = handleToManyRelationUnmarshal + } - continue - } + v, err := handler(data.Relationships[args[1]], fieldValue.Type(), included) + if err != nil { + return err + } + // set only if there is a val since val can be null (e.g. to disassociate the relationship) + if v != nil { + fieldValue.Set(*v) + } + delete(data.Relationships, args[1]) + return nil +} - if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - if iso8601 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { - er = ErrInvalidISO8601 - break - } +// to-one relationships +func handleToOneRelationUnmarshal(relationData interface{}, fieldType reflect.Type, included *map[string]*Node) (*reflect.Value, error) { + relationship := new(RelationshipOneNode) + + buf := bytes.NewBuffer(nil) + json.NewEncoder(buf).Encode(relationData) + json.NewDecoder(buf).Decode(relationship) + + m := reflect.New(fieldType.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 { + return nil, nil + } - v, err := time.Parse(iso8601TimeFormat, tm) - if err != nil { - er = ErrInvalidISO8601 - break - } + if err := unmarshalNode( + fullNode(relationship.Data, included), + m, + included, + ); err != nil { + return nil, err + } - t := &v + return &m, nil +} - fieldValue.Set(reflect.ValueOf(t)) +// to-many relationship +func handleToManyRelationUnmarshal(relationData interface{}, fieldType reflect.Type, included *map[string]*Node) (*reflect.Value, error) { + relationship := new(RelationshipManyNode) - continue - } + buf := bytes.NewBuffer(nil) + json.NewEncoder(buf).Encode(relationData) + json.NewDecoder(buf).Decode(relationship) - var at int64 + models := reflect.New(fieldType).Elem() - if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) - } else if v.Kind() == reflect.Int { - at = v.Int() - } else { - return ErrInvalidTime - } + rData := relationship.Data + for _, n := range rData { + m := reflect.New(fieldType.Elem().Elem()) - v := time.Unix(at, 0) - t := &v + if err := unmarshalNode( + fullNode(n, included), + m, + included, + ); err != nil { + return nil, err + } - fieldValue.Set(reflect.ValueOf(t)) + models = reflect.Append(models, m) + } - continue - } + return &models, nil +} - // JSON value was a float (numeric) - if v.Kind() == reflect.Float64 { - floatValue := v.Interface().(float64) - - // The field may or may not be a pointer to a numeric; the kind var - // will not contain a pointer type - var kind reflect.Kind - if fieldValue.Kind() == reflect.Ptr { - kind = fieldType.Type.Elem().Kind() - } else { - kind = fieldType.Type.Kind() - } - - var numericValue reflect.Value - - switch kind { - case reflect.Int: - n := int(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int8: - n := int8(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int16: - n := int16(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int32: - n := int32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int64: - n := int64(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint: - n := uint(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint8: - n := uint8(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint16: - n := uint16(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint32: - n := uint32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint64: - n := uint64(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Float32: - n := float32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Float64: - n := floatValue - numericValue = reflect.ValueOf(&n) - default: - return ErrUnknownFieldNumberType - } - - assign(fieldValue, numericValue) - continue - } +// TODO: break this out into smaller funcs +func handleAttributeUnmarshal(data *Node, args []string, fieldType reflect.StructField, fieldValue reflect.Value) error { + if len(args) < 2 { + return ErrBadJSONAPIStructTag + } + attributes := data.Attributes + if attributes == nil || len(data.Attributes) == 0 { + return nil + } - // Field was a Pointer type - if fieldValue.Kind() == reflect.Ptr { - var concreteVal reflect.Value - - switch cVal := val.(type) { - case string: - concreteVal = reflect.ValueOf(&cVal) - case bool: - concreteVal = reflect.ValueOf(&cVal) - case complex64: - concreteVal = reflect.ValueOf(&cVal) - case complex128: - concreteVal = reflect.ValueOf(&cVal) - case uintptr: - concreteVal = reflect.ValueOf(&cVal) - default: - return ErrUnsupportedPtrType - } - - if fieldValue.Type() != concreteVal.Type() { - return ErrUnsupportedPtrType - } - - fieldValue.Set(concreteVal) - continue - } + var iso8601 bool - // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != v.Kind() { - return ErrInvalidType + if len(args) > 2 { + for _, arg := range args[2:] { + if arg == annotationISO8601 { + iso8601 = true } - fieldValue.Set(reflect.ValueOf(val)) + } + } + + val := attributes[args[1]] + + // continue if the attribute was not included in the request + if val == nil { + return nil + } - } else if annotation == annotationRelation { - isSlice := fieldValue.Type().Kind() == reflect.Slice + v := reflect.ValueOf(val) - if data.Relationships == nil || data.Relationships[args[1]] == nil { - continue + // Handle field of type time.Time + if fieldValue.Type() == reflect.TypeOf(time.Time{}) { + if iso8601 { + var tm string + if v.Kind() == reflect.String { + tm = v.Interface().(string) + } else { + return ErrInvalidISO8601 } - if isSlice { - // to-many relationship - relationship := new(RelationshipManyNode) + t, err := time.Parse(iso8601TimeFormat, tm) + if err != nil { + return ErrInvalidISO8601 + } - buf := bytes.NewBuffer(nil) + fieldValue.Set(reflect.ValueOf(t)) + delete(data.Attributes, args[1]) + return nil + } - json.NewEncoder(buf).Encode(data.Relationships[args[1]]) - json.NewDecoder(buf).Decode(relationship) + var at int64 - data := relationship.Data - models := reflect.New(fieldValue.Type()).Elem() + if v.Kind() == reflect.Float64 { + at = int64(v.Interface().(float64)) + } else if v.Kind() == reflect.Int { + at = v.Int() + } else { + return ErrInvalidTime + } - for _, n := range data { - m := reflect.New(fieldValue.Type().Elem().Elem()) + t := time.Unix(at, 0) - if err := unmarshalNode( - fullNode(n, included), - m, - included, - ); err != nil { - er = err - break - } + fieldValue.Set(reflect.ValueOf(t)) + delete(data.Attributes, args[1]) + return nil + } - models = reflect.Append(models, m) - } + if fieldValue.Type() == reflect.TypeOf([]string{}) { + values := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + values[i] = v.Index(i).Interface().(string) + } - fieldValue.Set(models) + fieldValue.Set(reflect.ValueOf(values)) + delete(data.Attributes, args[1]) + return nil + } + + if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + if iso8601 { + var tm string + if v.Kind() == reflect.String { + tm = v.Interface().(string) } else { - // to-one relationships - relationship := new(RelationshipOneNode) - - buf := bytes.NewBuffer(nil) - - json.NewEncoder(buf).Encode( - data.Relationships[args[1]], - ) - json.NewDecoder(buf).Decode(relationship) - - /* - 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, - included, - ); err != nil { - er = err - break - } - - fieldValue.Set(m) + return ErrInvalidISO8601 + + } + v, err := time.Parse(iso8601TimeFormat, tm) + if err != nil { + return ErrInvalidISO8601 } + t := &v + + fieldValue.Set(reflect.ValueOf(t)) + delete(data.Attributes, args[1]) + return nil + } + + var at int64 + + if v.Kind() == reflect.Float64 { + at = int64(v.Interface().(float64)) + } else if v.Kind() == reflect.Int { + at = v.Int() + } else { + return ErrInvalidTime + } + + v := time.Unix(at, 0) + t := &v + + fieldValue.Set(reflect.ValueOf(t)) + delete(data.Attributes, args[1]) + return nil + } + + // JSON value was a float (numeric) + if v.Kind() == reflect.Float64 { + floatValue := v.Interface().(float64) + + // The field may or may not be a pointer to a numeric; the kind var + // will not contain a pointer type + var kind reflect.Kind + if fieldValue.Kind() == reflect.Ptr { + kind = fieldType.Type.Elem().Kind() } else { - er = fmt.Errorf(unsuportedStructTagMsg, annotation) + kind = fieldType.Type.Kind() + } + + var numericValue reflect.Value + + switch kind { + case reflect.Int: + n := int(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int8: + n := int8(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int16: + n := int16(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int32: + n := int32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int64: + n := int64(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint: + n := uint(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint8: + n := uint8(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint16: + n := uint16(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint32: + n := uint32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint64: + n := uint64(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Float32: + n := float32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Float64: + n := floatValue + numericValue = reflect.ValueOf(&n) + default: + return ErrUnknownFieldNumberType } + + assign(fieldValue, numericValue) + delete(data.Attributes, args[1]) + return nil + } + + // Field was a Pointer type + if fieldValue.Kind() == reflect.Ptr { + var concreteVal reflect.Value + + switch cVal := val.(type) { + case string: + concreteVal = reflect.ValueOf(&cVal) + case bool: + concreteVal = reflect.ValueOf(&cVal) + case complex64: + concreteVal = reflect.ValueOf(&cVal) + case complex128: + concreteVal = reflect.ValueOf(&cVal) + case uintptr: + concreteVal = reflect.ValueOf(&cVal) + default: + return ErrUnsupportedPtrType + } + + if fieldValue.Type() != concreteVal.Type() { + return ErrUnsupportedPtrType + } + + fieldValue.Set(concreteVal) + delete(data.Attributes, args[1]) + return nil + } + + // As a final catch-all, ensure types line up to avoid a runtime panic. + // Ignore interfaces since interfaces are poly + if fieldValue.Kind() != reflect.Interface && fieldValue.Kind() != v.Kind() { + return ErrInvalidType } - return er + // set val and clear attribute key so its not processed again + fieldValue.Set(reflect.ValueOf(val)) + delete(data.Attributes, args[1]) + return nil } func fullNode(n *Node, included *map[string]*Node) *Node { includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID) if included != nil && (*included)[includedKey] != nil { - return (*included)[includedKey] + return deepCopyNode((*included)[includedKey]) } - return n + return deepCopyNode(n) } // assign will take the value specified and assign it to the field; if diff --git a/request_test.go b/request_test.go index 2206449..a909d49 100644 --- a/request_test.go +++ b/request_test.go @@ -703,6 +703,97 @@ func TestManyPayload_withLinks(t *testing.T) { } } +func TestEmbeddedStructs_nonNilStructPtr(t *testing.T) { + originalVehicle := &Vehicle{ + Make: "VW", + Model: "R32", + Year: 2008, + Engine: Engine{ + NumberOfCylinders: 6, + HorsePower: 250, + }, + BlockHeater: &BlockHeater{ + Watts: 150, + }, + } + + // Serialize as JSON + jsonVehicle, err := json.Marshal(originalVehicle) + if err != nil { + t.Fatal(err) + } + + jsonUnmarshalledVehicle := &Vehicle{} + json.Unmarshal(jsonVehicle, jsonUnmarshalledVehicle) + + // Proves that the JSON standard lib will allocate a BlockHeater + if jsonUnmarshalledVehicle.BlockHeater == nil { + t.Fatal("was expecting a non nil Block Heater ptr") + } + if e, a := originalVehicle.BlockHeater.Watts, jsonUnmarshalledVehicle.BlockHeater.Watts; e != a { + t.Fatalf("was expecting watts to be %v, got %v", e, a) + } + + // Serialize as JSONAPI + jsonAPIVehicle := new(bytes.Buffer) + if err = MarshalPayload(jsonAPIVehicle, originalVehicle); err != nil { + t.Fatal(err) + } + + jsonAPIUnmarshalledVehicle := &Vehicle{} + if err = UnmarshalPayload(jsonAPIVehicle, jsonAPIUnmarshalledVehicle); err != nil { + t.Fatal(err) + } + + if jsonAPIUnmarshalledVehicle.BlockHeater == nil { + t.Fatal("was expecting a non nil Block Heater ptr") + } + if e, a := originalVehicle.BlockHeater.Watts, jsonAPIUnmarshalledVehicle.BlockHeater.Watts; e != a { + t.Fatalf("was expecting watts to be %v, got %v", e, a) + } +} + +func TestEmbeddedStructs_nilStructPtr(t *testing.T) { + originalVehicle := &Vehicle{ + Make: "VW", + Model: "R32", + Year: 2008, + Engine: Engine{ + NumberOfCylinders: 6, + HorsePower: 250, + }, + } + + // Serialize as JSON + jsonVehicle, err := json.Marshal(originalVehicle) + if err != nil { + t.Fatal(err) + } + + jsonUnmarshalledVehicle := &Vehicle{} + json.Unmarshal(jsonVehicle, jsonUnmarshalledVehicle) + + // Proves that the JSON standard lib will NOT allocate a BlockHeater + if e, a := originalVehicle.BlockHeater, jsonUnmarshalledVehicle.BlockHeater; e != a { + t.Fatalf("was expecting BlockHeater to be %v, got %v", e, a) + } + + // Serialize as JSONAPI + jsonAPIVehicle := new(bytes.Buffer) + if err = MarshalPayload(jsonAPIVehicle, originalVehicle); err != nil { + t.Fatal(err) + } + + jsonAPIUnmarshalledVehicle := &Vehicle{} + if err = UnmarshalPayload(jsonAPIVehicle, jsonAPIUnmarshalledVehicle); err != nil { + t.Fatal(err) + } + + if e, a := originalVehicle.BlockHeater, jsonAPIUnmarshalledVehicle.BlockHeater; e != a { + t.Fatalf("was expecting BlockHeater to be %v, got %v", e, a) + } +} + func samplePayloadWithoutIncluded() map[string]interface{} { return map[string]interface{}{ "data": map[string]interface{}{ diff --git a/response.go b/response.go index f6a1b86..06381e4 100644 --- a/response.go +++ b/response.go @@ -202,8 +202,10 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { return nil } -func visitModelNode(model interface{}, included *map[string]*Node, - sideload bool) (*Node, error) { +// visitModelNode converts models to jsonapi payloads +// it handles the deepest models first. (i.e.) embedded models +// this is so that upper-level attributes can overwrite lower-level attributes +func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) { node := new(Node) var er error @@ -211,16 +213,58 @@ func visitModelNode(model interface{}, included *map[string]*Node, modelValue := reflect.ValueOf(model).Elem() modelType := reflect.ValueOf(model).Type().Elem() + // handle just the embedded models first for i := 0; i < modelValue.NumField(); i++ { - structField := modelValue.Type().Field(i) - tag := structField.Tag.Get(annotationJSONAPI) - if tag == "" { + fieldValue := modelValue.Field(i) + fieldType := modelType.Field(i) + + // skip if annotated w/ ignore + tag := fieldType.Tag.Get(annotationJSONAPI) + if shouldIgnoreField(tag) { continue } + // handles embedded structs and pointers to embedded structs + if isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) { + var embModel interface{} + if fieldType.Type.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + continue + } + embModel = fieldValue.Interface() + } else { + embModel = fieldValue.Addr().Interface() + } + + embNode, err := visitModelNode(embModel, included, sideload) + if err != nil { + er = err + break + } + node.merge(embNode) + } + } + + // handle everthing else + for i := 0; i < modelValue.NumField(); i++ { fieldValue := modelValue.Field(i) fieldType := modelType.Field(i) + tag := fieldType.Tag.Get(annotationJSONAPI) + + if shouldIgnoreField(tag) { + continue + } + + // skip embedded because it was handled in a previous loop + if isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) { + continue + } + + if tag == "" { + continue + } + args := strings.Split(tag, annotationSeperator) if len(args) < 1 { @@ -533,3 +577,15 @@ func convertToSliceInterface(i *interface{}) ([]interface{}, error) { } return response, nil } + +func isEmbeddedStruct(sField reflect.StructField) bool { + return sField.Anonymous && sField.Type.Kind() == reflect.Struct +} + +func isEmbeddedStructPtr(sField reflect.StructField) bool { + return sField.Anonymous && sField.Type.Kind() == reflect.Ptr && sField.Type.Elem().Kind() == reflect.Struct +} + +func shouldIgnoreField(japiTag string) bool { + return strings.HasPrefix(japiTag, annotationIgnore) +}