-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathunmarshal.go
233 lines (204 loc) · 6.13 KB
/
unmarshal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package jsonapi
import (
"encoding/json"
"errors"
"fmt"
"reflect"
)
// The UnmarshalIdentifier interface must be implemented to set the ID during
// unmarshalling.
type UnmarshalIdentifier interface {
SetID(string) error
}
// The UnmarshalToOneRelations interface must be implemented to unmarshal
// to-one relations.
type UnmarshalToOneRelations interface {
SetToOneReferenceID(name, ID string) error
}
// The UnmarshalToManyRelations interface must be implemented to unmarshal
// to-many relations.
type UnmarshalToManyRelations interface {
SetToManyReferenceIDs(name string, IDs []string) error
}
// The EditToManyRelations interface can be optionally implemented to add and
// delete to-many relationships on a already unmarshalled struct. These methods
// are used by our API for the to-many relationship update routes.
//
// There are 3 HTTP Methods to edit to-many relations:
//
// PATCH /v1/posts/1/comments
// Content-Type: application/vnd.api+json
// Accept: application/vnd.api+json
//
// {
// "data": [
// { "type": "comments", "id": "2" },
// { "type": "comments", "id": "3" }
// ]
// }
//
// This replaces all of the comments that belong to post with ID 1 and the
// SetToManyReferenceIDs method will be called.
//
// POST /v1/posts/1/comments
// Content-Type: application/vnd.api+json
// Accept: application/vnd.api+json
//
// {
// "data": [
// { "type": "comments", "id": "123" }
// ]
// }
//
// Adds a new comment to the post with ID 1.
// The AddToManyIDs method will be called.
//
// DELETE /v1/posts/1/comments
// Content-Type: application/vnd.api+json
// Accept: application/vnd.api+json
//
// {
// "data": [
// { "type": "comments", "id": "12" },
// { "type": "comments", "id": "13" }
// ]
// }
//
// Deletes comments that belong to post with ID 1.
// The DeleteToManyIDs method will be called.
type EditToManyRelations interface {
AddToManyIDs(name string, IDs []string) error
DeleteToManyIDs(name string, IDs []string) error
}
// Unmarshal parses a JSON API compatible JSON and populates the target which
// must implement the `UnmarshalIdentifier` interface.
func Unmarshal(data []byte, target interface{}) error {
if target == nil {
return errors.New("target must not be nil")
}
if reflect.TypeOf(target).Kind() != reflect.Ptr {
return errors.New("target must be a ptr")
}
ctx := &Document{}
err := json.Unmarshal(data, ctx)
if err != nil {
return err
}
if ctx.Data == nil {
return errors.New(`Source JSON is empty and has no "attributes" payload object`)
}
if ctx.Data.DataObject != nil {
return setDataIntoTarget(ctx.Data.DataObject, target)
}
if ctx.Data.DataArray != nil {
targetSlice := reflect.TypeOf(target).Elem()
if targetSlice.Kind() != reflect.Slice {
return fmt.Errorf("Cannot unmarshal array to struct target %s", targetSlice)
}
targetType := targetSlice.Elem()
targetPointer := reflect.ValueOf(target)
targetValue := targetPointer.Elem()
for _, record := range ctx.Data.DataArray {
// check if there already is an entry with the same id in target slice,
// otherwise create a new target and append
var targetRecord, emptyValue reflect.Value
for i := 0; i < targetValue.Len(); i++ {
marshalCasted, ok := targetValue.Index(i).Interface().(MarshalIdentifier)
if !ok {
return errors.New("existing structs must implement interface MarshalIdentifier")
}
if record.ID == marshalCasted.GetID() {
targetRecord = targetValue.Index(i).Addr()
break
}
}
if targetRecord == emptyValue || targetRecord.IsNil() {
targetRecord = reflect.New(targetType)
err := setDataIntoTarget(&record, targetRecord.Interface())
if err != nil {
return err
}
targetValue = reflect.Append(targetValue, targetRecord.Elem())
} else {
err := setDataIntoTarget(&record, targetRecord.Interface())
if err != nil {
return err
}
}
}
targetPointer.Elem().Set(targetValue)
}
return nil
}
func setDataIntoTarget(data *Data, target interface{}) error {
castedTarget, ok := target.(UnmarshalIdentifier)
if !ok {
return errors.New("target must implement UnmarshalIdentifier interface")
}
if data.Type == "" {
return errors.New("invalid record, no type was specified")
}
// err := checkType(data.Type, castedTarget)
// if err != nil {
// return err
// }
if data.Attributes != nil {
err := json.Unmarshal(data.Attributes, castedTarget)
if err != nil {
return err
}
}
if err := castedTarget.SetID(data.ID); err != nil {
return err
}
return setRelationshipIDs(data.Relationships, castedTarget)
}
// extracts all found relationships and set's them via SetToOneReferenceID or
// SetToManyReferenceIDs
func setRelationshipIDs(relationships map[string]Relationship, target UnmarshalIdentifier) error {
for name, rel := range relationships {
// if Data is nil, it means that we have an empty toOne relationship
if rel.Data == nil {
castedToOne, ok := target.(UnmarshalToOneRelations)
if !ok {
return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target))
}
castedToOne.SetToOneReferenceID(name, "")
break
}
// valid toOne case
if rel.Data.DataObject != nil {
castedToOne, ok := target.(UnmarshalToOneRelations)
if !ok {
return fmt.Errorf("struct %s does not implement UnmarshalToOneRelations", reflect.TypeOf(target))
}
err := castedToOne.SetToOneReferenceID(name, rel.Data.DataObject.ID)
if err != nil {
return err
}
}
// valid toMany case
if rel.Data.DataArray != nil {
castedToMany, ok := target.(UnmarshalToManyRelations)
if !ok {
return fmt.Errorf("struct %s does not implement UnmarshalToManyRelations", reflect.TypeOf(target))
}
IDs := make([]string, len(rel.Data.DataArray))
for index, relData := range rel.Data.DataArray {
IDs[index] = relData.ID
}
err := castedToMany.SetToManyReferenceIDs(name, IDs)
if err != nil {
return err
}
}
}
return nil
}
func checkType(incomingType string, target UnmarshalIdentifier) error {
actualType := getStructType(target)
if incomingType != actualType {
return fmt.Errorf("Type %s in JSON does not match target struct type %s", incomingType, actualType)
}
return nil
}