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
11 changes: 11 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,22 @@ type Company struct {
ID string `jsonapi:"primary,companies"`
Name string `jsonapi:"attr,name"`
Boss Employee `jsonapi:"attr,boss"`
Manager *Employee `jsonapi:"attr,manager"`
Teams []Team `jsonapi:"attr,teams"`
People []*People `jsonapi:"attr,people"`
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"`
}

type CompanyOmitEmpty struct {
ID string `jsonapi:"primary,companies"`
Name string `jsonapi:"attr,name,omitempty"`
Boss Employee `jsonapi:"attr,boss,omitempty"`
Manager *Employee `jsonapi:"attr,manager,omitempty"`
Teams []Team `jsonapi:"attr,teams,omitempty"`
People []*People `jsonapi:"attr,people,omitempty"`
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601,omitempty"`
}

type People struct {
Name string `jsonapi:"attr,name"`
Age int `jsonapi:"attr,age"`
Expand Down
57 changes: 47 additions & 10 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,20 @@ func visitModelNode(model interface{}, included *map[string]*Node,
node := new(Node)

var er error
var modelValue reflect.Value
var modelType reflect.Type
value := reflect.ValueOf(model)
if value.IsNil() {
return nil, nil
if value.Type().Kind() == reflect.Pointer {
if value.IsNil() {
return nil, nil
}
modelValue = value.Elem()
modelType = value.Type().Elem()
} else {
modelValue = value
modelType = value.Type()
}

modelValue := value.Elem()
modelType := value.Type().Elem()

for i := 0; i < modelValue.NumField(); i++ {
fieldValue := modelValue.Field(i)
structField := modelValue.Type().Field(i)
Expand Down Expand Up @@ -395,11 +401,42 @@ func visitModelNode(model interface{}, included *map[string]*Node,
continue
}

strAttr, ok := fieldValue.Interface().(string)
if ok {
node.Attributes[args[1]] = strAttr
isStruct := fieldValue.Type().Kind() == reflect.Struct

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this new logic (that starts at this line and ends prior to strAttr, ok := fieldValue.Interface().(string) line) be extracted into a helper function like "handleStructFieldValues"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a refactor commit that breaks up individual attr/relation field handling

isPointerToStruct := fieldValue.Type().Kind() == reflect.Pointer && fieldValue.Elem().Kind() == reflect.Struct
isSliceOfStruct := fieldValue.Type().Kind() == reflect.Slice && fieldValue.Type().Elem().Kind() == reflect.Struct
isSliceOfPointerToStruct := fieldValue.Type().Kind() == reflect.Slice && fieldValue.Type().Elem().Kind() == reflect.Pointer && fieldValue.Type().Elem().Elem().Kind() == reflect.Struct

if isSliceOfStruct || isSliceOfPointerToStruct {
if fieldValue.Len() == 0 && omitEmpty {
continue
}
// Nested slice of object attributes
manyNested, err := visitModelNodeRelationships(fieldValue, nil, false)
if err != nil {
er = fmt.Errorf("failed to marshal slice of nested attribute %q: %w", args[1], err)
break
}
nestedNodes := make([]any, len(manyNested.Data))
for i, n := range manyNested.Data {
nestedNodes[i] = n.Attributes
}
node.Attributes[args[1]] = nestedNodes
} else if isStruct || isPointerToStruct {
// Nested object attribute
nested, err := visitModelNode(fieldValue.Interface(), nil, false)
if err != nil {
er = fmt.Errorf("failed to marshal nested attribute %q: %w", args[1], err)
break
}
node.Attributes[args[1]] = nested.Attributes
} else {
node.Attributes[args[1]] = fieldValue.Interface()
// Primative attribute
strAttr, ok := fieldValue.Interface().(string)
if ok {
node.Attributes[args[1]] = strAttr
} else {
node.Attributes[args[1]] = fieldValue.Interface()
}
}
}
} else if annotation == annotationRelation || annotation == annotationPolyRelation {
Expand Down Expand Up @@ -611,7 +648,7 @@ func visitModelNodeRelationships(models reflect.Value, included *map[string]*Nod

for i := 0; i < models.Len(); i++ {
model := models.Index(i)
if !model.IsValid() || model.IsNil() {
if !model.IsValid() || (model.Kind() == reflect.Pointer && model.IsNil()) {
return nil, ErrUnexpectedNil
}

Expand Down
125 changes: 125 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,131 @@ func TestSupportsAttributes(t *testing.T) {
}
}

func TestMarshalObjectAttribute(t *testing.T) {
now := time.Now()
testModel := &Company{
ID: "5",
Name: "test",
Boss: Employee{
HiredAt: &now,
},
Manager: &Employee{
Firstname: "Dave",
HiredAt: &now,
},
Teams: []Team{
{Name: "Team 1"},
{Name: "Team-2"},
},
People: []*People{
{Name: "Person-1"},
{Name: "Person-2"},
},
}

out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}

resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}

data := resp.Data

if data.Attributes == nil {
t.Fatalf("Expected attributes")
}

boss, ok := data.Attributes["boss"].(map[string]interface{})
if !ok {
t.Fatalf("Expected boss attribute, got %v", data.Attributes)
}

hiredAt, ok := boss["hired-at"]
if !ok {
t.Fatalf("Expected boss attribute to contain a \"hired-at\" property, got %v", boss)
}

if hiredAt != now.UTC().Format(iso8601TimeFormat) {
t.Fatalf("Expected hired-at to be %s, got %s", now.UTC().Format(iso8601TimeFormat), hiredAt)
}

manager, ok := data.Attributes["manager"].(map[string]interface{})
if !ok {
t.Fatalf("Expected manager attribute, got %v", data.Attributes)
}

if manager["firstname"] != "Dave" {
t.Fatalf("Expected manager.firstname to be \"Dave\", got %v", manager)
}

people, ok := data.Attributes["people"].([]interface{})
if !ok {
t.Fatalf("Expected people attribute, got %v", data.Attributes)
}
if len(people) != 2 {
t.Fatalf("Expected 2 people, got %v", people)
}

teams, ok := data.Attributes["teams"].([]interface{})
if !ok {
t.Fatalf("Expected teams attribute, got %v", data.Attributes)
}
if len(teams) != 2 {
t.Fatalf("Expected 2 teams, got %v", teams)
}
}

func TestMarshalObjectAttributeWithEmptyNested(t *testing.T) {
testModel := &CompanyOmitEmpty{
ID: "5",
Name: "test",
Boss: Employee{},
Manager: nil,
Teams: []Team{},
People: nil,
}

out := bytes.NewBuffer(nil)
if err := MarshalPayload(out, testModel); err != nil {
t.Fatal(err)
}

resp := new(OnePayload)
if err := json.NewDecoder(out).Decode(resp); err != nil {
t.Fatal(err)
}

data := resp.Data

if data.Attributes == nil {
t.Fatalf("Expected attributes")
}

_, ok := data.Attributes["boss"].(map[string]interface{})
if ok {
t.Fatalf("Expected omitted boss attribute, got %v", data.Attributes)
}

_, ok = data.Attributes["manager"].(map[string]interface{})
if ok {
t.Fatalf("Expected omitted manager attribute, got %v", data.Attributes)
}

_, ok = data.Attributes["people"].([]interface{})
if ok {
t.Fatalf("Expected omitted people attribute, got %v", data.Attributes)
}

_, ok = data.Attributes["teams"].([]interface{})
if ok {
t.Fatalf("Expected omitted teams attribute, got %v", data.Attributes)
}
}

func TestOmitsZeroTimes(t *testing.T) {
testModel := &Blog{
ID: 5,
Expand Down
Loading