diff options
Diffstat (limited to 'vendor/github.com/google/jsonapi/response.go')
| -rw-r--r-- | vendor/github.com/google/jsonapi/response.go | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/vendor/github.com/google/jsonapi/response.go b/vendor/github.com/google/jsonapi/response.go new file mode 100644 index 0000000..b44e4e9 --- /dev/null +++ b/vendor/github.com/google/jsonapi/response.go @@ -0,0 +1,538 @@ +package jsonapi + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "strconv" + "strings" + "time" +) + +var ( + // ErrBadJSONAPIStructTag is returned when the Struct field's JSON API + // annotation is invalid. + ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") + // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field + // was not a valid numeric type. + ErrBadJSONAPIID = errors.New( + "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") + // ErrExpectedSlice is returned when a variable or argument was expected to + // be a slice of *Structs; MarshalMany will return this error when its + // interface{} argument is invalid. + ErrExpectedSlice = errors.New("models should be a slice of struct pointers") + // ErrUnexpectedType is returned when marshalling an interface; the interface + // had to be a pointer or a slice; otherwise this error is returned. + ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers") +) + +// MarshalPayload writes a jsonapi response for one or many records. The +// related records are sideloaded into the "included" array. If this method is +// given a struct pointer as an argument it will serialize in the form +// "data": {...}. If this method is given a slice of pointers, this method will +// serialize in the form "data": [...] +// +// One Example: you could pass it, w, your http.ResponseWriter, and, models, a +// ptr to a Blog to be written to the response body: +// +// func ShowBlog(w http.ResponseWriter, r *http.Request) { +// blog := &Blog{} +// +// w.Header().Set("Content-Type", jsonapi.MediaType) +// w.WriteHeader(http.StatusOK) +// +// if err := jsonapi.MarshalPayload(w, blog); err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// } +// } +// +// Many Example: you could pass it, w, your http.ResponseWriter, and, models, a +// slice of Blog struct instance pointers to be written to the response body: +// +// func ListBlogs(w http.ResponseWriter, r *http.Request) { +// blogs := []*Blog{} +// +// w.Header().Set("Content-Type", jsonapi.MediaType) +// w.WriteHeader(http.StatusOK) +// +// if err := jsonapi.MarshalPayload(w, blogs); err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// } +// } +// +func MarshalPayload(w io.Writer, models interface{}) error { + payload, err := Marshal(models) + if err != nil { + return err + } + + return json.NewEncoder(w).Encode(payload) +} + +// Marshal does the same as MarshalPayload except it just returns the payload +// and doesn't write out results. Useful if you use your own JSON rendering +// library. +func Marshal(models interface{}) (Payloader, error) { + switch vals := reflect.ValueOf(models); vals.Kind() { + case reflect.Slice: + m, err := convertToSliceInterface(&models) + if err != nil { + return nil, err + } + + payload, err := marshalMany(m) + if err != nil { + return nil, err + } + + if linkableModels, isLinkable := models.(Linkable); isLinkable { + jl := linkableModels.JSONAPILinks() + if er := jl.validate(); er != nil { + return nil, er + } + payload.Links = linkableModels.JSONAPILinks() + } + + if metableModels, ok := models.(Metable); ok { + payload.Meta = metableModels.JSONAPIMeta() + } + + return payload, nil + case reflect.Ptr: + // Check that the pointer was to a struct + if reflect.Indirect(vals).Kind() != reflect.Struct { + return nil, ErrUnexpectedType + } + return marshalOne(models) + default: + return nil, ErrUnexpectedType + } +} + +// MarshalPayloadWithoutIncluded writes a jsonapi response with one or many +// records, without the related records sideloaded into "included" array. +// If you want to serialize the relations into the "included" array see +// MarshalPayload. +// +// models interface{} should be either a struct pointer or a slice of struct +// pointers. +func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error { + payload, err := Marshal(model) + if err != nil { + return err + } + payload.clearIncluded() + + return json.NewEncoder(w).Encode(payload) +} + +// marshalOne does the same as MarshalOnePayload except it just returns the +// payload and doesn't write out results. Useful is you use your JSON rendering +// library. +func marshalOne(model interface{}) (*OnePayload, error) { + included := make(map[string]*Node) + + rootNode, err := visitModelNode(model, &included, true) + if err != nil { + return nil, err + } + payload := &OnePayload{Data: rootNode} + + payload.Included = nodeMapValues(&included) + + return payload, nil +} + +// marshalMany does the same as MarshalManyPayload except it just returns the +// payload and doesn't write out results. Useful is you use your JSON rendering +// library. +func marshalMany(models []interface{}) (*ManyPayload, error) { + payload := &ManyPayload{ + Data: []*Node{}, + } + included := map[string]*Node{} + + for _, model := range models { + node, err := visitModelNode(model, &included, true) + if err != nil { + return nil, err + } + payload.Data = append(payload.Data, node) + } + payload.Included = nodeMapValues(&included) + + return payload, nil +} + +// MarshalOnePayloadEmbedded - This method not meant to for use in +// implementation code, although feel free. The purpose of this +// method is for use in tests. In most cases, your request +// payloads for create will be embedded rather than sideloaded for +// related records. This method will serialize a single struct +// pointer into an embedded json response. In other words, there +// will be no, "included", array in the json all relationships will +// be serailized inline in the data. +// +// However, in tests, you may want to construct payloads to post +// to create methods that are embedded to most closely resemble +// the payloads that will be produced by the client. This is what +// this method is intended for. +// +// model interface{} should be a pointer to a struct. +func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { + rootNode, err := visitModelNode(model, nil, false) + if err != nil { + return err + } + + payload := &OnePayload{Data: rootNode} + + return json.NewEncoder(w).Encode(payload) +} + +func visitModelNode(model interface{}, included *map[string]*Node, + sideload bool) (*Node, error) { + node := new(Node) + + var er error + value := reflect.ValueOf(model) + if value.IsNil() { + return nil, nil + } + + modelValue := value.Elem() + modelType := value.Type().Elem() + + for i := 0; i < modelValue.NumField(); i++ { + structField := modelValue.Type().Field(i) + tag := structField.Tag.Get(annotationJSONAPI) + if tag == "" { + continue + } + + fieldValue := modelValue.Field(i) + fieldType := modelType.Field(i) + + args := strings.Split(tag, annotationSeperator) + + if len(args) < 1 { + er = ErrBadJSONAPIStructTag + break + } + + annotation := args[0] + + if (annotation == annotationClientID && len(args) != 1) || + (annotation != annotationClientID && len(args) < 2) { + er = ErrBadJSONAPIStructTag + break + } + + if annotation == annotationPrimary { + v := fieldValue + + // Deal with PTRS + var kind reflect.Kind + if fieldValue.Kind() == reflect.Ptr { + kind = fieldType.Type.Elem().Kind() + v = reflect.Indirect(fieldValue) + } else { + kind = fieldType.Type.Kind() + } + + // Handle allowed types + switch kind { + case reflect.String: + node.ID = v.Interface().(string) + case reflect.Int: + node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) + case reflect.Int8: + node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) + case reflect.Int16: + node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) + case reflect.Int32: + node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) + case reflect.Int64: + node.ID = strconv.FormatInt(v.Interface().(int64), 10) + case reflect.Uint: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) + case reflect.Uint8: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) + case reflect.Uint16: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) + case reflect.Uint32: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) + case reflect.Uint64: + node.ID = strconv.FormatUint(v.Interface().(uint64), 10) + default: + // We had a JSON float (numeric), but our field was not one of the + // allowed numeric types + er = ErrBadJSONAPIID + } + + if er != nil { + break + } + + node.Type = args[1] + } else if annotation == annotationClientID { + clientID := fieldValue.String() + if clientID != "" { + node.ClientID = clientID + } + } else if annotation == annotationAttribute { + var omitEmpty, iso8601, rfc3339 bool + + if len(args) > 2 { + for _, arg := range args[2:] { + switch arg { + case annotationOmitEmpty: + omitEmpty = true + case annotationISO8601: + iso8601 = true + case annotationRFC3339: + rfc3339 = true + } + } + } + + if node.Attributes == nil { + node.Attributes = make(map[string]interface{}) + } + + if fieldValue.Type() == reflect.TypeOf(time.Time{}) { + t := fieldValue.Interface().(time.Time) + + if t.IsZero() { + continue + } + + if iso8601 { + node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat) + } else if rfc3339 { + node.Attributes[args[1]] = t.UTC().Format(time.RFC3339) + } else { + node.Attributes[args[1]] = t.Unix() + } + } else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + // A time pointer may be nil + if fieldValue.IsNil() { + if omitEmpty { + continue + } + + node.Attributes[args[1]] = nil + } else { + tm := fieldValue.Interface().(*time.Time) + + if tm.IsZero() && omitEmpty { + continue + } + + if iso8601 { + node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat) + } else if rfc3339 { + node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339) + } else { + node.Attributes[args[1]] = tm.Unix() + } + } + } else { + // Dealing with a fieldValue that is not a time + emptyValue := reflect.Zero(fieldValue.Type()) + + // See if we need to omit this field + if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) { + continue + } + + strAttr, ok := fieldValue.Interface().(string) + if ok { + node.Attributes[args[1]] = strAttr + } else { + node.Attributes[args[1]] = fieldValue.Interface() + } + } + } else if annotation == annotationRelation { + var omitEmpty bool + + //add support for 'omitempty' struct tag for marshaling as absent + if len(args) > 2 { + omitEmpty = args[2] == annotationOmitEmpty + } + + isSlice := fieldValue.Type().Kind() == reflect.Slice + if omitEmpty && + (isSlice && fieldValue.Len() < 1 || + (!isSlice && fieldValue.IsNil())) { + continue + } + + if node.Relationships == nil { + node.Relationships = make(map[string]interface{}) + } + + var relLinks *Links + if linkableModel, ok := model.(RelationshipLinkable); ok { + relLinks = linkableModel.JSONAPIRelationshipLinks(args[1]) + } + + var relMeta *Meta + if metableModel, ok := model.(RelationshipMetable); ok { + relMeta = metableModel.JSONAPIRelationshipMeta(args[1]) + } + + if isSlice { + // to-many relationship + relationship, err := visitModelNodeRelationships( + fieldValue, + included, + sideload, + ) + if err != nil { + er = err + break + } + relationship.Links = relLinks + relationship.Meta = relMeta + + if sideload { + shallowNodes := []*Node{} + for _, n := range relationship.Data { + appendIncluded(included, n) + shallowNodes = append(shallowNodes, toShallowNode(n)) + } + + node.Relationships[args[1]] = &RelationshipManyNode{ + Data: shallowNodes, + Links: relationship.Links, + Meta: relationship.Meta, + } + } else { + node.Relationships[args[1]] = relationship + } + } else { + // to-one relationships + + // Handle null relationship case + if fieldValue.IsNil() { + node.Relationships[args[1]] = &RelationshipOneNode{Data: nil} + continue + } + + relationship, err := visitModelNode( + fieldValue.Interface(), + included, + sideload, + ) + if err != nil { + er = err + break + } + + if sideload { + appendIncluded(included, relationship) + node.Relationships[args[1]] = &RelationshipOneNode{ + Data: toShallowNode(relationship), + Links: relLinks, + Meta: relMeta, + } + } else { + node.Relationships[args[1]] = &RelationshipOneNode{ + Data: relationship, + Links: relLinks, + Meta: relMeta, + } + } + } + + } else { + er = ErrBadJSONAPIStructTag + break + } + } + + if er != nil { + return nil, er + } + + if linkableModel, isLinkable := model.(Linkable); isLinkable { + jl := linkableModel.JSONAPILinks() + if er := jl.validate(); er != nil { + return nil, er + } + node.Links = linkableModel.JSONAPILinks() + } + + if metableModel, ok := model.(Metable); ok { + node.Meta = metableModel.JSONAPIMeta() + } + + return node, nil +} + +func toShallowNode(node *Node) *Node { + return &Node{ + ID: node.ID, + Type: node.Type, + } +} + +func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node, + sideload bool) (*RelationshipManyNode, error) { + nodes := []*Node{} + + for i := 0; i < models.Len(); i++ { + n := models.Index(i).Interface() + + node, err := visitModelNode(n, included, sideload) + if err != nil { + return nil, err + } + + nodes = append(nodes, node) + } + + return &RelationshipManyNode{Data: nodes}, nil +} + +func appendIncluded(m *map[string]*Node, nodes ...*Node) { + included := *m + + for _, n := range nodes { + k := fmt.Sprintf("%s,%s", n.Type, n.ID) + + if _, hasNode := included[k]; hasNode { + continue + } + + included[k] = n + } +} + +func nodeMapValues(m *map[string]*Node) []*Node { + mp := *m + nodes := make([]*Node, len(mp)) + + i := 0 + for _, n := range mp { + nodes[i] = n + i++ + } + + return nodes +} + +func convertToSliceInterface(i *interface{}) ([]interface{}, error) { + vals := reflect.ValueOf(*i) + if vals.Kind() != reflect.Slice { + return nil, ErrExpectedSlice + } + var response []interface{} + for x := 0; x < vals.Len(); x++ { + response = append(response, vals.Index(x).Interface()) + } + return response, nil +} |
