389 lines
10 KiB
Go
389 lines
10 KiB
Go
|
package jsonapi
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// RelationshipType specifies the type of a relationship.
|
||
|
type RelationshipType int
|
||
|
|
||
|
// The available relationship types.
|
||
|
//
|
||
|
// Note: DefaultRelationship guesses the relationship type based on the
|
||
|
// pluralization of the reference name.
|
||
|
const (
|
||
|
DefaultRelationship RelationshipType = iota
|
||
|
ToOneRelationship
|
||
|
ToManyRelationship
|
||
|
)
|
||
|
|
||
|
// The MarshalIdentifier interface is necessary to give an element a unique ID.
|
||
|
//
|
||
|
// Note: The implementation of this interface is mandatory.
|
||
|
type MarshalIdentifier interface {
|
||
|
GetID() string
|
||
|
}
|
||
|
|
||
|
// ReferenceID contains all necessary information in order to reference another
|
||
|
// struct in JSON API.
|
||
|
type ReferenceID struct {
|
||
|
ID string
|
||
|
Type string
|
||
|
Name string
|
||
|
Relationship RelationshipType
|
||
|
}
|
||
|
|
||
|
// A Reference information about possible references of a struct.
|
||
|
//
|
||
|
// Note: If IsNotLoaded is set to true, the `data` field will be omitted and only
|
||
|
// the `links` object will be generated. You should do this if there are some
|
||
|
// references, but you do not want to load them. Otherwise, if IsNotLoaded is
|
||
|
// false and GetReferencedIDs() returns no IDs for this reference name, an
|
||
|
// empty `data` field will be added which means that there are no references.
|
||
|
type Reference struct {
|
||
|
Type string
|
||
|
Name string
|
||
|
IsNotLoaded bool
|
||
|
Relationship RelationshipType
|
||
|
}
|
||
|
|
||
|
// The MarshalReferences interface must be implemented if the struct to be
|
||
|
// serialized has relationships.
|
||
|
type MarshalReferences interface {
|
||
|
GetReferences() []Reference
|
||
|
}
|
||
|
|
||
|
// The MarshalLinkedRelations interface must be implemented if there are
|
||
|
// reference ids that should be included in the document.
|
||
|
type MarshalLinkedRelations interface {
|
||
|
MarshalReferences
|
||
|
MarshalIdentifier
|
||
|
GetReferencedIDs() []ReferenceID
|
||
|
}
|
||
|
|
||
|
// The MarshalIncludedRelations interface must be implemented if referenced
|
||
|
// structs should be included in the document.
|
||
|
type MarshalIncludedRelations interface {
|
||
|
MarshalReferences
|
||
|
MarshalIdentifier
|
||
|
GetReferencedStructs() []MarshalIdentifier
|
||
|
}
|
||
|
|
||
|
// The MarshalCustomLinks interface can be implemented if the struct should
|
||
|
// want any custom links.
|
||
|
type MarshalCustomLinks interface {
|
||
|
MarshalIdentifier
|
||
|
GetCustomLinks(string) Links
|
||
|
}
|
||
|
|
||
|
// A ServerInformation implementor can be passed to MarshalWithURLs to generate
|
||
|
// the `self` and `related` urls inside `links`.
|
||
|
type ServerInformation interface {
|
||
|
GetBaseURL() string
|
||
|
GetPrefix() string
|
||
|
}
|
||
|
|
||
|
// MarshalWithURLs can be used to pass along a ServerInformation implementor.
|
||
|
func MarshalWithURLs(data interface{}, information ServerInformation) ([]byte, error) {
|
||
|
document, err := MarshalToStruct(data, information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return json.Marshal(document)
|
||
|
}
|
||
|
|
||
|
// Marshal wraps data in a Document and returns its JSON encoding.
|
||
|
//
|
||
|
// Data can be a struct, a pointer to a struct or a slice of structs. All structs
|
||
|
// must at least implement the `MarshalIdentifier` interface.
|
||
|
func Marshal(data interface{}) ([]byte, error) {
|
||
|
document, err := MarshalToStruct(data, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return json.Marshal(document)
|
||
|
}
|
||
|
|
||
|
// MarshalToStruct marshals an api2go compatible struct into a jsonapi Document
|
||
|
// structure which then can be marshaled to JSON. You only need this method if
|
||
|
// you want to extract or extend parts of the document. You should directly use
|
||
|
// Marshal to get a []byte with JSON in it.
|
||
|
func MarshalToStruct(data interface{}, information ServerInformation) (*Document, error) {
|
||
|
if data == nil {
|
||
|
return &Document{}, nil
|
||
|
}
|
||
|
|
||
|
switch reflect.TypeOf(data).Kind() {
|
||
|
case reflect.Slice:
|
||
|
return marshalSlice(data, information)
|
||
|
case reflect.Struct, reflect.Ptr:
|
||
|
return marshalStruct(data.(MarshalIdentifier), information)
|
||
|
default:
|
||
|
return nil, errors.New("Marshal only accepts slice, struct or ptr types")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func marshalSlice(data interface{}, information ServerInformation) (*Document, error) {
|
||
|
result := &Document{}
|
||
|
|
||
|
val := reflect.ValueOf(data)
|
||
|
dataElements := make([]Data, val.Len())
|
||
|
var referencedStructs []MarshalIdentifier
|
||
|
|
||
|
for i := 0; i < val.Len(); i++ {
|
||
|
k := val.Index(i).Interface()
|
||
|
element, ok := k.(MarshalIdentifier)
|
||
|
if !ok {
|
||
|
return nil, errors.New("all elements within the slice must implement api2go.MarshalIdentifier")
|
||
|
}
|
||
|
|
||
|
err := marshalData(element, &dataElements[i], information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
included, ok := k.(MarshalIncludedRelations)
|
||
|
if ok {
|
||
|
referencedStructs = append(referencedStructs, included.GetReferencedStructs()...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
includedElements, err := filterDuplicates(referencedStructs, information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
result.Data = &DataContainer{
|
||
|
DataArray: dataElements,
|
||
|
}
|
||
|
|
||
|
if includedElements != nil && len(includedElements) > 0 {
|
||
|
result.Included = includedElements
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func filterDuplicates(input []MarshalIdentifier, information ServerInformation) ([]Data, error) {
|
||
|
alreadyIncluded := map[string]map[string]bool{}
|
||
|
includedElements := []Data{}
|
||
|
|
||
|
for _, referencedStruct := range input {
|
||
|
structType := getStructType(referencedStruct)
|
||
|
|
||
|
if alreadyIncluded[structType] == nil {
|
||
|
alreadyIncluded[structType] = make(map[string]bool)
|
||
|
}
|
||
|
|
||
|
if !alreadyIncluded[structType][referencedStruct.GetID()] {
|
||
|
var data Data
|
||
|
err := marshalData(referencedStruct, &data, information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
includedElements = append(includedElements, data)
|
||
|
alreadyIncluded[structType][referencedStruct.GetID()] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return includedElements, nil
|
||
|
}
|
||
|
|
||
|
func marshalData(element MarshalIdentifier, data *Data, information ServerInformation) error {
|
||
|
refValue := reflect.ValueOf(element)
|
||
|
if refValue.Kind() == reflect.Ptr && refValue.IsNil() {
|
||
|
return errors.New("MarshalIdentifier must not be nil")
|
||
|
}
|
||
|
|
||
|
attributes, err := json.Marshal(element)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
data.Attributes = attributes
|
||
|
data.ID = element.GetID()
|
||
|
data.Type = getStructType(element)
|
||
|
|
||
|
if information != nil {
|
||
|
if customLinks, ok := element.(MarshalCustomLinks); ok {
|
||
|
if data.Links == nil {
|
||
|
data.Links = make(Links)
|
||
|
}
|
||
|
base := getLinkBaseURL(element, information)
|
||
|
for k, v := range customLinks.GetCustomLinks(base) {
|
||
|
if _, ok := data.Links[k]; !ok {
|
||
|
data.Links[k] = v
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if references, ok := element.(MarshalLinkedRelations); ok {
|
||
|
data.Relationships = getStructRelationships(references, information)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func isToMany(relationshipType RelationshipType, name string) bool {
|
||
|
if relationshipType == DefaultRelationship {
|
||
|
return Pluralize(name) == name
|
||
|
}
|
||
|
|
||
|
return relationshipType == ToManyRelationship
|
||
|
}
|
||
|
|
||
|
func getStructRelationships(relationer MarshalLinkedRelations, information ServerInformation) map[string]Relationship {
|
||
|
referencedIDs := relationer.GetReferencedIDs()
|
||
|
sortedResults := map[string][]ReferenceID{}
|
||
|
relationships := map[string]Relationship{}
|
||
|
|
||
|
for _, referenceID := range referencedIDs {
|
||
|
sortedResults[referenceID.Name] = append(sortedResults[referenceID.Name], referenceID)
|
||
|
}
|
||
|
|
||
|
references := relationer.GetReferences()
|
||
|
|
||
|
// helper map to check if all references are included to also include empty ones
|
||
|
notIncludedReferences := map[string]Reference{}
|
||
|
for _, reference := range references {
|
||
|
notIncludedReferences[reference.Name] = reference
|
||
|
}
|
||
|
|
||
|
for name, referenceIDs := range sortedResults {
|
||
|
relationships[name] = Relationship{}
|
||
|
|
||
|
// if referenceType is plural, we need to use an array for data, otherwise it's just an object
|
||
|
container := RelationshipDataContainer{}
|
||
|
|
||
|
if isToMany(referenceIDs[0].Relationship, referenceIDs[0].Name) {
|
||
|
// multiple elements in links
|
||
|
container.DataArray = []RelationshipData{}
|
||
|
for _, referenceID := range referenceIDs {
|
||
|
container.DataArray = append(container.DataArray, RelationshipData{
|
||
|
Type: referenceID.Type,
|
||
|
ID: referenceID.ID,
|
||
|
})
|
||
|
}
|
||
|
} else {
|
||
|
container.DataObject = &RelationshipData{
|
||
|
Type: referenceIDs[0].Type,
|
||
|
ID: referenceIDs[0].ID,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// set URLs if necessary
|
||
|
links := getLinksForServerInformation(relationer, name, information)
|
||
|
|
||
|
relationship := Relationship{
|
||
|
Data: &container,
|
||
|
Links: links,
|
||
|
}
|
||
|
|
||
|
relationships[name] = relationship
|
||
|
|
||
|
// this marks the reference as already included
|
||
|
delete(notIncludedReferences, referenceIDs[0].Name)
|
||
|
}
|
||
|
|
||
|
// check for empty references
|
||
|
for name, reference := range notIncludedReferences {
|
||
|
container := RelationshipDataContainer{}
|
||
|
|
||
|
// Plural empty relationships need an empty array and empty to-one need a null in the json
|
||
|
if !reference.IsNotLoaded && isToMany(reference.Relationship, reference.Name) {
|
||
|
container.DataArray = []RelationshipData{}
|
||
|
}
|
||
|
|
||
|
links := getLinksForServerInformation(relationer, name, information)
|
||
|
relationship := Relationship{
|
||
|
Links: links,
|
||
|
}
|
||
|
|
||
|
// skip relationship data completely if IsNotLoaded is set
|
||
|
if !reference.IsNotLoaded {
|
||
|
relationship.Data = &container
|
||
|
}
|
||
|
|
||
|
relationships[name] = relationship
|
||
|
}
|
||
|
|
||
|
return relationships
|
||
|
}
|
||
|
|
||
|
func getLinkBaseURL(element MarshalIdentifier, information ServerInformation) string {
|
||
|
prefix := strings.Trim(information.GetBaseURL(), "/")
|
||
|
namespace := strings.Trim(information.GetPrefix(), "/")
|
||
|
structType := getStructType(element)
|
||
|
|
||
|
if namespace != "" {
|
||
|
prefix += "/" + namespace
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("%s/%s/%s", prefix, structType, element.GetID())
|
||
|
}
|
||
|
|
||
|
func getLinksForServerInformation(relationer MarshalLinkedRelations, name string, information ServerInformation) Links {
|
||
|
if information == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
links := make(Links)
|
||
|
base := getLinkBaseURL(relationer, information)
|
||
|
|
||
|
links["self"] = Link{Href: fmt.Sprintf("%s/relationships/%s", base, name)}
|
||
|
links["related"] = Link{Href: fmt.Sprintf("%s/%s", base, name)}
|
||
|
|
||
|
return links
|
||
|
}
|
||
|
|
||
|
func marshalStruct(data MarshalIdentifier, information ServerInformation) (*Document, error) {
|
||
|
var contentData Data
|
||
|
|
||
|
err := marshalData(data, &contentData, information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
result := &Document{
|
||
|
Data: &DataContainer{
|
||
|
DataObject: &contentData,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
included, ok := data.(MarshalIncludedRelations)
|
||
|
if ok {
|
||
|
included, err := filterDuplicates(included.GetReferencedStructs(), information)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if len(included) > 0 {
|
||
|
result.Included = included
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func getStructType(data interface{}) string {
|
||
|
entityName, ok := data.(EntityNamer)
|
||
|
if ok {
|
||
|
return entityName.GetName()
|
||
|
}
|
||
|
|
||
|
reflectType := reflect.TypeOf(data)
|
||
|
if reflectType.Kind() == reflect.Ptr {
|
||
|
return Pluralize(Jsonify(reflectType.Elem().Name()))
|
||
|
}
|
||
|
|
||
|
return Pluralize(Jsonify(reflectType.Name()))
|
||
|
}
|