pkg/jsonapi/jsonapi.go
Rama master · réplica de lectura del repositorio original.
// Package jsonapi is for using the JSON-API format: parsing, serialization,
// checking the content-type, etc.
package jsonapi
import (
"compress/gzip"
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/labstack/echo/v4"
)
// ContentType is the official mime-type for JSON-API
const ContentType = "application/vnd.api+json"
// Document is JSON-API document, identified by the mediatype
// application/vnd.api+json
// See http://jsonapi.org/format/#document-structure
type Document struct {
Data *json.RawMessage `json:"data,omitempty"`
Errors ErrorList `json:"errors,omitempty"`
Links *LinksList `json:"links,omitempty"`
Meta *Meta `json:"meta,omitempty"`
Included []interface{} `json:"included,omitempty"`
}
// WriteData can be called to write an answer with a JSON-API document
// containing a single object as data into an io.Writer.
func WriteData(w io.Writer, o Object, links *LinksList) error {
var included []interface{}
if inc := o.Included(); inc != nil {
included = make([]interface{}, len(inc))
for i, o := range inc {
data, err := MarshalObject(o)
if err != nil {
return err
}
included[i] = &data
}
}
data, err := MarshalObject(o)
if err != nil {
return err
}
doc := Document{
Data: &data,
Links: links,
Included: included,
}
return json.NewEncoder(w).Encode(doc)
}
// Data can be called to send an answer with a JSON-API document containing a
// single object as data
func Data(c echo.Context, statusCode int, o Object, links *LinksList) error {
resp := c.Response()
w := compressedWriter(c.Request(), resp)
defer func() {
_ = w.Close()
}()
resp.WriteHeader(statusCode)
return WriteData(w, o, links)
}
// DataList can be called to send an multiple-value answer with a
// JSON-API document contains multiple objects.
func DataList(c echo.Context, statusCode int, objs []Object, links *LinksList) error {
count := len(objs)
meta := Meta{Count: &count}
return DataListWithMeta(c, statusCode, meta, objs, links)
}
// DataListWithMeta can be called to send a list of Objects with meta like a
// count, useful to indicate total number of results with pagination.
func DataListWithMeta(c echo.Context, statusCode int, meta Meta, objs []Object, links *LinksList) error {
objsMarshaled := make([]json.RawMessage, len(objs))
for i, o := range objs {
j, err := MarshalObject(o)
if err != nil {
return InternalServerError(err)
}
objsMarshaled[i] = j
}
data, err := json.Marshal(objsMarshaled)
if err != nil {
return InternalServerError(err)
}
doc := Document{
Data: (*json.RawMessage)(&data),
Meta: &meta,
Links: links,
}
resp := c.Response()
w := compressedWriter(c.Request(), resp)
defer func() {
_ = w.Close()
}()
resp.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(doc)
}
func compressedWriter(req *http.Request, resp *echo.Response) io.WriteCloser {
headers := resp.Header()
headers.Set(echo.HeaderContentType, ContentType)
headers.Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if !acceptGzipEncoding(req) {
return &nopCloser{resp}
}
headers.Set(echo.HeaderContentEncoding, "gzip")
return gzip.NewWriter(resp)
}
// nopCloser adds a Close method to a io.Writer (io.NopCloser does that for
// io.Reader).
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func acceptGzipEncoding(req *http.Request) bool {
return strings.Contains(req.Header.Get(echo.HeaderAcceptEncoding), "gzip")
}
// DataRelations can be called to send a Relations page,
// a list of ResourceIdentifier
func DataRelations(c echo.Context, statusCode int, refs []couchdb.DocReference, meta *Meta, links *LinksList, included []Object) error {
data, err := json.Marshal(refs)
if err != nil {
return InternalServerError(err)
}
doc := Document{
Data: (*json.RawMessage)(&data),
Meta: meta,
Links: links,
}
if included != nil {
includedMarshaled := make([]interface{}, len(included))
for i, o := range included {
j, err := MarshalObject(o)
if err != nil {
return InternalServerError(err)
}
includedMarshaled[i] = &j
}
doc.Included = includedMarshaled
}
resp := c.Response()
resp.Header().Set(echo.HeaderContentType, ContentType)
resp.WriteHeader(statusCode)
return json.NewEncoder(resp).Encode(doc)
}
// DataError can be called to send an error answer with a JSON-API document
// containing a single value error.
func DataError(c echo.Context, err *Error) error {
doc := Document{
Errors: ErrorList{err},
}
resp := c.Response()
resp.Header().Set(echo.HeaderContentType, ContentType)
resp.WriteHeader(err.Status)
return json.NewEncoder(resp).Encode(doc)
}
// DataErrorList can be called to send an error answer with a JSON-API document
// containing multiple errors.
func DataErrorList(c echo.Context, errs ...*Error) error {
doc := Document{
Errors: errs,
}
if len(errs) == 0 {
panic("jsonapi.DataErrorList called with empty list.")
}
resp := c.Response()
resp.Header().Set(echo.HeaderContentType, ContentType)
resp.WriteHeader(errs[0].Status)
return json.NewEncoder(resp).Encode(doc)
}
// Bind is used to unmarshal an input JSONApi document. It binds an
// incoming request to a attribute type.
func Bind(body io.Reader, attrs interface{}) (*ObjectMarshalling, error) {
decoder := json.NewDecoder(body)
var doc *Document
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if doc == nil || doc.Data == nil {
return nil, BadJSON()
}
var obj *ObjectMarshalling
if err := json.Unmarshal(*doc.Data, &obj); err != nil {
return nil, err
}
if obj != nil && obj.Attributes != nil && attrs != nil {
if err := json.Unmarshal(*obj.Attributes, &attrs); err != nil {
return nil, err
}
}
return obj, nil
}
// BindCompound is used to unmarshal an compound input JSONApi document.
func BindCompound(body io.Reader) ([]*ObjectMarshalling, error) {
decoder := json.NewDecoder(body)
var doc Document
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if doc.Data == nil {
return nil, BadJSON()
}
var objs []*ObjectMarshalling
if err := json.Unmarshal(*doc.Data, &objs); err != nil {
return nil, err
}
for _, obj := range objs {
if obj == nil {
return nil, BadJSON()
}
}
return objs, nil
}
// BindRelations extracts a Relationships request ( a list of ResourceIdentifier)
func BindRelations(req *http.Request) ([]couchdb.DocReference, error) {
var out []couchdb.DocReference
decoder := json.NewDecoder(req.Body)
var doc Document
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if doc.Data == nil {
return nil, BadJSON()
}
// Attempt Unmarshaling either as ResourceIdentifier or []ResourceIdentifier
if err := json.Unmarshal(*doc.Data, &out); err != nil {
var ri couchdb.DocReference
if err = json.Unmarshal(*doc.Data, &ri); err != nil {
return nil, err
}
out = []couchdb.DocReference{ri}
return out, nil
}
return out, nil
}
// PaginationCursorToParams transforms a Cursor into url.Values
// the url.Values contains only keys page[limit] & page[cursor]
// if the cursor is Done, the values will be empty.
func PaginationCursorToParams(cursor couchdb.Cursor) (url.Values, error) {
v := url.Values{}
if !cursor.HasMore() {
return v, nil
}
switch c := cursor.(type) {
case *couchdb.StartKeyCursor:
cursorObj := []interface{}{c.NextKey, c.NextDocID}
cursorBytes, err := json.Marshal(cursorObj)
if err != nil {
return nil, err
}
v.Set("page[limit]", strconv.Itoa(c.Limit))
v.Set("page[cursor]", string(cursorBytes))
case *couchdb.SkipCursor:
v.Set("page[limit]", strconv.Itoa(c.Limit))
v.Set("page[skip]", strconv.Itoa(c.Skip))
}
return v, nil
}
// ExtractPaginationCursor creates a Cursor from context Query.
func ExtractPaginationCursor(c echo.Context, defaultLimit, maxLimit int) (couchdb.Cursor, error) {
limit := defaultLimit
if limitString := c.QueryParam("page[limit]"); limitString != "" {
reqLimit, err := strconv.ParseInt(limitString, 10, 32)
if err != nil {
return nil, NewError(http.StatusBadRequest, "page limit is not a number")
}
if reqLimit <= 0 {
return nil, NewError(http.StatusBadRequest, "page limit must be positive")
}
limit = int(reqLimit)
}
if maxLimit > 0 && limit > maxLimit {
limit = maxLimit
}
if cursor := c.QueryParam("page[cursor]"); cursor != "" {
var parts []interface{}
err := json.Unmarshal([]byte(cursor), &parts)
if err != nil {
return nil, Errorf(http.StatusBadRequest, "bad json cursor %s", cursor)
}
if len(parts) != 2 {
return nil, Errorf(http.StatusBadRequest, "bad cursor length %s", cursor)
}
nextKey := parts[0]
nextDocID, ok := parts[1].(string)
if !ok {
return nil, Errorf(http.StatusBadRequest, "bad cursor id %s", cursor)
}
return couchdb.NewKeyCursor(limit, nextKey, nextDocID), nil
}
if skipString := c.QueryParam("page[skip]"); skipString != "" {
reqSkip, err := strconv.Atoi(skipString)
if err != nil {
return nil, NewError(http.StatusBadRequest, "page skip is not a number")
}
return couchdb.NewSkipCursor(limit, reqSkip), nil
}
return couchdb.NewKeyCursor(limit, nil, ""), nil
}