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 }