Skip to content

Commit

Permalink
Add support to Marshal slices of nested objects
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonc committed Jan 13, 2025
1 parent 9333e5c commit e03a6d4
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 2 deletions.
10 changes: 10 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ type Company struct {
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
26 changes: 24 additions & 2 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,36 @@ func visitModelNode(model interface{}, included *map[string]*Node,
continue
}

if fieldValue.Type().Kind() == reflect.Struct || (fieldValue.Type().Kind() == reflect.Pointer && fieldValue.Elem().Kind() == reflect.Struct) {
isStruct := fieldValue.Type().Kind() == reflect.Struct
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 {
// Primative attribute
strAttr, ok := fieldValue.Interface().(string)
if ok {
node.Attributes[args[1]] = strAttr
Expand Down Expand Up @@ -626,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
71 changes: 71 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,14 @@ func TestMarshalObjectAttribute(t *testing.T) {
Firstname: "Dave",
HiredAt: &now,
},
Teams: []Team{
{Name: "Team 1"},
{Name: "Team-2"},
},
People: []*People{
{Name: "Person-1"},
{Name: "Person-2"},
},
}

out := bytes.NewBuffer(nil)
Expand Down Expand Up @@ -734,6 +742,69 @@ func TestMarshalObjectAttribute(t *testing.T) {
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) {
Expand Down

0 comments on commit e03a6d4

Please sign in to comment.