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 }