From a71017c0d62d9235abfed3c398d36314c3dc9733 Mon Sep 17 00:00:00 2001 From: Esteve Badia Date: Fri, 19 Nov 2021 11:57:19 +0100 Subject: [PATCH 1/7] meta jsonapi tag --- constants.go | 1 + request.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/constants.go b/constants.go index 35bbe054..0fc42e41 100644 --- a/constants.go +++ b/constants.go @@ -7,6 +7,7 @@ const ( annotationClientID = "client-id" annotationAttribute = "attr" annotationRelation = "relation" + annotationMeta = "meta" annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationRFC3339 = "rfc3339" diff --git a/request.go b/request.go index f665857f..9cc55fe0 100644 --- a/request.go +++ b/request.go @@ -326,7 +326,28 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) fieldValue.Set(m) } + } else if annotation == annotationMeta { + metas := *data.Meta + // continue if no meta avaiable. + if len(metas) == 0 { + continue + } + meta := metas[args[1]] + + // continue if the meta value was not included in the request + if meta == nil { + continue + } + + structField := fieldType + value, err := unmarshalAttribute(meta, args, structField, fieldValue) + if err != nil { + er = err + break + } + + assign(fieldValue, value) } else { er = fmt.Errorf(unsupportedStructTagMsg, annotation) } From 91a4f80f1456f6ac9d1517037ee6b072c1b7ab14 Mon Sep 17 00:00:00 2001 From: Esteve Badia Date: Fri, 19 Nov 2021 11:58:48 +0100 Subject: [PATCH 2/7] change declared module path --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d1fb59d6..d13bd98c 100644 --- a/go.mod +++ b/go.mod @@ -1 +1 @@ -module github.com/google/jsonapi \ No newline at end of file +module github.com/komunitin/jsonapi \ No newline at end of file From 9411d02366cca258a2039a81e8e490bcd8e6ad86 Mon Sep 17 00:00:00 2001 From: Esteve Badia Date: Fri, 19 Nov 2021 12:04:54 +0100 Subject: [PATCH 3/7] update path --- examples/app.go | 2 +- examples/handler.go | 2 +- examples/handler_test.go | 2 +- examples/models.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/app.go b/examples/app.go index 2b29e0d8..00459ef8 100644 --- a/examples/app.go +++ b/examples/app.go @@ -10,7 +10,7 @@ import ( "net/http/httptest" "time" - "github.com/google/jsonapi" + "github.com/komunitin/jsonapi" ) func main() { diff --git a/examples/handler.go b/examples/handler.go index 77894c79..a172cfaf 100644 --- a/examples/handler.go +++ b/examples/handler.go @@ -4,7 +4,7 @@ import ( "net/http" "strconv" - "github.com/google/jsonapi" + "github.com/komunitin/jsonapi" ) const ( diff --git a/examples/handler_test.go b/examples/handler_test.go index 34c0bc5d..354c550a 100644 --- a/examples/handler_test.go +++ b/examples/handler_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/google/jsonapi" + "github.com/komunitin/jsonapi" ) func TestExampleHandler_post(t *testing.T) { diff --git a/examples/models.go b/examples/models.go index 080790e7..4af3e2fa 100644 --- a/examples/models.go +++ b/examples/models.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/google/jsonapi" + "github.com/komunitin/jsonapi" ) // Blog is a model representing a blog site From 3ec5fff38921df5078782744c2a8bcdf396ec838 Mon Sep 17 00:00:00 2001 From: Esteve Badia Date: Wed, 29 Dec 2021 16:37:11 +0100 Subject: [PATCH 4/7] Fix meta feature for writing, fixed tests --- models_test.go | 5 +++++ request_test.go | 25 +++++++++++++++++++++++++ response.go | 10 ++++++++++ response_test.go | 16 ++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/models_test.go b/models_test.go index f552d4fe..f0e14b10 100644 --- a/models_test.go +++ b/models_test.go @@ -195,3 +195,8 @@ type CustomAttributeTypes struct { Float CustomFloatType `jsonapi:"attr,float"` String CustomStringType `jsonapi:"attr,string"` } + +type PostWithMeta struct { + Post + MetaField string `jsonapi:"meta,metafield"` +} diff --git a/request_test.go b/request_test.go index 300c7de3..ef811656 100644 --- a/request_test.go +++ b/request_test.go @@ -1416,3 +1416,28 @@ func TestUnmarshalNestedStructSlice(t *testing.T) { out.Teams[0].Members[0].Firstname) } } + +func TestUnmarshalMeta(t *testing.T) { + sample := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "posts", + "id": "1", + "attributes": map[string]interface{}{ + "body": "Hello", + "title": "World", + }, + "meta": map[string]interface{}{ + "metafield": "meta!", + }, + }, + } + data, _ := json.Marshal(sample) + post := new(PostWithMeta) + err := UnmarshalPayload(bytes.NewReader(data), post) + if err != nil { + t.Fatal(err) + } + if post.MetaField != "meta!" { + t.Fatalf("Meta data not unmarshalled: expected `meta!` but got `%s`", post.MetaField) + } +} diff --git a/response.go b/response.go index b44e4e97..62aae6ff 100644 --- a/response.go +++ b/response.go @@ -448,6 +448,16 @@ func visitModelNode(model interface{}, included *map[string]*Node, } } + } else if annotation == annotationMeta { + if node.Meta == nil { + node.Meta = &Meta{} + } + strAttr, ok := fieldValue.Interface().(string) + if ok { + (*node.Meta)[args[1]] = strAttr + } else { + (*node.Meta)[args[1]] = fieldValue.Interface() + } } else { er = ErrBadJSONAPIStructTag break diff --git a/response_test.go b/response_test.go index b1d5967a..9bd478c4 100644 --- a/response_test.go +++ b/response_test.go @@ -1023,3 +1023,19 @@ func testBlog() *Blog { }, } } + +func TestMarshalMeta(t *testing.T) { + post := &PostWithMeta{ + Post: Post{ + ID: 1, + Title: "Title", + Body: "Body", + }, + MetaField: "meta!", + } + out := new(bytes.Buffer) + err := MarshalPayload(out, post) + if err != nil { + t.Fatal(err) + } +} From 2c4c77393dc4d75bb73660215043f7541ae45ad1 Mon Sep 17 00:00:00 2001 From: Esteve Badia Date: Sun, 31 Mar 2024 12:21:07 +0200 Subject: [PATCH 5/7] Add support for unmarshaling links & extras --- node.go | 5 +++++ request.go | 41 ++++++++++++++++++++++------------------- request_test.go | 13 ++++++++++++- runtime.go | 4 ++-- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/node.go b/node.go index a58488c8..3a872702 100644 --- a/node.go +++ b/node.go @@ -29,6 +29,11 @@ type ManyPayload struct { Meta *Meta `json:"meta,omitempty"` } +type PayloadExtras struct { + Links *Links + Meta *Meta +} + func (p *ManyPayload) clearIncluded() { p.Included = []*Node{} } diff --git a/request.go b/request.go index 9cc55fe0..9223d614 100644 --- a/request.go +++ b/request.go @@ -70,24 +70,23 @@ func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField refl // 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) +// 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 -// } +// if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { +// http.Error(w, err.Error(), 500) +// return +// } // -// // ...do stuff with your blog... +// // ...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) -// } -// } +// 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. // @@ -113,15 +112,16 @@ func UnmarshalPayload(in io.Reader, model interface{}) error { // 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) { +func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, *PayloadExtras, error) { payload := new(ManyPayload) if err := json.NewDecoder(in).Decode(payload); err != nil { - return nil, err + return nil, nil, err } models := []interface{}{} // will be populated from the "data" - includedMap := map[string]*Node{} // will be populate from the "included" + includedMap := map[string]*Node{} // will be populated from the "included" + extras := new(PayloadExtras) // will be populated from the "links" and "meta" if payload.Included != nil { for _, included := range payload.Included { @@ -134,12 +134,15 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { model := reflect.New(t.Elem()) err := unmarshalNode(data, model, &includedMap) if err != nil { - return nil, err + return nil, nil, err } models = append(models, model.Interface()) } - return models, nil + extras.Links = payload.Links + extras.Meta = payload.Meta + + return models, extras, nil } func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { diff --git a/request_test.go b/request_test.go index ef811656..e740703b 100644 --- a/request_test.go +++ b/request_test.go @@ -776,6 +776,9 @@ func TestUnmarshalManyPayload(t *testing.T) { }, }, }, + "links": map[string]interface{}{ + "self": "http://example.com/posts", + }, } data, err := json.Marshal(sample) @@ -784,7 +787,7 @@ func TestUnmarshalManyPayload(t *testing.T) { } in := bytes.NewReader(data) - posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) + posts, extras, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) if err != nil { t.Fatal(err) } @@ -799,6 +802,14 @@ func TestUnmarshalManyPayload(t *testing.T) { t.Fatal("Was expecting a Post") } } + + if extras.Meta != nil { + t.Fatal("Was not expecting a meta object") + } + + if (*extras.Links)["self"] != "http://example.com/posts" { + t.Fatal("Incorrect self link") + } } func TestManyPayload_withLinks(t *testing.T) { diff --git a/runtime.go b/runtime.go index db2d9f2f..333fc5fb 100644 --- a/runtime.go +++ b/runtime.go @@ -76,9 +76,9 @@ func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error { } // UnmarshalManyPayload has docs in request.go for UnmarshalManyPayload. -func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) { +func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, extras *PayloadExtras, err error) { r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { - elems, err = UnmarshalManyPayload(reader, kind) + elems, extras, err = UnmarshalManyPayload(reader, kind) return err }) From a89946cc92850f5c69745761e65fff63ae781340 Mon Sep 17 00:00:00 2001 From: Esteve Date: Mon, 27 Jan 2025 11:07:21 +0100 Subject: [PATCH 6/7] Add "link" annotation --- README.md | 5 ++++- constants.go | 1 + models_test.go | 5 +++++ request.go | 20 ++++++++++++++++++++ request_test.go | 25 +++++++++++++++++++++++++ response.go | 25 +++++++++++++++++-------- 6 files changed, 72 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8dfb9438..d5c8f0ed 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ A serializer/deserializer for JSON payloads that comply to the [JSON API - jsonapi.org](http://jsonapi.org) spec in go. - +## This fork +This is a fork of the original [google/jsonapi](https://github.com/google/jsonapi) repository. Additional features: +- Support for `meta` and `link` annotation for resources +- Support for getting response links. ## Installation diff --git a/constants.go b/constants.go index 0fc42e41..b81b1230 100644 --- a/constants.go +++ b/constants.go @@ -8,6 +8,7 @@ const ( annotationAttribute = "attr" annotationRelation = "relation" annotationMeta = "meta" + annotationLink = "link" annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationRFC3339 = "rfc3339" diff --git a/models_test.go b/models_test.go index f0e14b10..79c2cf2e 100644 --- a/models_test.go +++ b/models_test.go @@ -200,3 +200,8 @@ type PostWithMeta struct { Post MetaField string `jsonapi:"meta,metafield"` } + +type PostWithLink struct { + Post + SelfLink string `jsonapi:"link,self"` +} diff --git a/request.go b/request.go index 9223d614..45397633 100644 --- a/request.go +++ b/request.go @@ -350,6 +350,26 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) break } + assign(fieldValue, value) + } else if annotation == annotationLink { + links := *data.Links + // continue if no links avaiable. + if len(links) == 0 { + continue + } + link := links[args[1]] + + if link == nil { + continue + } + + structField := fieldType + value, err := unmarshalAttribute(link, args, structField, fieldValue) + if err != nil { + er = err + break + } + assign(fieldValue, value) } else { er = fmt.Errorf(unsupportedStructTagMsg, annotation) diff --git a/request_test.go b/request_test.go index e740703b..2109c300 100644 --- a/request_test.go +++ b/request_test.go @@ -1452,3 +1452,28 @@ func TestUnmarshalMeta(t *testing.T) { t.Fatalf("Meta data not unmarshalled: expected `meta!` but got `%s`", post.MetaField) } } + +func TestUnmarshalLink(t *testing.T) { + sample := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "posts", + "id": "1", + "attributes": map[string]interface{}{ + "body": "Hello", + "title": "World", + }, + "links": map[string]interface{}{ + "self": "http://example.com/posts/1", + }, + }, + } + data, _ := json.Marshal(sample) + post := new(PostWithLink) + err := UnmarshalPayload(bytes.NewReader(data), post) + if err != nil { + t.Fatal(err) + } + if post.SelfLink != "http://example.com/posts/1" { + t.Fatalf("Link data not unmarshalled: expected `http://example.com/posts/1` but got `%s`", post.SelfLink) + } +} diff --git a/response.go b/response.go index 62aae6ff..3c5d356c 100644 --- a/response.go +++ b/response.go @@ -51,17 +51,16 @@ var ( // 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{} +// func ListBlogs(w http.ResponseWriter, r *http.Request) { +// blogs := []*Blog{} // -// w.Header().Set("Content-Type", jsonapi.MediaType) -// w.WriteHeader(http.StatusOK) +// 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) +// 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 { @@ -458,6 +457,16 @@ func visitModelNode(model interface{}, included *map[string]*Node, } else { (*node.Meta)[args[1]] = fieldValue.Interface() } + } else if annotation == annotationLink { + if node.Links == nil { + node.Links = &Links{} + } + strAttr, ok := fieldValue.Interface().(string) + if ok { + (*node.Links)[args[1]] = strAttr + } else { + (*node.Links)[args[1]] = fieldValue.Interface() + } } else { er = ErrBadJSONAPIStructTag break From cc470520194efe34208c6d2c98095a00f9af94e5 Mon Sep 17 00:00:00 2001 From: Esteve Date: Mon, 3 Feb 2025 10:21:34 +0100 Subject: [PATCH 7/7] Fix nil pointer dereference in unmarshalNode for Meta and Links --- request.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/request.go b/request.go index 45397633..84ce67aa 100644 --- a/request.go +++ b/request.go @@ -330,8 +330,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } } else if annotation == annotationMeta { - metas := *data.Meta // continue if no meta avaiable. + if data.Meta == nil { + continue + } + metas := *data.Meta if len(metas) == 0 { continue } @@ -352,8 +355,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) assign(fieldValue, value) } else if annotation == annotationLink { - links := *data.Links // continue if no links avaiable. + if data.Links == nil { + continue + } + links := *data.Links if len(links) == 0 { continue }