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
}
|