github.com/seeker-insurance/kit@v0.0.13/jsonapi/response.go (about)

     1  package jsonapi
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/seeker-insurance/kit/web/pagination"
    14  )
    15  
    16  var (
    17  	// ErrBadJSONAPIStructTag is returned when the Struct field's JSON API
    18  	// annotation is invalid.
    19  	ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format")
    20  	// ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field
    21  	// was not a valid numeric type.
    22  	ErrBadJSONAPIID = errors.New(
    23  		"id should be either string, int(8,16,32,64) or uint(8,16,32,64)")
    24  	// ErrExpectedSlice is returned when a variable or arugment was expected to
    25  	// be a slice of *Structs; MarshalMany will return this error when its
    26  	// interface{} argument is invalid.
    27  	ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
    28  	// ErrUnexpectedType is returned when marshalling an interface; the interface
    29  	// had to be a pointer or a slice; otherwise this error is returned.
    30  	ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers")
    31  )
    32  
    33  // MarshalPayload writes a jsonapi response for one or many records. The
    34  // related records are sideloaded into the "included" array. If this method is
    35  // given a struct pointer as an argument it will serialize in the form
    36  // "data": {...}. If this method is given a slice of pointers, this method will
    37  // serialize in the form "data": [...]
    38  //
    39  // One Example: you could pass it, w, your http.ResponseWriter, and, models, a
    40  // ptr to a Blog to be written to the response body:
    41  //
    42  //	 func ShowBlog(w http.ResponseWriter, r *http.Request) {
    43  //		 blog := &Blog{}
    44  //
    45  //		 w.Header().Set("Content-Type", jsonapi.MediaType)
    46  //		 w.WriteHeader(http.StatusOK)
    47  //
    48  //		 if err := jsonapi.MarshalPayload(w, blog); err != nil {
    49  //			 http.Error(w, err.Error(), http.StatusInternalServerError)
    50  //		 }
    51  //	 }
    52  //
    53  // Many Example: you could pass it, w, your http.ResponseWriter, and, models, a
    54  // slice of Blog struct instance pointers to be written to the response body:
    55  //
    56  //	 func ListBlogs(w http.ResponseWriter, r *http.Request) {
    57  //     blogs := []*Blog{}
    58  //
    59  //		 w.Header().Set("Content-Type", jsonapi.MediaType)
    60  //		 w.WriteHeader(http.StatusOK)
    61  //
    62  //		 if err := jsonapi.MarshalPayload(w, blogs); err != nil {
    63  //			 http.Error(w, err.Error(), http.StatusInternalServerError)
    64  //		 }
    65  //	 }
    66  //
    67  func MarshalPayload(w io.Writer, models interface{}) error {
    68  	payload, err := Marshal(models)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	if err := json.NewEncoder(w).Encode(payload); err != nil {
    74  		return err
    75  	}
    76  	return nil
    77  }
    78  
    79  // MarshalPayloadPaged does the same as MarshalPayload except it takes a "paged" argument
    80  // that is appended to the meta information with the key "page"
    81  func MarshalPayloadPaged(w io.Writer, models interface{}, page *pagination.Pagination) error {
    82  	payload, err := marshal(models, page)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	if err := json.NewEncoder(w).Encode(payload); err != nil {
    88  		return err
    89  	}
    90  	return nil
    91  }
    92  
    93  // Marshal does the same as MarshalPayload except it just returns the payload
    94  // and doesn't write out results. Useful if you use your own JSON rendering
    95  // library.
    96  func Marshal(models interface{}) (Payloader, error) {
    97  	return marshal(models)
    98  }
    99  
   100  // MarshalPayloadWithoutIncluded writes a jsonapi response with one or many
   101  // records, without the related records sideloaded into "included" array.
   102  // If you want to serialize the relations into the "included" array see
   103  // MarshalPayload.
   104  //
   105  // models interface{} should be either a struct pointer or a slice of struct
   106  // pointers.
   107  func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error {
   108  	payload, err := Marshal(model)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	payload.clearIncluded()
   113  
   114  	if err := json.NewEncoder(w).Encode(payload); err != nil {
   115  		return err
   116  	}
   117  	return nil
   118  }
   119  
   120  // marshalOne does the same as MarshalOnePayload except it just returns the
   121  // payload and doesn't write out results. Useful is you use your JSON rendering
   122  // library.
   123  func marshalOne(model interface{}) (*OnePayload, error) {
   124  	included := make(map[string]*Node)
   125  
   126  	rootNode, err := visitModelNode(model, &included, true)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	payload := &OnePayload{Data: rootNode}
   131  
   132  	payload.Included = nodeMapValues(&included)
   133  
   134  	return payload, nil
   135  }
   136  
   137  // marshalMany does the same as MarshalManyPayload except it just returns the
   138  // payload and doesn't write out results. Useful is you use your JSON rendering
   139  // library.
   140  func marshalMany(models []interface{}) (*ManyPayload, error) {
   141  	payload := &ManyPayload{
   142  		Data: []*Node{},
   143  	}
   144  	included := map[string]*Node{}
   145  
   146  	for _, model := range models {
   147  		node, err := visitModelNode(model, &included, true)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		payload.Data = append(payload.Data, node)
   152  	}
   153  	payload.Included = nodeMapValues(&included)
   154  
   155  	return payload, nil
   156  }
   157  
   158  func marshal(models interface{}, page ...*pagination.Pagination) (Payloader, error) {
   159  	switch vals := reflect.ValueOf(models); vals.Kind() {
   160  	case reflect.Slice:
   161  		m, err := convertToSliceInterface(&models)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  
   166  		payload, err := marshalMany(m)
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  
   171  		if linkableModels, isLinkable := models.(Linkable); isLinkable {
   172  			jl := linkableModels.JSONAPILinks()
   173  			if er := jl.validate(); er != nil {
   174  				return nil, er
   175  			}
   176  			payload.Links = linkableModels.JSONAPILinks()
   177  		}
   178  
   179  		if metableModels, ok := models.(Metable); ok {
   180  			payload.Meta = metableModels.JSONAPIMeta()
   181  		}
   182  
   183  		if len(page) > 0 {
   184  			pg := page[0]
   185  			for k, v := range pg.Links() {
   186  				(*payload.Links)[k] = v
   187  			}
   188  		}
   189  
   190  		return payload, nil
   191  	case reflect.Ptr:
   192  		// Check that the pointer was to a struct
   193  		if reflect.Indirect(vals).Kind() != reflect.Struct {
   194  			return nil, ErrUnexpectedType
   195  		}
   196  		return marshalOne(models)
   197  	default:
   198  		return nil, ErrUnexpectedType
   199  	}
   200  }
   201  
   202  // MarshalOnePayloadEmbedded - This method not meant to for use in
   203  // implementation code, although feel free.  The purpose of this
   204  // method is for use in tests.  In most cases, your request
   205  // payloads for create will be embedded rather than sideloaded for
   206  // related records. This method will serialize a single struct
   207  // pointer into an embedded json response. In other words, there
   208  // will be no, "included", array in the json all relationships will
   209  // be serailized inline in the data.
   210  //
   211  // However, in tests, you may want to construct payloads to post
   212  // to create methods that are embedded to most closely resemble
   213  // the payloads that will be produced by the client. This is what
   214  // this method is intended for.
   215  //
   216  // model interface{} should be a pointer to a struct.
   217  func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error {
   218  	rootNode, err := visitModelNode(model, nil, false)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	payload := &OnePayload{Data: rootNode}
   224  
   225  	if err := json.NewEncoder(w).Encode(payload); err != nil {
   226  		return err
   227  	}
   228  
   229  	return nil
   230  }
   231  
   232  // visitModelNode converts models to jsonapi payloads
   233  // it handles the deepest models first. (i.e.) embedded models
   234  // this is so that upper-level attributes can overwrite lower-level attributes
   235  func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) {
   236  	node := new(Node)
   237  
   238  	var er error
   239  	value := reflect.ValueOf(model)
   240  	if value.IsNil() {
   241  		return nil, nil
   242  	}
   243  
   244  	modelValue := value.Elem()
   245  	modelType := value.Type().Elem()
   246  
   247  	// handle just the embedded models first
   248  	for i := 0; i < modelValue.NumField(); i++ {
   249  		fieldValue := modelValue.Field(i)
   250  		fieldType := modelType.Field(i)
   251  
   252  		// skip if annotated w/ ignore
   253  		tag := fieldType.Tag.Get(annotationJSONAPI)
   254  		if shouldIgnoreField(tag) {
   255  			continue
   256  		}
   257  
   258  		// handles embedded structs and pointers to embedded structs
   259  		if shouldTreatEmbeded(tag) || isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) {
   260  			var embModel interface{}
   261  			if fieldType.Type.Kind() == reflect.Ptr {
   262  				if fieldValue.IsNil() {
   263  					continue
   264  				}
   265  				embModel = fieldValue.Interface()
   266  			} else {
   267  				embModel = fieldValue.Addr().Interface()
   268  			}
   269  
   270  			embNode, err := visitModelNode(embModel, included, sideload)
   271  			if err != nil {
   272  				er = err
   273  				break
   274  			}
   275  			node.merge(embNode)
   276  		}
   277  	}
   278  
   279  	// handle everthing else
   280  	for i := 0; i < modelValue.NumField(); i++ {
   281  		fieldValue := modelValue.Field(i)
   282  		fieldType := modelType.Field(i)
   283  
   284  		tag := fieldType.Tag.Get(annotationJSONAPI)
   285  
   286  		if shouldIgnoreField(tag) {
   287  			continue
   288  		}
   289  
   290  		// skip embedded because it was handled in a previous loop
   291  		if shouldTreatEmbeded(tag) || isEmbeddedStruct(fieldType) || isEmbeddedStructPtr(fieldType) {
   292  			continue
   293  		}
   294  
   295  		if tag == "" {
   296  			continue
   297  		}
   298  
   299  		args := strings.Split(tag, annotationSeperator)
   300  
   301  		if len(args) < 1 {
   302  			er = ErrBadJSONAPIStructTag
   303  			break
   304  		}
   305  
   306  		annotation := args[0]
   307  
   308  		if (annotation == annotationClientID && len(args) != 1) ||
   309  			(annotation != annotationClientID && len(args) < 2) {
   310  			er = ErrBadJSONAPIStructTag
   311  			break
   312  		}
   313  
   314  		if annotation == annotationPrimary {
   315  			v := fieldValue
   316  
   317  			// Deal with PTRS
   318  			var kind reflect.Kind
   319  			if fieldValue.Kind() == reflect.Ptr {
   320  				kind = fieldType.Type.Elem().Kind()
   321  				v = reflect.Indirect(fieldValue)
   322  			} else {
   323  				kind = fieldType.Type.Kind()
   324  			}
   325  
   326  			// Handle allowed types
   327  			switch kind {
   328  			case reflect.String:
   329  				node.ID = v.Interface().(string)
   330  			case reflect.Int:
   331  				node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10)
   332  			case reflect.Int8:
   333  				node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10)
   334  			case reflect.Int16:
   335  				node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10)
   336  			case reflect.Int32:
   337  				node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10)
   338  			case reflect.Int64:
   339  				node.ID = strconv.FormatInt(v.Interface().(int64), 10)
   340  			case reflect.Uint:
   341  				node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10)
   342  			case reflect.Uint8:
   343  				node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10)
   344  			case reflect.Uint16:
   345  				node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10)
   346  			case reflect.Uint32:
   347  				node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10)
   348  			case reflect.Uint64:
   349  				node.ID = strconv.FormatUint(v.Interface().(uint64), 10)
   350  			default:
   351  				// We had a JSON float (numeric), but our field was not one of the
   352  				// allowed numeric types
   353  				er = ErrBadJSONAPIID
   354  				break
   355  			}
   356  
   357  			node.Type = args[1]
   358  		} else if annotation == annotationClientID {
   359  			clientID := fieldValue.String()
   360  			if clientID != "" {
   361  				node.ClientID = clientID
   362  			}
   363  		} else if annotation == annotationAttribute {
   364  			var omitEmpty, iso8601 bool
   365  
   366  			if len(args) > 2 {
   367  				for _, arg := range args[2:] {
   368  					switch arg {
   369  					case annotationOmitEmpty:
   370  						omitEmpty = true
   371  					case annotationISO8601:
   372  						iso8601 = true
   373  					}
   374  				}
   375  			}
   376  
   377  			if node.Attributes == nil {
   378  				node.Attributes = make(map[string]interface{})
   379  			}
   380  
   381  			if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
   382  				t := fieldValue.Interface().(time.Time)
   383  
   384  				if t.IsZero() {
   385  					continue
   386  				}
   387  
   388  				if iso8601 {
   389  					node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
   390  				} else {
   391  					node.Attributes[args[1]] = t.Unix()
   392  				}
   393  			} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
   394  				// A time pointer may be nil
   395  				if fieldValue.IsNil() {
   396  					if omitEmpty {
   397  						continue
   398  					}
   399  
   400  					node.Attributes[args[1]] = nil
   401  				} else {
   402  					tm := fieldValue.Interface().(*time.Time)
   403  
   404  					if tm.IsZero() && omitEmpty {
   405  						continue
   406  					}
   407  
   408  					if iso8601 {
   409  						node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
   410  					} else {
   411  						node.Attributes[args[1]] = tm.Unix()
   412  					}
   413  				}
   414  			} else {
   415  				// Dealing with a fieldValue that is not a time
   416  				emptyValue := reflect.Zero(fieldValue.Type())
   417  
   418  				// See if we need to omit this field
   419  				if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) {
   420  					continue
   421  				}
   422  
   423  				strAttr, ok := fieldValue.Interface().(string)
   424  				if ok {
   425  					node.Attributes[args[1]] = strAttr
   426  				} else {
   427  					node.Attributes[args[1]] = fieldValue.Interface()
   428  				}
   429  			}
   430  		} else if annotation == annotationRelation {
   431  			var omitEmpty bool
   432  
   433  			//add support for 'omitempty' struct tag for marshaling as absent
   434  			if len(args) > 2 {
   435  				omitEmpty = args[2] == annotationOmitEmpty
   436  			}
   437  
   438  			isSlice := fieldValue.Type().Kind() == reflect.Slice
   439  			if omitEmpty &&
   440  				(isSlice && fieldValue.Len() < 1 ||
   441  					(!isSlice && fieldValue.IsNil())) {
   442  				continue
   443  			}
   444  
   445  			if node.Relationships == nil {
   446  				node.Relationships = make(map[string]interface{})
   447  			}
   448  
   449  			var relLinks *Links
   450  			if linkableModel, ok := model.(RelationshipLinkable); ok {
   451  				relLinks = linkableModel.JSONAPIRelationshipLinks(args[1])
   452  			}
   453  
   454  			var relMeta *Meta
   455  			if metableModel, ok := model.(RelationshipMetable); ok {
   456  				relMeta = metableModel.JSONAPIRelationshipMeta(args[1])
   457  			}
   458  
   459  			if isSlice {
   460  				// to-many relationship
   461  				relationship, err := visitModelNodeRelationships(
   462  					fieldValue,
   463  					included,
   464  					sideload,
   465  				)
   466  				if err != nil {
   467  					er = err
   468  					break
   469  				}
   470  				relationship.Links = relLinks
   471  				relationship.Meta = relMeta
   472  
   473  				if sideload {
   474  					shallowNodes := []*Node{}
   475  					for _, n := range relationship.Data {
   476  						appendIncluded(included, n)
   477  						shallowNodes = append(shallowNodes, toShallowNode(n))
   478  					}
   479  
   480  					node.Relationships[args[1]] = &RelationshipManyNode{
   481  						Data:  shallowNodes,
   482  						Links: relationship.Links,
   483  						Meta:  relationship.Meta,
   484  					}
   485  				} else {
   486  					node.Relationships[args[1]] = relationship
   487  				}
   488  			} else {
   489  				// to-one relationships
   490  
   491  				// Handle null relationship case
   492  				if fieldValue.IsNil() {
   493  					node.Relationships[args[1]] = &RelationshipOneNode{Data: nil}
   494  					continue
   495  				}
   496  
   497  				relationship, err := visitModelNode(
   498  					fieldValue.Interface(),
   499  					included,
   500  					sideload,
   501  				)
   502  				if err != nil {
   503  					er = err
   504  					break
   505  				}
   506  
   507  				if sideload {
   508  					appendIncluded(included, relationship)
   509  					node.Relationships[args[1]] = &RelationshipOneNode{
   510  						Data:  toShallowNode(relationship),
   511  						Links: relLinks,
   512  						Meta:  relMeta,
   513  					}
   514  				} else {
   515  					node.Relationships[args[1]] = &RelationshipOneNode{
   516  						Data:  relationship,
   517  						Links: relLinks,
   518  						Meta:  relMeta,
   519  					}
   520  				}
   521  			}
   522  
   523  		} else {
   524  			er = ErrBadJSONAPIStructTag
   525  			break
   526  		}
   527  	}
   528  
   529  	if er != nil {
   530  		return nil, er
   531  	}
   532  
   533  	if linkableModel, isLinkable := model.(Linkable); isLinkable {
   534  		jl := linkableModel.JSONAPILinks()
   535  		if er := jl.validate(); er != nil {
   536  			return nil, er
   537  		}
   538  		node.Links = linkableModel.JSONAPILinks()
   539  	}
   540  
   541  	if metableModel, ok := model.(Metable); ok {
   542  		node.Meta = metableModel.JSONAPIMeta()
   543  	}
   544  
   545  	return node, nil
   546  }
   547  
   548  func toShallowNode(node *Node) *Node {
   549  	return &Node{
   550  		ID:   node.ID,
   551  		Type: node.Type,
   552  	}
   553  }
   554  
   555  func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node,
   556  	sideload bool) (*RelationshipManyNode, error) {
   557  	nodes := []*Node{}
   558  
   559  	for i := 0; i < models.Len(); i++ {
   560  		n := models.Index(i).Interface()
   561  
   562  		node, err := visitModelNode(n, included, sideload)
   563  		if err != nil {
   564  			return nil, err
   565  		}
   566  
   567  		nodes = append(nodes, node)
   568  	}
   569  
   570  	return &RelationshipManyNode{Data: nodes}, nil
   571  }
   572  
   573  func appendIncluded(m *map[string]*Node, nodes ...*Node) {
   574  	included := *m
   575  
   576  	for _, n := range nodes {
   577  		k := fmt.Sprintf("%s,%s", n.Type, n.ID)
   578  
   579  		if _, hasNode := included[k]; hasNode {
   580  			continue
   581  		}
   582  
   583  		included[k] = n
   584  	}
   585  }
   586  
   587  func nodeMapValues(m *map[string]*Node) []*Node {
   588  	mp := *m
   589  	nodes := make([]*Node, len(mp))
   590  
   591  	i := 0
   592  	for _, n := range mp {
   593  		nodes[i] = n
   594  		i++
   595  	}
   596  
   597  	return nodes
   598  }
   599  
   600  func convertToSliceInterface(i *interface{}) ([]interface{}, error) {
   601  	vals := reflect.ValueOf(*i)
   602  	if vals.Kind() != reflect.Slice {
   603  		return nil, ErrExpectedSlice
   604  	}
   605  	var response []interface{}
   606  	for x := 0; x < vals.Len(); x++ {
   607  		response = append(response, vals.Index(x).Interface())
   608  	}
   609  	return response, nil
   610  }
   611  
   612  func isEmbeddedStruct(sField reflect.StructField) bool {
   613  	return sField.Anonymous && sField.Type.Kind() == reflect.Struct
   614  }
   615  
   616  func isEmbeddedStructPtr(sField reflect.StructField) bool {
   617  	return sField.Anonymous && sField.Type.Kind() == reflect.Ptr && sField.Type.Elem().Kind() == reflect.Struct
   618  }
   619  
   620  func shouldIgnoreField(japiTag string) bool {
   621  	return strings.HasPrefix(japiTag, annotationIgnore)
   622  }
   623  
   624  func shouldTreatEmbeded(japiTag string) bool {
   625  	return strings.HasPrefix(japiTag, annotationEmbed)
   626  }