terraform/vendor/github.com/manyminds/api2go/jsonapi/unmarshal.go

234 lines
6.1 KiB
Go

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
}