jsonapi-master/0000755000000000000000000000000013200641625012507 5ustar rootrootjsonapi-master/errors_test.go0000644000000000000000000000322513200641625015413 0ustar rootrootpackage jsonapi import ( "bytes" "encoding/json" "fmt" "io" "reflect" "testing" ) func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { err := &ErrorObject{Title: "Title test.", Detail: "Detail test."} var input error = err output := input.Error() if output != fmt.Sprintf("Error: %s %s\n", err.Title, err.Detail) { t.Fatal("Unexpected output.") } } func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) { var marshalErrorsTableTasts = []struct { Title string In []*ErrorObject Out map[string]interface{} }{ { Title: "TestFieldsAreSerializedAsNeeded", In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}}, Out: map[string]interface{}{"errors": []interface{}{ map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"}, }}, }, { Title: "TestMetaFieldIsSerializedProperly", In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}}, Out: map[string]interface{}{"errors": []interface{}{ map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}}, }}, }, } for _, testRow := range marshalErrorsTableTasts { t.Run(testRow.Title, func(t *testing.T) { buffer, output := bytes.NewBuffer(nil), map[string]interface{}{} var writer io.Writer = buffer _ = MarshalErrors(writer, testRow.In) json.Unmarshal(buffer.Bytes(), &output) if !reflect.DeepEqual(output, testRow.Out) { t.Fatalf("Expected: \n%#v \nto equal: \n%#v", output, testRow.Out) } }) } } jsonapi-master/LICENSE0000644000000000000000000000206613200641625013520 0ustar rootrootThe MIT License (MIT) Copyright (c) 2015 Google Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jsonapi-master/script/0000755000000000000000000000000013200641625014013 5ustar rootrootjsonapi-master/script/test0000755000000000000000000000004713200641625014721 0ustar rootroot#!/bin/bash set -e go test ./... "$@" jsonapi-master/script/example0000755000000000000000000000014713200641625015376 0ustar rootroot#!/bin/bash set -e go run examples/app.go examples/handler.go examples/fixtures.go examples/models.go jsonapi-master/request.go0000644000000000000000000003303713200641625014534 0ustar rootrootpackage jsonapi import ( "bytes" "encoding/json" "errors" "fmt" "io" "reflect" "strconv" "strings" "time" ) const ( unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" ) var ( // ErrInvalidTime is returned when a struct has a time.Time type field, but // the JSON value was not a unix timestamp integer. ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps") // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") // ErrUnknownFieldNumberType is returned when the JSON value was a float // (numeric) but the Struct field was a non numeric type (i.e. not int, uint, // float, etc) ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type") // ErrUnsupportedPtrType is returned when the Struct field was a pointer but // the JSON value was of a different type ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported") // ErrInvalidType is returned when the given type is incompatible with the expected type. ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation. ) // UnmarshalPayload converts an io into a struct instance using jsonapi tags on // struct fields. This method supports single request payloads only, at the // moment. Bulk creates and updates are not supported yet. // // Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the // object graph is complete. That is, in the "relationships" data there are type and id, // keys that correspond to records in the "included" array. // // For example you could pass it, in, req.Body and, model, a BlogPost // struct instance to populate in an http handler, // // func CreateBlog(w http.ResponseWriter, r *http.Request) { // blog := new(Blog) // // if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { // http.Error(w, err.Error(), 500) // return // } // // // ...do stuff with your blog... // // w.Header().Set("Content-Type", jsonapi.MediaType) // w.WriteHeader(201) // // if err := jsonapi.MarshalPayload(w, blog); err != nil { // http.Error(w, err.Error(), 500) // } // } // // // Visit https://github.com/google/jsonapi#create for more info. // // model interface{} should be a pointer to a struct. func UnmarshalPayload(in io.Reader, model interface{}) error { payload := new(OnePayload) if err := json.NewDecoder(in).Decode(payload); err != nil { return err } if payload.Included != nil { includedMap := make(map[string]*Node) for _, included := range payload.Included { key := fmt.Sprintf("%s,%s", included.Type, included.ID) includedMap[key] = included } return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap) } return unmarshalNode(payload.Data, reflect.ValueOf(model), nil) } // UnmarshalManyPayload converts an io into a set of struct instances using // jsonapi tags on the type's struct fields. func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { payload := new(ManyPayload) if err := json.NewDecoder(in).Decode(payload); err != nil { return nil, err } models := []interface{}{} // will be populated from the "data" includedMap := map[string]*Node{} // will be populate from the "included" if payload.Included != nil { for _, included := range payload.Included { key := fmt.Sprintf("%s,%s", included.Type, included.ID) includedMap[key] = included } } for _, data := range payload.Data { model := reflect.New(t.Elem()) err := unmarshalNode(data, model, &includedMap) if err != nil { return nil, err } models = append(models, model.Interface()) } return models, nil } func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type()) } }() modelValue := model.Elem() modelType := model.Type().Elem() var er error for i := 0; i < modelValue.NumField(); i++ { fieldType := modelType.Field(i) tag := fieldType.Tag.Get("jsonapi") if tag == "" { continue } fieldValue := modelValue.Field(i) args := strings.Split(tag, ",") 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 { if data.ID == "" { continue } // Check the JSON API Type if data.Type != args[1] { er = fmt.Errorf( "Trying to Unmarshal an object of type %#v, but %#v does not match", data.Type, args[1], ) break } // ID will have to be transmitted as astring per the JSON API spec v := reflect.ValueOf(data.ID) // Deal with PTRS var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { kind = fieldType.Type.Elem().Kind() } else { kind = fieldType.Type.Kind() } // Handle String case if kind == reflect.String { assign(fieldValue, v) continue } // Value was not a string... only other supported type was a numeric, // which would have been sent as a float value. floatValue, err := strconv.ParseFloat(data.ID, 64) if err != nil { // Could not convert the value in the "id" attr to a float er = ErrBadJSONAPIID break } // Convert the numeric float to one of the supported ID numeric types // (int[8,16,32,64] or uint[8,16,32,64]) var idValue reflect.Value switch kind { case reflect.Int: n := int(floatValue) idValue = reflect.ValueOf(&n) case reflect.Int8: n := int8(floatValue) idValue = reflect.ValueOf(&n) case reflect.Int16: n := int16(floatValue) idValue = reflect.ValueOf(&n) case reflect.Int32: n := int32(floatValue) idValue = reflect.ValueOf(&n) case reflect.Int64: n := int64(floatValue) idValue = reflect.ValueOf(&n) case reflect.Uint: n := uint(floatValue) idValue = reflect.ValueOf(&n) case reflect.Uint8: n := uint8(floatValue) idValue = reflect.ValueOf(&n) case reflect.Uint16: n := uint16(floatValue) idValue = reflect.ValueOf(&n) case reflect.Uint32: n := uint32(floatValue) idValue = reflect.ValueOf(&n) case reflect.Uint64: n := uint64(floatValue) idValue = reflect.ValueOf(&n) default: // We had a JSON float (numeric), but our field was not one of the // allowed numeric types er = ErrBadJSONAPIID break } assign(fieldValue, idValue) } else if annotation == annotationClientID { if data.ClientID == "" { continue } fieldValue.Set(reflect.ValueOf(data.ClientID)) } else if annotation == annotationAttribute { attributes := data.Attributes if attributes == nil || len(data.Attributes) == 0 { continue } var iso8601 bool if len(args) > 2 { for _, arg := range args[2:] { if arg == annotationISO8601 { iso8601 = true } } } val := attributes[args[1]] // continue if the attribute was not included in the request if val == nil { continue } v := reflect.ValueOf(val) // Handle field of type time.Time if fieldValue.Type() == reflect.TypeOf(time.Time{}) { if iso8601 { var tm string if v.Kind() == reflect.String { tm = v.Interface().(string) } else { er = ErrInvalidISO8601 break } t, err := time.Parse(iso8601TimeFormat, tm) if err != nil { er = ErrInvalidISO8601 break } fieldValue.Set(reflect.ValueOf(t)) continue } var at int64 if v.Kind() == reflect.Float64 { at = int64(v.Interface().(float64)) } else if v.Kind() == reflect.Int { at = v.Int() } else { return ErrInvalidTime } t := time.Unix(at, 0) fieldValue.Set(reflect.ValueOf(t)) continue } if fieldValue.Type() == reflect.TypeOf([]string{}) { values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { values[i] = v.Index(i).Interface().(string) } fieldValue.Set(reflect.ValueOf(values)) continue } if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { if iso8601 { var tm string if v.Kind() == reflect.String { tm = v.Interface().(string) } else { er = ErrInvalidISO8601 break } v, err := time.Parse(iso8601TimeFormat, tm) if err != nil { er = ErrInvalidISO8601 break } t := &v fieldValue.Set(reflect.ValueOf(t)) continue } var at int64 if v.Kind() == reflect.Float64 { at = int64(v.Interface().(float64)) } else if v.Kind() == reflect.Int { at = v.Int() } else { return ErrInvalidTime } v := time.Unix(at, 0) t := &v fieldValue.Set(reflect.ValueOf(t)) continue } // JSON value was a float (numeric) if v.Kind() == reflect.Float64 { floatValue := v.Interface().(float64) // The field may or may not be a pointer to a numeric; the kind var // will not contain a pointer type var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { kind = fieldType.Type.Elem().Kind() } else { kind = fieldType.Type.Kind() } var numericValue reflect.Value switch kind { case reflect.Int: n := int(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Int8: n := int8(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Int16: n := int16(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Int32: n := int32(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Int64: n := int64(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Uint: n := uint(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Uint8: n := uint8(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Uint16: n := uint16(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Uint32: n := uint32(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Uint64: n := uint64(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Float32: n := float32(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Float64: n := floatValue numericValue = reflect.ValueOf(&n) default: return ErrUnknownFieldNumberType } assign(fieldValue, numericValue) continue } // Field was a Pointer type if fieldValue.Kind() == reflect.Ptr { var concreteVal reflect.Value switch cVal := val.(type) { case string: concreteVal = reflect.ValueOf(&cVal) case bool: concreteVal = reflect.ValueOf(&cVal) case complex64: concreteVal = reflect.ValueOf(&cVal) case complex128: concreteVal = reflect.ValueOf(&cVal) case uintptr: concreteVal = reflect.ValueOf(&cVal) default: return ErrUnsupportedPtrType } if fieldValue.Type() != concreteVal.Type() { return ErrUnsupportedPtrType } fieldValue.Set(concreteVal) continue } // As a final catch-all, ensure types line up to avoid a runtime panic. if fieldValue.Kind() != v.Kind() { return ErrInvalidType } fieldValue.Set(reflect.ValueOf(val)) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice if data.Relationships == nil || data.Relationships[args[1]] == nil { continue } if isSlice { // to-many relationship relationship := new(RelationshipManyNode) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode(data.Relationships[args[1]]) json.NewDecoder(buf).Decode(relationship) data := relationship.Data models := reflect.New(fieldValue.Type()).Elem() for _, n := range data { m := reflect.New(fieldValue.Type().Elem().Elem()) if err := unmarshalNode( fullNode(n, included), m, included, ); err != nil { er = err break } models = reflect.Append(models, m) } fieldValue.Set(models) } else { // to-one relationships relationship := new(RelationshipOneNode) buf := bytes.NewBuffer(nil) json.NewEncoder(buf).Encode( data.Relationships[args[1]], ) json.NewDecoder(buf).Decode(relationship) /* http://jsonapi.org/format/#document-resource-object-relationships http://jsonapi.org/format/#document-resource-object-linkage relationship can have a data node set to null (e.g. to disassociate the relationship) so unmarshal and set fieldValue only if data obj is not null */ if relationship.Data == nil { continue } m := reflect.New(fieldValue.Type().Elem()) if err := unmarshalNode( fullNode(relationship.Data, included), m, included, ); err != nil { er = err break } fieldValue.Set(m) } } else { er = fmt.Errorf(unsuportedStructTagMsg, annotation) } } return er } func fullNode(n *Node, included *map[string]*Node) *Node { includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID) if included != nil && (*included)[includedKey] != nil { return (*included)[includedKey] } return n } // assign will take the value specified and assign it to the field; if // field is expecting a ptr assign will assign a ptr. func assign(field, value reflect.Value) { if field.Kind() == reflect.Ptr { field.Set(value) } else { field.Set(reflect.Indirect(value)) } } jsonapi-master/request_test.go0000644000000000000000000005020313200641625015565 0ustar rootrootpackage jsonapi import ( "bytes" "encoding/json" "fmt" "io" "reflect" "sort" "strings" "testing" "time" ) func TestUnmarshall_attrStringSlice(t *testing.T) { out := &Book{} tags := []string{"fiction", "sale"} data := map[string]interface{}{ "data": map[string]interface{}{ "type": "books", "id": "1", "attributes": map[string]interface{}{"tags": tags}, }, } b, err := json.Marshal(data) if err != nil { t.Fatal(err) } if err := UnmarshalPayload(bytes.NewReader(b), out); err != nil { t.Fatal(err) } if e, a := len(tags), len(out.Tags); e != a { t.Fatalf("Was expecting %d tags, got %d", e, a) } sort.Strings(tags) sort.Strings(out.Tags) for i, tag := range tags { if e, a := tag, out.Tags[i]; e != a { t.Fatalf("At index %d, was expecting %s got %s", i, e, a) } } } func TestUnmarshalToStructWithPointerAttr(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": "The name", "is-active": true, "int-val": 8, "float-val": 1.1, } if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { t.Fatal(err) } if *out.Name != "The name" { t.Fatalf("Error unmarshalling to string ptr") } if !*out.IsActive { t.Fatalf("Error unmarshalling to bool ptr") } if *out.IntVal != 8 { t.Fatalf("Error unmarshalling to int ptr") } if *out.FloatVal != 1.1 { t.Fatalf("Error unmarshalling to float ptr") } } func TestUnmarshalPayload_ptrsAllNil(t *testing.T) { out := new(WithPointer) if err := UnmarshalPayload( strings.NewReader(`{"data": {}}`), out); err != nil { t.Fatalf("Error unmarshalling to Foo") } if out.ID != nil { t.Fatalf("Error unmarshalling; expected ID ptr to be nil") } } func TestUnmarshalPayloadWithPointerID(t *testing.T) { out := new(WithPointer) attrs := map[string]interface{}{} if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil { t.Fatalf("Error unmarshalling to Foo") } // these were present in the payload -- expect val to be not nil if out.ID == nil { t.Fatalf("Error unmarshalling; expected ID ptr to be not nil") } if e, a := uint64(2), *out.ID; e != a { t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a) } } func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": "The name", "is-active": true, } if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { t.Fatalf("Error unmarshalling to Foo") } // these were present in the payload -- expect val to be not nil if out.Name == nil || out.IsActive == nil { t.Fatalf("Error unmarshalling; expected ptr to be not nil") } // these were absent in the payload -- expect val to be nil if out.IntVal != nil || out.FloatVal != nil { t.Fatalf("Error unmarshalling; expected ptr to be nil") } } func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": true, // This is the wrong type. } expectedErrorMessage := ErrUnsupportedPtrType.Error() err := UnmarshalPayload(sampleWithPointerPayload(in), out) if err == nil { t.Fatalf("Expected error due to invalid type.") } if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } } func TestStringPointerField(t *testing.T) { // Build Book payload description := "Hello World!" data := map[string]interface{}{ "data": map[string]interface{}{ "type": "books", "id": "5", "attributes": map[string]interface{}{ "author": "aren55555", "description": description, "isbn": "", }, }, } payload, err := json.Marshal(data) if err != nil { t.Fatal(err) } // Parse JSON API payload book := new(Book) if err := UnmarshalPayload(bytes.NewReader(payload), book); err != nil { t.Fatal(err) } if book.Description == nil { t.Fatal("Was not expecting a nil pointer for book.Description") } if expected, actual := description, *book.Description; expected != actual { t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual) } } func TestMalformedTag(t *testing.T) { out := new(BadModel) err := UnmarshalPayload(samplePayload(), out) if err == nil || err != ErrBadJSONAPIStructTag { t.Fatalf("Did not error out with wrong number of arguments in tag") } } func TestUnmarshalInvalidJSON(t *testing.T) { in := strings.NewReader("{}") out := new(Blog) err := UnmarshalPayload(in, out) if err == nil { t.Fatalf("Did not error out the invalid JSON.") } } func TestUnmarshalInvalidJSON_BadType(t *testing.T) { var badTypeTests = []struct { Field string BadValue interface{} Error error }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. {Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType}, // Expected string. {Field: "float_field", BadValue: "A string.", Error: ErrInvalidType}, // Expected float64. {Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected int64. {Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64. } for _, test := range badTypeTests { t.Run(fmt.Sprintf("Test_%s", test.Field), func(t *testing.T) { out := new(ModelBadTypes) in := map[string]interface{}{} in[test.Field] = test.BadValue expectedErrorMessage := test.Error.Error() err := UnmarshalPayload(samplePayloadWithBadTypes(in), out) if err == nil { t.Fatalf("Expected error due to invalid type.") } if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } }) } } func TestUnmarshalSetsID(t *testing.T) { in := samplePayloadWithID() out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if out.ID != 2 { t.Fatalf("Did not set ID on dst interface") } } func TestUnmarshal_nonNumericID(t *testing.T) { data := samplePayloadWithoutIncluded() data["data"].(map[string]interface{})["id"] = "non-numeric-id" payload, _ := payload(data) in := bytes.NewReader(payload) out := new(Post) if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID { t.Fatalf( "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, ) } } func TestUnmarshalSetsAttrs(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CreatedAt.IsZero() { t.Fatalf("Did not parse time") } if out.ViewCount != 1000 { t.Fatalf("View count not properly serialized") } } func TestUnmarshalParsesISO8601(t *testing.T) { payload := &OnePayload{ Data: &Node{ Type: "timestamps", Attributes: map[string]interface{}{ "timestamp": "2016-08-17T08:27:12Z", }, }, } in := bytes.NewBuffer(nil) json.NewEncoder(in).Encode(payload) out := new(Timestamp) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) if !out.Time.Equal(expected) { t.Fatal("Parsing the ISO8601 timestamp failed") } } func TestUnmarshalParsesISO8601TimePointer(t *testing.T) { payload := &OnePayload{ Data: &Node{ Type: "timestamps", Attributes: map[string]interface{}{ "next": "2016-08-17T08:27:12Z", }, }, } in := bytes.NewBuffer(nil) json.NewEncoder(in).Encode(payload) out := new(Timestamp) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) if !out.Next.Equal(expected) { t.Fatal("Parsing the ISO8601 timestamp failed") } } func TestUnmarshalInvalidISO8601(t *testing.T) { payload := &OnePayload{ Data: &Node{ Type: "timestamps", Attributes: map[string]interface{}{ "timestamp": "17 Aug 16 08:027 MST", }, }, } in := bytes.NewBuffer(nil) json.NewEncoder(in).Encode(payload) out := new(Timestamp) if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 { t.Fatalf("Expected ErrInvalidISO8601, got %v", err) } } func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { data, _ := payload(samplePayloadWithoutIncluded()) in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } // Verify each comment has at least an ID for _, comment := range out.Comments { if comment.ID == 0 { t.Fatalf("The comment did not have an ID") } } } func TestUnmarshalRelationships(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Bas" || out.CurrentPost.Body != "Fuubar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } } func TestUnmarshalNullRelationship(t *testing.T) { sample := map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "latest_comment": map[string]interface{}{ "data": nil, // empty to-one relationship }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if out.LatestComment != nil { t.Fatalf("Latest Comment was not set to nil") } } func TestUnmarshalNullRelationshipInSlice(t *testing.T) { sample := map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "comments": map[string]interface{}{ "data": []interface{}{}, // empty to-many relationships }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) out := new(Post) if err := UnmarshalPayload(in, out); err != nil { t.Fatal(err) } if len(out.Comments) != 0 { t.Fatalf("Wrong number of comments; Comments should be empty") } } func TestUnmarshalNestedRelationships(t *testing.T) { out, err := unmarshalSamplePayload() if err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(out.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } } func TestUnmarshalRelationshipsSerializedEmbedded(t *testing.T) { out := sampleSerializedEmbeddedTestModel() if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } if out.Posts[0].LatestComment.Body != "foo" { t.Fatalf("The comment body was not set") } } func TestUnmarshalNestedRelationshipsEmbedded(t *testing.T) { out := bytes.NewBuffer(nil) if err := MarshalOnePayloadEmbedded(out, testModel()); err != nil { t.Fatal(err) } model := new(Blog) if err := UnmarshalPayload(out, model); err != nil { t.Fatal(err) } if model.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if model.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(model.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } if model.CurrentPost.Comments[0].Body != "foo" { t.Fatalf("Comment body not set") } } func TestUnmarshalRelationshipsSideloaded(t *testing.T) { payload := samplePayloadWithSideloaded() out := new(Blog) if err := UnmarshalPayload(payload, out); err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { t.Fatalf("Attributes were not set") } if len(out.Posts) != 2 { t.Fatalf("There should have been 2 posts") } } func TestUnmarshalNestedRelationshipsSideloaded(t *testing.T) { payload := samplePayloadWithSideloaded() out := new(Blog) if err := UnmarshalPayload(payload, out); err != nil { t.Fatal(err) } if out.CurrentPost == nil { t.Fatalf("Current post was not materialized") } if out.CurrentPost.Comments == nil { t.Fatalf("Did not materialize nested records, comments") } if len(out.CurrentPost.Comments) != 2 { t.Fatalf("Wrong number of comments") } if out.CurrentPost.Comments[0].Body != "foo" { t.Fatalf("Comment body not set") } } func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { model := new(Blog) if err := UnmarshalPayload(samplePayload(), model); err != nil { t.Fatal(err) } if model.Posts[0].ClientID == "" { t.Fatalf("ClientID not set from request on related record") } } func unmarshalSamplePayload() (*Blog, error) { in := samplePayload() out := new(Blog) if err := UnmarshalPayload(in, out); err != nil { return nil, err } return out, nil } func TestUnmarshalManyPayload(t *testing.T) { sample := map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "First", "title": "Post", }, }, map[string]interface{}{ "type": "posts", "id": "2", "attributes": map[string]interface{}{ "body": "Second", "title": "Post", }, }, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) if err != nil { t.Fatal(err) } if len(posts) != 2 { t.Fatal("Wrong number of posts") } for _, p := range posts { _, ok := p.(*Post) if !ok { t.Fatal("Was expecting a Post") } } } func TestManyPayload_withLinks(t *testing.T) { firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50" prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0" nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100" lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500" sample := map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "First", "title": "Post", }, }, map[string]interface{}{ "type": "posts", "id": "2", "attributes": map[string]interface{}{ "body": "Second", "title": "Post", }, }, }, "links": map[string]interface{}{ KeyFirstPage: firstPageURL, KeyPreviousPage: prevPageURL, KeyNextPage: nextPageURL, KeyLastPage: lastPageURL, }, } data, err := json.Marshal(sample) if err != nil { t.Fatal(err) } in := bytes.NewReader(data) payload := new(ManyPayload) if err = json.NewDecoder(in).Decode(payload); err != nil { t.Fatal(err) } if payload.Links == nil { t.Fatal("Was expecting a non nil ptr Link field") } links := *payload.Links first, ok := links[KeyFirstPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := firstPageURL, first; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a) } prev, ok := links[KeyPreviousPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := prevPageURL, prev; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a) } next, ok := links[KeyNextPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := nextPageURL, next; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a) } last, ok := links[KeyLastPage] if !ok { t.Fatal("Was expecting a non nil ptr Link field") } if e, a := lastPageURL, last; e != a { t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a) } } func samplePayloadWithoutIncluded() map[string]interface{} { return map[string]interface{}{ "data": map[string]interface{}{ "type": "posts", "id": "1", "attributes": map[string]interface{}{ "body": "Hello", "title": "World", }, "relationships": map[string]interface{}{ "comments": map[string]interface{}{ "data": []interface{}{ map[string]interface{}{ "type": "comments", "id": "123", }, map[string]interface{}{ "type": "comments", "id": "456", }, }, }, "latest_comment": map[string]interface{}{ "data": map[string]interface{}{ "type": "comments", "id": "55555", }, }, }, }, } } func payload(data map[string]interface{}) (result []byte, err error) { result, err = json.Marshal(data) return } func samplePayload() io.Reader { payload := &OnePayload{ Data: &Node{ Type: "blogs", Attributes: map[string]interface{}{ "title": "New blog", "created_at": 1436216820, "view_count": 1000, }, Relationships: map[string]interface{}{ "posts": &RelationshipManyNode{ Data: []*Node{ { Type: "posts", Attributes: map[string]interface{}{ "title": "Foo", "body": "Bar", }, ClientID: "1", }, { Type: "posts", Attributes: map[string]interface{}{ "title": "X", "body": "Y", }, ClientID: "2", }, }, }, "current_post": &RelationshipOneNode{ Data: &Node{ Type: "posts", Attributes: map[string]interface{}{ "title": "Bas", "body": "Fuubar", }, ClientID: "3", Relationships: map[string]interface{}{ "comments": &RelationshipManyNode{ Data: []*Node{ { Type: "comments", Attributes: map[string]interface{}{ "body": "Great post!", }, ClientID: "4", }, { Type: "comments", Attributes: map[string]interface{}{ "body": "Needs some work!", }, ClientID: "5", }, }, }, }, }, }, }, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func samplePayloadWithID() io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "blogs", Attributes: map[string]interface{}{ "title": "New blog", "view_count": 1000, }, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "badtypes", Attributes: m, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func sampleWithPointerPayload(m map[string]interface{}) io.Reader { payload := &OnePayload{ Data: &Node{ ID: "2", Type: "with-pointers", Attributes: m, }, } out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(payload) return out } func testModel() *Blog { return &Blog{ ID: 5, ClientID: "1", Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, { ID: 2, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 3, Body: "bas", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, } } func samplePayloadWithSideloaded() io.Reader { testModel := testModel() out := bytes.NewBuffer(nil) MarshalPayload(out, testModel) return out } func sampleSerializedEmbeddedTestModel() *Blog { out := bytes.NewBuffer(nil) MarshalOnePayloadEmbedded(out, testModel()) blog := new(Blog) UnmarshalPayload(out, blog) return blog } jsonapi-master/.gitignore0000644000000000000000000000003113200641625014471 0ustar rootroot/bin/ /pkg/ /src/ /.idea jsonapi-master/response.go0000644000000000000000000003323713200641625014704 0ustar rootrootpackage 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 } if err := json.NewEncoder(w).Encode(payload); err != nil { return err } return nil } // 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() if err := json.NewEncoder(w).Encode(payload); err != nil { return err } return nil } // 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} if err := json.NewEncoder(w).Encode(payload); err != nil { return err } return nil } func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) { node := new(Node) var er error modelValue := reflect.ValueOf(model).Elem() modelType := reflect.ValueOf(model).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 break } node.Type = args[1] } else if annotation == annotationClientID { clientID := fieldValue.String() if clientID != "" { node.ClientID = clientID } } else if annotation == annotationAttribute { var omitEmpty, iso8601 bool if len(args) > 2 { for _, arg := range args[2:] { switch arg { case annotationOmitEmpty: omitEmpty = true case annotationISO8601: iso8601 = 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 { 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 { 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 && 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 } jsonapi-master/errors.go0000644000000000000000000000420213200641625014350 0ustar rootrootpackage jsonapi import ( "encoding/json" "fmt" "io" ) // MarshalErrors writes a JSON API response using the given `[]error`. // // For more information on JSON API error payloads, see the spec here: // http://jsonapi.org/format/#document-top-level // and here: http://jsonapi.org/format/#error-objects. func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error { if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil { return err } return nil } // ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. type ErrorsPayload struct { Errors []*ErrorObject `json:"errors"` } // ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. // // The main idea behind this struct is that you can use it directly in your code as an error type // and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. // For more information on Golang errors, see: https://golang.org/pkg/errors/ // For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects type ErrorObject struct { // ID is a unique identifier for this particular occurrence of a problem. ID string `json:"id,omitempty"` // Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. Title string `json:"title,omitempty"` // Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. Detail string `json:"detail,omitempty"` // Status is the HTTP status code applicable to this problem, expressed as a string value. Status string `json:"status,omitempty"` // Code is an application-specific error code, expressed as a string value. Code string `json:"code,omitempty"` // Meta is an object containing non-standard meta-information about the error. Meta *map[string]interface{} `json:"meta,omitempty"` } // Error implements the `Error` interface. func (e *ErrorObject) Error() string { return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) } jsonapi-master/response_test.go0000644000000000000000000005013013200641625015732 0ustar rootrootpackage jsonapi import ( "bytes" "encoding/json" "reflect" "sort" "testing" "time" ) func TestMarshalPayload(t *testing.T) { book := &Book{ID: 1} books := []*Book{book, {ID: 2}} var jsonData map[string]interface{} // One out1 := bytes.NewBuffer(nil) MarshalPayload(out1, book) if err := json.Unmarshal(out1.Bytes(), &jsonData); err != nil { t.Fatal(err) } if _, ok := jsonData["data"].(map[string]interface{}); !ok { t.Fatalf("data key did not contain an Hash/Dict/Map") } // Many out2 := bytes.NewBuffer(nil) MarshalPayload(out2, books) if err := json.Unmarshal(out2.Bytes(), &jsonData); err != nil { t.Fatal(err) } if _, ok := jsonData["data"].([]interface{}); !ok { t.Fatalf("data key did not contain an Array") } } func TestMarshal_attrStringSlice(t *testing.T) { tags := []string{"fiction", "sale"} b := &Book{ID: 1, Tags: tags} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, b); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } jsonTags := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})["tags"].([]interface{}) if e, a := len(tags), len(jsonTags); e != a { t.Fatalf("Was expecting tags of length %d got %d", e, a) } // Convert from []interface{} to []string jsonTagsStrings := []string{} for _, tag := range jsonTags { jsonTagsStrings = append(jsonTagsStrings, tag.(string)) } // Sort both sort.Strings(jsonTagsStrings) sort.Strings(tags) for i, tag := range tags { if e, a := tag, jsonTagsStrings[i]; e != a { t.Fatalf("At index %d, was expecting %s got %s", i, e, a) } } } func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) { blog := &Blog{} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{}) // Verifiy the "posts" relation was an empty array posts, ok := relationships["posts"] if !ok { t.Fatal("Was expecting the data.relationships.posts key/value to have been present") } postsMap, ok := posts.(map[string]interface{}) if !ok { t.Fatal("data.relationships.posts was not a map") } postsData, ok := postsMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present") } postsDataSlice, ok := postsData.([]interface{}) if !ok { t.Fatal("data.relationships.posts.data was not a slice []") } if len(postsDataSlice) != 0 { t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []") } // Verifiy the "current_post" was a null currentPost, postExists := relationships["current_post"] if !postExists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") } currentPostMap, ok := currentPost.(map[string]interface{}) if !ok { t.Fatal("data.relationships.current_post was not a map") } currentPostData, ok := currentPostMap["data"] if !ok { t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present") } if currentPostData != nil { t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null") } } func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) { type BlogOptionalPosts struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts,omitempty"` CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` } blog := &BlogOptionalPosts{ID: 999} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify relationship was NOT set if val, exists := payload["relationships"]; exists { t.Fatalf("Was expecting the data.relationships key/value to have been empty - it was not and had a value of %v", val) } } func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) { type BlogOptionalPosts struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts,omitempty"` CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` } blog := &BlogOptionalPosts{ ID: 999, CurrentPost: &Post{ ID: 123, }, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, blog); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify relationship was set if _, exists := payload["relationships"]; !exists { t.Fatal("Was expecting the data.relationships key/value to have NOT been empty") } relationships := payload["relationships"].(map[string]interface{}) // Verify the relationship was not omitted, and is not null if val, exists := relationships["current_post"]; !exists { t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") } else if val.(map[string]interface{})["data"] == nil { t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null") } } func TestMarshalIDPtr(t *testing.T) { id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang" car := &Car{ ID: &id, Make: &make, Model: &model, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, car); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } data := jsonData["data"].(map[string]interface{}) // attributes := data["attributes"].(map[string]interface{}) // Verify that the ID was sent val, exists := data["id"] if !exists { t.Fatal("Was expecting the data.id member to exist") } if val != id { t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val) } } func TestMarshalOnePayload_omitIDString(t *testing.T) { type Foo struct { ID string `jsonapi:"primary,foo"` Title string `jsonapi:"attr,title"` } foo := &Foo{Title: "Foo"} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, foo); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } payload := jsonData["data"].(map[string]interface{}) // Verify that empty ID of type string gets omitted. See: // https://github.com/google/jsonapi/issues/83#issuecomment-285611425 _, ok := payload["id"] if ok { t.Fatal("Was expecting the data.id member to be omitted") } } func TestMarshall_invalidIDType(t *testing.T) { type badIDStruct struct { ID *bool `jsonapi:"primary,cars"` } id := true o := &badIDStruct{ID: &id} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, o); err != ErrBadJSONAPIID { t.Fatalf( "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, ) } } func TestOmitsEmptyAnnotation(t *testing.T) { book := &Book{ Author: "aren55555", PublishedAt: time.Now().AddDate(0, -1, 0), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, book); err != nil { t.Fatal(err) } var jsonData map[string]interface{} if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { t.Fatal(err) } attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{}) // Verify that the specifically omitted field were omitted if val, exists := attributes["title"]; exists { t.Fatalf("Was expecting the data.attributes.title key/value to have been omitted - it was not and had a value of %v", val) } if val, exists := attributes["pages"]; exists { t.Fatalf("Was expecting the data.attributes.pages key/value to have been omitted - it was not and had a value of %v", val) } // Verify the implicitly omitted fields were omitted if val, exists := attributes["PublishedAt"]; exists { t.Fatalf("Was expecting the data.attributes.PublishedAt key/value to have been implicitly omitted - it was not and had a value of %v", val) } // Verify the unset fields were not omitted if _, exists := attributes["isbn"]; !exists { t.Fatal("Was expecting the data.attributes.isbn key/value to have NOT been omitted") } } func TestHasPrimaryAnnotation(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Type != "blogs" { t.Fatalf("type should have been blogs, got %s", data.Type) } if data.ID != "5" { t.Fatalf("ID not transferred") } } func TestSupportsAttributes(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Attributes == nil { t.Fatalf("Expected attributes") } if data.Attributes["title"] != "Title 1" { t.Fatalf("Attributes hash not populated using tags correctly") } } func TestOmitsZeroTimes(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Time{}, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Attributes == nil { t.Fatalf("Expected attributes") } if data.Attributes["created_at"] != nil { t.Fatalf("Created at was serialized even though it was a zero Time") } } func TestMarshalISO8601Time(t *testing.T) { testModel := &Timestamp{ ID: 5, Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Attributes == nil { t.Fatalf("Expected attributes") } if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" { t.Fatal("Timestamp was not serialised into ISO8601 correctly") } } func TestMarshalISO8601TimePointer(t *testing.T) { tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC) testModel := &Timestamp{ ID: 5, Next: &tm, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Attributes == nil { t.Fatalf("Expected attributes") } if data.Attributes["next"] != "2016-08-17T08:27:12Z" { t.Fatal("Next was not serialised into ISO8601 correctly") } } func TestSupportsLinkable(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Links == nil { t.Fatal("Expected data.links") } links := *data.Links self, hasSelf := links["self"] if !hasSelf { t.Fatal("Expected 'self' link to be present") } if _, isString := self.(string); !isString { t.Fatal("Expected 'self' to contain a string") } comments, hasComments := links["comments"] if !hasComments { t.Fatal("expect 'comments' to be present") } commentsMap, isMap := comments.(map[string]interface{}) if !isMap { t.Fatal("Expected 'comments' to contain a map") } commentsHref, hasHref := commentsMap["href"] if !hasHref { t.Fatal("Expect 'comments' to contain an 'href' key/value") } if _, isString := commentsHref.(string); !isString { t.Fatal("Expected 'href' to contain a string") } commentsMeta, hasMeta := commentsMap["meta"] if !hasMeta { t.Fatal("Expect 'comments' to contain a 'meta' key/value") } commentsMetaMap, isMap := commentsMeta.(map[string]interface{}) if !isMap { t.Fatal("Expected 'comments' to contain a map") } commentsMetaObject := Meta(commentsMetaMap) countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{}) if !isMap { t.Fatal("Expected 'counts' to contain a map") } for k, v := range countsMap { if _, isNum := v.(float64); !isNum { t.Fatalf("Exepected value at '%s' to be a numeric (float64)", k) } } } func TestInvalidLinkable(t *testing.T) { testModel := &BadComment{ ID: 5, Body: "Hello World", } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err == nil { t.Fatal("Was expecting an error") } } func TestSupportsMetable(t *testing.T) { testModel := &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } data := resp.Data if data.Meta == nil { t.Fatalf("Expected data.meta") } meta := Meta(*data.Meta) if e, a := "extra details regarding the blog", meta["detail"]; e != a { t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a) } } func TestRelations(t *testing.T) { testModel := testBlog() out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } relations := resp.Data.Relationships if relations == nil { t.Fatalf("Relationships were not materialized") } if relations["posts"] == nil { t.Fatalf("Posts relationship was not materialized") } else { if relations["posts"].(map[string]interface{})["links"] == nil { t.Fatalf("Posts relationship links were not materialized") } if relations["posts"].(map[string]interface{})["meta"] == nil { t.Fatalf("Posts relationship meta were not materialized") } } if relations["current_post"] == nil { t.Fatalf("Current post relationship was not materialized") } else { if relations["current_post"].(map[string]interface{})["links"] == nil { t.Fatalf("Current post relationship links were not materialized") } if relations["current_post"].(map[string]interface{})["meta"] == nil { t.Fatalf("Current post relationship meta were not materialized") } } if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 { t.Fatalf("Did not materialize two posts") } } func TestNoRelations(t *testing.T) { testModel := &Blog{ID: 1, Title: "Title 1", CreatedAt: time.Now()} out := bytes.NewBuffer(nil) if err := MarshalPayload(out, testModel); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalPayloadWithoutIncluded(t *testing.T) { data := &Post{ ID: 1, BlogID: 2, ClientID: "123e4567-e89b-12d3-a456-426655440000", Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 20, Body: "First", }, { ID: 21, Body: "Hello World", }, }, LatestComment: &Comment{ ID: 22, Body: "Cool!", }, } out := bytes.NewBuffer(nil) if err := MarshalPayloadWithoutIncluded(out, data); err != nil { t.Fatal(err) } resp := new(OnePayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalPayload_many(t *testing.T) { data := []interface{}{ &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", }, { ID: 2, Title: "Fuubar", Body: "Bas", }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", }, }, &Blog{ ID: 6, Title: "Title 2", CreatedAt: time.Now(), Posts: []*Post{ { ID: 3, Title: "Foo", Body: "Bar", }, { ID: 4, Title: "Fuubar", Body: "Bas", }, }, CurrentPost: &Post{ ID: 4, Title: "Foo", Body: "Bar", }, }, } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } } func TestMarshalMany_WithSliceOfStructPointers(t *testing.T) { var data []*Blog for len(data) < 2 { data = append(data, testBlog()) } out := bytes.NewBuffer(nil) if err := MarshalPayload(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } } func TestMarshalManyWithoutIncluded(t *testing.T) { var data []*Blog for len(data) < 2 { data = append(data, testBlog()) } out := bytes.NewBuffer(nil) if err := MarshalPayloadWithoutIncluded(out, data); err != nil { t.Fatal(err) } resp := new(ManyPayload) if err := json.NewDecoder(out).Decode(resp); err != nil { t.Fatal(err) } d := resp.Data if len(d) != 2 { t.Fatalf("data should have two elements") } if resp.Included != nil { t.Fatalf("Encoding json response did not omit included") } } func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { structs := []*Book{ {ID: 1, Author: "aren55555", ISBN: "abc"}, {ID: 2, Author: "shwoodard", ISBN: "xyz"}, } interfaces := []interface{}{} for _, s := range structs { interfaces = append(interfaces, s) } // Perform Marshals structsOut := new(bytes.Buffer) if err := MarshalPayload(structsOut, structs); err != nil { t.Fatal(err) } interfacesOut := new(bytes.Buffer) if err := MarshalPayload(interfacesOut, interfaces); err != nil { t.Fatal(err) } // Generic JSON Unmarshal structsData, interfacesData := make(map[string]interface{}), make(map[string]interface{}) if err := json.Unmarshal(structsOut.Bytes(), &structsData); err != nil { t.Fatal(err) } if err := json.Unmarshal(interfacesOut.Bytes(), &interfacesData); err != nil { t.Fatal(err) } // Compare Result if !reflect.DeepEqual(structsData, interfacesData) { t.Fatal("Was expecting the JSON API generated to be the same") } } func TestMarshal_InvalidIntefaceArgument(t *testing.T) { out := new(bytes.Buffer) if err := MarshalPayload(out, true); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } if err := MarshalPayload(out, 25); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } if err := MarshalPayload(out, Book{}); err != ErrUnexpectedType { t.Fatal("Was expecting an error") } } func testBlog() *Blog { return &Blog{ ID: 5, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, { ID: 2, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 3, Body: "bas", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, }, CurrentPost: &Post{ ID: 1, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1, Body: "foo", }, { ID: 2, Body: "bar", }, }, LatestComment: &Comment{ ID: 1, Body: "foo", }, }, } } jsonapi-master/node.go0000644000000000000000000000744613200641625013776 0ustar rootrootpackage jsonapi import "fmt" // Payloader is used to encapsulate the One and Many payload types type Payloader interface { clearIncluded() } // OnePayload is used to represent a generic JSON API payload where a single // resource (Node) was included as an {} in the "data" key type OnePayload struct { Data *Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } func (p *OnePayload) clearIncluded() { p.Included = []*Node{} } // ManyPayload is used to represent a generic JSON API payload where many // resources (Nodes) were included in an [] in the "data" key type ManyPayload struct { Data []*Node `json:"data"` Included []*Node `json:"included,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } func (p *ManyPayload) clearIncluded() { p.Included = []*Node{} } // Node is used to represent a generic JSON API Resource type Node struct { Type string `json:"type"` ID string `json:"id,omitempty"` ClientID string `json:"client-id,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` Relationships map[string]interface{} `json:"relationships,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // RelationshipOneNode is used to represent a generic has one JSON API relation type RelationshipOneNode struct { Data *Node `json:"data"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // RelationshipManyNode is used to represent a generic has many JSON API // relation type RelationshipManyNode struct { Data []*Node `json:"data"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,omitempty"` } // Links is used to represent a `links` object. // http://jsonapi.org/format/#document-links type Links map[string]interface{} func (l *Links) validate() (err error) { // Each member of a links object is a “link”. A link MUST be represented as // either: // - a string containing the link’s URL. // - an object (“link object”) which can contain the following members: // - href: a string containing the link’s URL. // - meta: a meta object containing non-standard meta-information about the // link. for k, v := range *l { _, isString := v.(string) _, isLink := v.(Link) if !(isString || isLink) { return fmt.Errorf( "The %s member of the links object was not a string or link object", k, ) } } return } // Link is used to represent a member of the `links` object. type Link struct { Href string `json:"href"` Meta Meta `json:"meta,omitempty"` } // Linkable is used to include document links in response data // e.g. {"self": "http://example.com/posts/1"} type Linkable interface { JSONAPILinks() *Links } // RelationshipLinkable is used to include relationship links in response data // e.g. {"related": "http://example.com/posts/1/comments"} type RelationshipLinkable interface { // JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipLinks(relation string) *Links } // Meta is used to represent a `meta` object. // http://jsonapi.org/format/#document-meta type Meta map[string]interface{} // Metable is used to include document meta in response data // e.g. {"foo": "bar"} type Metable interface { JSONAPIMeta() *Meta } // RelationshipMetable is used to include relationship meta in response data type RelationshipMetable interface { // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) JSONAPIRelationshipMeta(relation string) *Meta } jsonapi-master/runtime.go0000644000000000000000000000434613200641625014530 0ustar rootrootpackage jsonapi import ( "crypto/rand" "fmt" "io" "reflect" "time" ) type Event int const ( UnmarshalStart Event = iota UnmarshalStop MarshalStart MarshalStop ) type Runtime struct { ctx map[string]interface{} } type Events func(*Runtime, Event, string, time.Duration) var Instrumentation Events func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} } func (r *Runtime) WithValue(key string, value interface{}) *Runtime { r.ctx[key] = value return r } func (r *Runtime) Value(key string) interface{} { return r.ctx[key] } func (r *Runtime) Instrument(key string) *Runtime { return r.WithValue("instrument", key) } func (r *Runtime) shouldInstrument() bool { return Instrumentation != nil } func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error { return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { return UnmarshalPayload(reader, model) }) } func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) { r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { elems, err = UnmarshalManyPayload(reader, kind) return err }) return } func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error { return r.instrumentCall(MarshalStart, MarshalStop, func() error { return MarshalPayload(w, model) }) } func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error { if !r.shouldInstrument() { return c() } instrumentationGUID, err := newUUID() if err != nil { return err } begin := time.Now() Instrumentation(r, start, instrumentationGUID, time.Duration(0)) if err := c(); err != nil { return err } diff := time.Duration(time.Now().UnixNano() - begin.UnixNano()) Instrumentation(r, stop, instrumentationGUID, diff) return nil } // citation: http://play.golang.org/p/4FkNSiUDMg func newUUID() (string, error) { uuid := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, uuid); err != nil { return "", err } // variant bits; see section 4.1.1 uuid[8] = uuid[8]&^0xc0 | 0x80 // version 4 (pseudo-random); see section 4.1.3 uuid[6] = uuid[6]&^0xf0 | 0x40 return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil } jsonapi-master/.travis.yml0000644000000000000000000000011613200641625014616 0ustar rootrootlanguage: go go: - 1.7.x - 1.8.x - 1.9.x - tip script: script/test -v jsonapi-master/models_test.go0000644000000000000000000000770613200641625015372 0ustar rootrootpackage jsonapi import ( "fmt" "time" ) type BadModel struct { ID int `jsonapi:"primary"` } type ModelBadTypes struct { ID string `jsonapi:"primary,badtypes"` StringField string `jsonapi:"attr,string_field"` FloatField float64 `jsonapi:"attr,float_field"` TimeField time.Time `jsonapi:"attr,time_field"` TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` } type WithPointer struct { ID *uint64 `jsonapi:"primary,with-pointers"` Name *string `jsonapi:"attr,name"` IsActive *bool `jsonapi:"attr,is-active"` IntVal *int `jsonapi:"attr,int-val"` FloatVal *float32 `jsonapi:"attr,float-val"` } type Timestamp struct { ID int `jsonapi:"primary,timestamps"` Time time.Time `jsonapi:"attr,timestamp,iso8601"` Next *time.Time `jsonapi:"attr,next,iso8601"` } type Car struct { ID *string `jsonapi:"primary,cars"` Make *string `jsonapi:"attr,make,omitempty"` Model *string `jsonapi:"attr,model,omitempty"` Year *uint `jsonapi:"attr,year,omitempty"` } type Post struct { Blog ID uint64 `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` ClientID string `jsonapi:"client-id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` LatestComment *Comment `jsonapi:"relation,latest_comment"` } type Comment struct { ID int `jsonapi:"primary,comments"` ClientID string `jsonapi:"client-id"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` } type Book struct { ID uint64 `jsonapi:"primary,books"` Author string `jsonapi:"attr,author"` ISBN string `jsonapi:"attr,isbn"` Title string `jsonapi:"attr,title,omitempty"` Description *string `jsonapi:"attr,description"` Pages *uint `jsonapi:"attr,pages,omitempty"` PublishedAt time.Time Tags []string `jsonapi:"attr,tags"` } type Blog struct { ID int `jsonapi:"primary,blogs"` ClientID string `jsonapi:"client-id"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } func (b *Blog) JSONAPILinks() *Links { return &Links{ "self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID), "comments": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID), Meta: Meta{ "counts": map[string]uint{ "likes": 4, "comments": 20, }, }, }, } } func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { if relation == "posts" { return &Links{ "related": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID), Meta: Meta{ "count": len(b.Posts), }, }, } } if relation == "current_post" { return &Links{ "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), "related": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID), }, } } return nil } func (b *Blog) JSONAPIMeta() *Meta { return &Meta{ "detail": "extra details regarding the blog", } } func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { if relation == "posts" { return &Meta{ "this": map[string]interface{}{ "can": map[string]interface{}{ "go": []interface{}{ "as", "deep", map[string]interface{}{ "as": "required", }, }, }, }, } } if relation == "current_post" { return &Meta{ "detail": "extra current_post detail", } } return nil } type BadComment struct { ID uint64 `jsonapi:"primary,bad-comment"` Body string `jsonapi:"attr,body"` } func (bc *BadComment) JSONAPILinks() *Links { return &Links{ "self": []string{"invalid", "should error"}, } } jsonapi-master/README.md0000644000000000000000000003417613200641625014001 0ustar rootroot# jsonapi [![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi) [![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi) [![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi) A serializer/deserializer for JSON payloads that comply to the [JSON API - jsonapi.org](http://jsonapi.org) spec in go. ## Installation ``` go get -u github.com/google/jsonapi ``` Or, see [Alternative Installation](#alternative-installation). ## Background You are working in your Go web application and you have a struct that is organized similarly to your database schema. You need to send and receive json payloads that adhere to the JSON API spec. Once you realize that your json needed to take on this special form, you go down the path of creating more structs to be able to serialize and deserialize JSON API payloads. Then there are more models required with this additional structure. Ugh! With JSON API, you can keep your model structs as is and use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate to JSON API how you want your response built or your request deserialized. What about your relationships? JSON API supports relationships out of the box and will even put them in your response into an `included` side-loaded slice--that contains associated records. ## Introduction JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField) tags to annotate the structs fields that you already have and use in your app and then reads and writes [JSON API](http://jsonapi.org) output based on the instructions you give the library in your JSON API tags. Let's take an example. In your app, you most likely have structs that look similar to these: ```go type Blog struct { ID int `json:"id"` Title string `json:"title"` Posts []*Post `json:"posts"` CurrentPost *Post `json:"current_post"` CurrentPostId int `json:"current_post_id"` CreatedAt time.Time `json:"created_at"` ViewCount int `json:"view_count"` } type Post struct { ID int `json:"id"` BlogID int `json:"blog_id"` Title string `json:"title"` Body string `json:"body"` Comments []*Comment `json:"comments"` } type Comment struct { Id int `json:"id"` PostID int `json:"post_id"` Body string `json:"body"` Likes uint `json:"likes_count,omitempty"` } ``` These structs may or may not resemble the layout of your database. But these are the ones that you want to use right? You wouldn't want to use structs like those that JSON API sends because it is difficult to get at all of your data easily. ## Example App [examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go) This runnable file demonstrates the implementation of a create, a show, and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It outputs some example requests and responses as well as serialized examples of the source/target structs to json. That is to say, I show you that the library has successfully taken your JSON API request and turned it into your struct types. To run, * Make sure you have go installed * Create the following directories or similar: `~/go` * Set `GOPATH` to `PWD` in your shell session, `export GOPATH=$PWD` * `go get github.com/google/jsonapi`. (Append `-u` after `get` if you are updating.) * `go run $GOPATH/src/github.com/google/jsonapi/examples/app.go` or `cd $GOPATH/src/github.com/google/jsonapi/examples && go run app.go` ## `jsonapi` Tag Reference ### Example The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag) tells this library how to marshal and unmarshal your structs into JSON API payloads and your JSON API payloads to structs, respectively. Then Use JSON API's Marshal and Unmarshal methods to construct and read your responses and replies. Here's an example of the structs above using JSON API tags: ```go type Blog struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } type Post struct { ID int `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` } type Comment struct { ID int `jsonapi:"primary,comments"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` Likes uint `jsonapi:"attr,likes-count,omitempty"` } ``` ### Permitted Tag Values #### `primary` ``` `jsonapi:"primary,"` ``` This indicates this is the primary key field for this struct type. Tag value arguments are comma separated. The first argument must be, `primary`, and the second must be the name that should appear in the `type`\* field for all data objects that represent this type of model. \* According the [JSON API](http://jsonapi.org) spec, the plural record types are shown in the examples, but not required. #### `attr` ``` `jsonapi:"attr,,"` ``` These fields' values will end up in the `attributes`hash for a record. The first argument must be, `attr`, and the second should be the name for the key to display in the `attributes` hash for that record. The optional third argument is `omitempty` - if it is present the field will not be present in the `"attributes"` if the field's value is equivalent to the field types empty value (ie if the `count` field is of type `int`, `omitempty` will omit the field when `count` has a value of `0`). Lastly, the spec indicates that `attributes` key names should be dasherized for multiple word field names. #### `relation` ``` `jsonapi:"relation,,"` ``` Relations are struct fields that represent a one-to-one or one-to-many relationship with other structs. JSON API will traverse the graph of relationships and marshal or unmarshal records. The first argument must be, `relation`, and the second should be the name of the relationship, used as the key in the `relationships` hash for the record. The optional third argument is `omitempty` - if present will prevent non existent to-one and to-many from being serialized. ## Methods Reference **All `Marshal` and `Unmarshal` methods expect pointers to struct instance or slices of the same contained with the `interface{}`s** Now you have your structs prepared to be seralized or materialized, What about the rest? ### Create Record Example You can Unmarshal a JSON API payload using [jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload). It reads from an [io.Reader](https://golang.org/pkg/io/#Reader) containing a JSON API payload for one record (but can have related records). Then, it materializes a struct that you created and passed in (using new or &). Again, the method supports single records only, at the top level, in request payloads at the moment. Bulk creates and updates are not supported yet. After saving your record, you can use, [MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload), to write the JSON API response to an [io.Writer](https://golang.org/pkg/io/#Writer). #### `UnmarshalPayload` ```go UnmarshalPayload(in io.Reader, model interface{}) ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload) #### `MarshalPayload` ```go MarshalPayload(w io.Writer, models interface{}) error ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload) Writes a JSON API response, with related records sideloaded, into an `included` array. This method encodes a response for either a single record or many records. ##### Handler Example Code ```go func CreateBlog(w http.ResponseWriter, r *http.Request) { blog := new(Blog) if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // ...save your blog... w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusCreated) if err := jsonapi.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } ``` ### Create Records Example #### `UnmarshalManyPayload` ```go UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload) Takes an `io.Reader` and a `reflect.Type` representing the uniform type contained within the `"data"` JSON API member. ##### Handler Example Code ```go func CreateBlogs(w http.ResponseWriter, r *http.Request) { // ...create many blogs at once blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog))) if err != nil { t.Fatal(err) } for _, blog := range blogs { b, ok := blog.(*Blog) // ...save each of your blogs } w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusCreated) if err := jsonapi.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } ``` ### Links If you need to include [link objects](http://jsonapi.org/format/#document-links) along with response data, implement the `Linkable` interface for document-links, and `RelationshipLinkable` for relationship links: ```go func (post Post) JSONAPILinks() *Links { return &Links{ "self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID), "comments": Link{ Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID), Meta: map[string]interface{}{ "counts": map[string]uint{ "likes": 4, }, }, }, } } // Invoked for each relationship defined on the Post struct when marshaled func (post Post) JSONAPIRelationshipLinks(relation string) *Links { if relation == "comments" { return &Links{ "related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID), } } return nil } ``` ### Meta If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta: ```go func (post Post) JSONAPIMeta() *Meta { return &Meta{ "details": "sample details here", } } // Invoked for each relationship defined on the Post struct when marshaled func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { if relation == "comments" { return &Meta{ "this": map[string]interface{}{ "can": map[string]interface{}{ "go": []interface{}{ "as", "deep", map[string]interface{}{ "as": "required", }, }, }, }, } } return nil } ``` ### Errors This package also implements support for JSON API compatible `errors` payloads using the following types. #### `MarshalErrors` ```go MarshalErrors(w io.Writer, errs []*ErrorObject) error ``` Writes a JSON API response using the given `[]error`. #### `ErrorsPayload` ```go type ErrorsPayload struct { Errors []*ErrorObject `json:"errors"` } ``` ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. #### `ErrorObject` ```go type ErrorObject struct { ... } // Error implements the `Error` interface. func (e *ErrorObject) Error() string { return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) } ``` ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. ##### Errors Example Code ```go // An error has come up in your code, so set an appropriate status, and serialize the error. if err := validate(&myStructToValidate); err != nil { context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status. jsonapi.MarshalErrors(w, []*ErrorObject{{ Title: "Validation Error", Detail: "Given request body was invalid.", Status: "400", Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"}, }}) return } ``` ## Testing ### `MarshalOnePayloadEmbedded` ```go MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error ``` Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded) This method is not strictly meant to for use in implementation code, although feel free. It was mainly created 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 serialized inline with the data. However, in tests, you may want to construct payloads to post to create methods that are embedded to most closely model the payloads that will be produced by the client. This method aims to enable that. ### Example ```go out := bytes.NewBuffer(nil) // testModel returns a pointer to a Blog jsonapi.MarshalOnePayloadEmbedded(out, testModel()) h := new(BlogsHandler) w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodPost, "/blogs", out) h.CreateBlog(w, r) blog := new(Blog) jsonapi.UnmarshalPayload(w.Body, blog) // ... assert stuff about blog here ... ``` ## Alternative Installation I use git subtrees to manage dependencies rather than `go get` so that the src is committed to my repo. ``` git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master ``` To update, ``` git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master ``` This assumes that I have my repo structured with a `src` dir containing a collection of packages and `GOPATH` is set to the root folder--containing `src`. ## Contributing Fork, Change, Pull Request *with tests*. jsonapi-master/doc.go0000644000000000000000000000553313200641625013611 0ustar rootroot/* Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads. You can keep your model structs as is and use struct field tags to indicate to jsonapi how you want your response built or your request deserialzied. What about my relationships? jsonapi supports relationships out of the box and will even side load them in your response into an "included" array--that contains associated objects. jsonapi uses StructField tags to annotate the structs fields that you already have and use in your app and then reads and writes jsonapi.org output based on the instructions you give the library in your jsonapi tags. Example structs using a Blog > Post > Comment structure, type Blog struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } type Post struct { ID int `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` } type Comment struct { ID int `jsonapi:"primary,comments"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` } jsonapi Tag Reference Value, primary: "primary," This indicates that this is the primary key field for this struct type. Tag value arguments are comma separated. The first argument must be, "primary", and the second must be the name that should appear in the "type" field for all data objects that represent this type of model. Value, attr: "attr,[,]" These fields' values should end up in the "attribute" hash for a record. The first argument must be, "attr', and the second should be the name for the key to display in the the "attributes" hash for that record. The following extra arguments are also supported: "omitempty": excludes the fields value from the "attribute" hash. "iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value. Value, relation: "relation," Relations are struct fields that represent a one-to-one or one-to-many to other structs. jsonapi will traverse the graph of relationships and marshal or unmarshal records. The first argument must be, "relation", and the second should be the name of the relationship, used as the key in the "relationships" hash for the record. Use the methods below to Marshal and Unmarshal jsonapi.org json payloads. Visit the readme at https://github.com/google/jsonapi */ package jsonapi jsonapi-master/examples/0000755000000000000000000000000013200641625014325 5ustar rootrootjsonapi-master/examples/handler.go0000644000000000000000000000563013200641625016275 0ustar rootrootpackage main import ( "net/http" "strconv" "github.com/google/jsonapi" ) const ( headerAccept = "Accept" headerContentType = "Content-Type" ) // ExampleHandler is the handler we are using to demonstrate building an HTTP // server with the jsonapi library. type ExampleHandler struct{} func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get(headerAccept) != jsonapi.MediaType { http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) } var methodHandler http.HandlerFunc switch r.Method { case http.MethodPost: methodHandler = h.createBlog case http.MethodPut: methodHandler = h.echoBlogs case http.MethodGet: if r.FormValue("id") != "" { methodHandler = h.showBlog } else { methodHandler = h.listBlogs } default: http.Error(w, "Not Found", http.StatusNotFound) return } methodHandler(w, r) } func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create") blog := new(Blog) if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // ...do stuff with your blog... w.WriteHeader(http.StatusCreated) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") // ...fetch your blogs, filter, offset, limit, etc... // but, for now blogs := fixtureBlogsList() w.WriteHeader(http.StatusOK) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) showBlog(w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") // ...fetch your blog... intID, err := strconv.Atoi(id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show") // but, for now blog := fixtureBlogCreate(intID) w.WriteHeader(http.StatusOK) w.Header().Set(headerContentType, jsonapi.MediaType) if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func (h *ExampleHandler) listBlogs(w http.ResponseWriter, r *http.Request) { jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") // ...fetch your blogs, filter, offset, limit, etc... // but, for now blogs := fixtureBlogsList() w.Header().Set("Content-Type", jsonapi.MediaType) w.WriteHeader(http.StatusOK) if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } jsonapi-master/examples/app.go0000644000000000000000000000735513200641625015446 0ustar rootrootpackage main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "time" "github.com/google/jsonapi" ) func main() { jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) { metricPrefix := r.Value("instrument").(string) if eventType == jsonapi.UnmarshalStart { fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now()) } if eventType == jsonapi.UnmarshalStop { fmt.Printf("%s: id, %s, stopped at, %v , and took %v to unmarshal payload\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now(), dur) } if eventType == jsonapi.MarshalStart { fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now()) } if eventType == jsonapi.MarshalStop { fmt.Printf("%s: id, %s, stopped at, %v , and took %v to marshal payload\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now(), dur) } } exampleHandler := &ExampleHandler{} http.HandleFunc("/blogs", exampleHandler.ServeHTTP) exerciseHandler() } func exerciseHandler() { // list req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) req.Header.Set(headerAccept, jsonapi.MediaType) w := httptest.NewRecorder() fmt.Println("============ start list ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop list ===========") jsonReply, _ := ioutil.ReadAll(w.Body) fmt.Println("============ jsonapi response from list ===========") fmt.Println(string(jsonReply)) fmt.Println("============== end raw jsonapi from list =============") // show req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start show ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop show ===========") jsonReply, _ = ioutil.ReadAll(w.Body) fmt.Println("============ jsonapi response from show ===========") fmt.Println(string(jsonReply)) fmt.Println("============== end raw jsonapi from show =============") // create blog := fixtureBlogCreate(1) in := bytes.NewBuffer(nil) jsonapi.MarshalOnePayloadEmbedded(in, blog) req, _ = http.NewRequest(http.MethodPost, "/blogs", in) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start create ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop create ===========") buf := bytes.NewBuffer(nil) io.Copy(buf, w.Body) fmt.Println("============ jsonapi response from create ===========") fmt.Println(buf.String()) fmt.Println("============== end raw jsonapi response =============") // echo blogs := []interface{}{ fixtureBlogCreate(1), fixtureBlogCreate(2), fixtureBlogCreate(3), } in = bytes.NewBuffer(nil) jsonapi.MarshalPayload(in, blogs) req, _ = http.NewRequest(http.MethodPut, "/blogs", in) req.Header.Set(headerAccept, jsonapi.MediaType) w = httptest.NewRecorder() fmt.Println("============ start echo ===========") http.DefaultServeMux.ServeHTTP(w, req) fmt.Println("============ stop echo ===========") buf = bytes.NewBuffer(nil) io.Copy(buf, w.Body) fmt.Println("============ jsonapi response from create ===========") fmt.Println(buf.String()) fmt.Println("============== end raw jsonapi response =============") responseBlog := new(Blog) jsonapi.UnmarshalPayload(buf, responseBlog) out := bytes.NewBuffer(nil) json.NewEncoder(out).Encode(responseBlog) fmt.Println("================ Viola! Converted back our Blog struct =================") fmt.Println(string(out.Bytes())) fmt.Println("================ end marshal materialized Blog struct =================") } jsonapi-master/examples/fixtures.go0000644000000000000000000000165313200641625016532 0ustar rootrootpackage main import "time" func fixtureBlogCreate(i int) *Blog { return &Blog{ ID: 1 * i, Title: "Title 1", CreatedAt: time.Now(), Posts: []*Post{ { ID: 1 * i, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 2 * i, Body: "bar", }, }, }, { ID: 2 * i, Title: "Fuubar", Body: "Bas", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 3 * i, Body: "bas", }, }, }, }, CurrentPost: &Post{ ID: 1 * i, Title: "Foo", Body: "Bar", Comments: []*Comment{ { ID: 1 * i, Body: "foo", }, { ID: 2 * i, Body: "bar", }, }, }, } } func fixtureBlogsList() (blogs []interface{}) { for i := 0; i < 10; i++ { blogs = append(blogs, fixtureBlogCreate(i)) } return blogs } jsonapi-master/examples/handler_test.go0000644000000000000000000000526313200641625017336 0ustar rootrootpackage main import ( "bytes" "net/http" "net/http/httptest" "testing" "github.com/google/jsonapi" ) func TestExampleHandler_post(t *testing.T) { blog := fixtureBlogCreate(1) requestBody := bytes.NewBuffer(nil) jsonapi.MarshalOnePayloadEmbedded(requestBody, blog) r, err := http.NewRequest(http.MethodPost, "/blogs?id=1", requestBody) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusCreated, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_put(t *testing.T) { blogs := []interface{}{ fixtureBlogCreate(1), fixtureBlogCreate(2), fixtureBlogCreate(3), } requestBody := bytes.NewBuffer(nil) jsonapi.MarshalPayload(requestBody, blogs) r, err := http.NewRequest(http.MethodPut, "/blogs", requestBody) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_get_show(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/blogs?id=1", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestExampleHandler_get_list(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/blogs", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if e, a := http.StatusOK, rr.Code; e != a { t.Fatalf("Expected a status of %d, got %d", e, a) } } func TestHttpErrorWhenHeaderDoesNotMatch(t *testing.T) { r, err := http.NewRequest(http.MethodGet, "/blogs", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, "application/xml") rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if rr.Code != http.StatusUnsupportedMediaType { t.Fatal("expected Unsupported Media Type staus error") } } func TestHttpErrorWhenMethodDoesNotMatch(t *testing.T) { r, err := http.NewRequest(http.MethodPatch, "/blogs", nil) if err != nil { t.Fatal(err) } r.Header.Set(headerAccept, jsonapi.MediaType) rr := httptest.NewRecorder() handler := &ExampleHandler{} handler.ServeHTTP(rr, r) if rr.Code != http.StatusNotFound { t.Fatal("expected HTTP Status Not Found status error") } } jsonapi-master/examples/models.go0000644000000000000000000000427513200641625016147 0ustar rootrootpackage main import ( "fmt" "time" "github.com/google/jsonapi" ) // Blog is a model representing a blog site type Blog struct { ID int `jsonapi:"primary,blogs"` Title string `jsonapi:"attr,title"` Posts []*Post `jsonapi:"relation,posts"` CurrentPost *Post `jsonapi:"relation,current_post"` CurrentPostID int `jsonapi:"attr,current_post_id"` CreatedAt time.Time `jsonapi:"attr,created_at"` ViewCount int `jsonapi:"attr,view_count"` } // Post is a model representing a post on a blog type Post struct { ID int `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` Title string `jsonapi:"attr,title"` Body string `jsonapi:"attr,body"` Comments []*Comment `jsonapi:"relation,comments"` } // Comment is a model representing a user submitted comment type Comment struct { ID int `jsonapi:"primary,comments"` PostID int `jsonapi:"attr,post_id"` Body string `jsonapi:"attr,body"` } // JSONAPILinks implements the Linkable interface for a blog func (blog Blog) JSONAPILinks() *jsonapi.Links { return &jsonapi.Links{ "self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), } } // JSONAPIRelationshipLinks implements the RelationshipLinkable interface for a blog func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links { if relation == "posts" { return &jsonapi.Links{ "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), } } if relation == "current_post" { return &jsonapi.Links{ "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), } } return nil } // JSONAPIMeta implements the Metable interface for a blog func (blog Blog) JSONAPIMeta() *jsonapi.Meta { return &jsonapi.Meta{ "detail": "extra details regarding the blog", } } // JSONAPIRelationshipMeta implements the RelationshipMetable interface for a blog func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta { if relation == "posts" { return &jsonapi.Meta{ "detail": "posts meta information", } } if relation == "current_post" { return &jsonapi.Meta{ "detail": "current post meta information", } } return nil } jsonapi-master/constants.go0000644000000000000000000000366713200641625015066 0ustar rootrootpackage jsonapi const ( // StructTag annotation strings annotationJSONAPI = "jsonapi" annotationPrimary = "primary" annotationClientID = "client-id" annotationAttribute = "attr" annotationRelation = "relation" annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationSeperator = "," iso8601TimeFormat = "2006-01-02T15:04:05Z" // MediaType is the identifier for the JSON API media type // // see http://jsonapi.org/format/#document-structure MediaType = "application/vnd.api+json" // Pagination Constants // // http://jsonapi.org/format/#fetching-pagination // KeyFirstPage is the key to the links object whose value contains a link to // the first page of data KeyFirstPage = "first" // KeyLastPage is the key to the links object whose value contains a link to // the last page of data KeyLastPage = "last" // KeyPreviousPage is the key to the links object whose value contains a link // to the previous page of data KeyPreviousPage = "prev" // KeyNextPage is the key to the links object whose value contains a link to // the next page of data KeyNextPage = "next" // QueryParamPageNumber is a JSON API query parameter used in a page based // pagination strategy in conjunction with QueryParamPageSize QueryParamPageNumber = "page[number]" // QueryParamPageSize is a JSON API query parameter used in a page based // pagination strategy in conjunction with QueryParamPageNumber QueryParamPageSize = "page[size]" // QueryParamPageOffset is a JSON API query parameter used in an offset based // pagination strategy in conjunction with QueryParamPageLimit QueryParamPageOffset = "page[offset]" // QueryParamPageLimit is a JSON API query parameter used in an offset based // pagination strategy in conjunction with QueryParamPageOffset QueryParamPageLimit = "page[limit]" // QueryParamPageCursor is a JSON API query parameter used with a cursor-based // strategy QueryParamPageCursor = "page[cursor]" )