Skip to content

Commit

Permalink
Support for custom pagination logic (#281)
Browse files Browse the repository at this point in the history
* check pagination interface before query params
  • Loading branch information
icgood authored and wwwdata committed Dec 29, 2016
1 parent 3bccfef commit dc368bb
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 37 deletions.
80 changes: 47 additions & 33 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ const (
defaultContentTypHeader = "application/vnd.api+json"
)

var queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`)
var (
queryPageRegex = regexp.MustCompile(`^page\[(\w+)\]$`)
queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`)
)

type information struct {
prefix string
Expand Down Expand Up @@ -404,9 +407,15 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD) *r
func buildRequest(c APIContexter, r *http.Request) Request {
req := Request{PlainRequest: r}
params := make(map[string][]string)
pagination := make(map[string]string)
for key, values := range r.URL.Query() {
params[key] = strings.Split(values[0], ",")
pageMatches := queryPageRegex.FindStringSubmatch(key)
if len(pageMatches) > 1 {
pagination[pageMatches[1]] = values[0]
}
}
req.Pagination = pagination
req.QueryParams = params
req.Header = r.Header
req.Context = c
Expand All @@ -427,25 +436,24 @@ func (res *resource) marshalResponse(resp interface{}, w http.ResponseWriter, st
}

func (res *resource) handleIndex(c APIContexter, w http.ResponseWriter, r *http.Request, info information) error {
pagination := newPaginationQueryParams(r)
if pagination.isValid() {
source, ok := res.source.(PaginatedFindAll)
if !ok {
return NewHTTPError(nil, "Resource does not implement the PaginatedFindAll interface", http.StatusNotFound)
}
if source, ok := res.source.(PaginatedFindAll); ok {
pagination := newPaginationQueryParams(r)

count, response, err := source.PaginatedFindAll(buildRequest(c, r))
if err != nil {
return err
}
if pagination.isValid() {
count, response, err := source.PaginatedFindAll(buildRequest(c, r))
if err != nil {
return err
}

paginationLinks, err := pagination.getLinks(r, count, info)
if err != nil {
return err
}
paginationLinks, err := pagination.getLinks(r, count, info)
if err != nil {
return err
}

return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
}
}

source, ok := res.source.(FindAll)
if !ok {
return NewHTTPError(nil, "Resource does not implement the FindAll interface", http.StatusNotFound)
Expand Down Expand Up @@ -506,26 +514,23 @@ func (res *resource) handleLinked(c APIContexter, api *API, w http.ResponseWrite
request.QueryParams[res.name+"ID"] = []string{id}
request.QueryParams[res.name+"Name"] = []string{linked.Name}

// check for pagination, otherwise normal FindAll
pagination := newPaginationQueryParams(r)
if pagination.isValid() {
source, ok := resource.source.(PaginatedFindAll)
if !ok {
return NewHTTPError(nil, "Resource does not implement the PaginatedFindAll interface", http.StatusNotFound)
}
if source, ok := resource.source.(PaginatedFindAll); ok {
// check for pagination, otherwise normal FindAll
pagination := newPaginationQueryParams(r)
if pagination.isValid() {
var count uint
count, response, err := source.PaginatedFindAll(request)
if err != nil {
return err
}

var count uint
count, response, err := source.PaginatedFindAll(request)
if err != nil {
return err
}
paginationLinks, err := pagination.getLinks(r, count, info)
if err != nil {
return err
}

paginationLinks, err := pagination.getLinks(r, count, info)
if err != nil {
return err
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
}

return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
}

source, ok := resource.source.(FindAll)
Expand Down Expand Up @@ -917,6 +922,15 @@ func (res *resource) respondWith(obj Responder, info information, status int, w
data.Meta = meta
}

if objWithLinks, ok := obj.(LinksResponder); ok {
baseURL := strings.Trim(info.GetBaseURL(), "/")
requestURL := fmt.Sprintf("%s%s", baseURL, r.URL.Path)
links := objWithLinks.Links(r, requestURL)
if len(links) > 0 {
data.Links = links
}
}

return res.marshalResponse(data, w, status, r)
}

Expand Down
21 changes: 20 additions & 1 deletion api_interfaces.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api2go

import "net/http"
import (
"net/http"

"github.com/manyminds/api2go/jsonapi"
)

// The CRUD interface MUST be implemented in order to use the api2go api.
// Use Responder for success status codes and content/meta data. In case of an error,
Expand Down Expand Up @@ -33,6 +37,14 @@ type CRUD interface {
Update(obj interface{}, req Request) (Responder, error)
}

// Pagination represents information needed to return pagination links
type Pagination struct {
Next map[string]string
Prev map[string]string
First map[string]string
Last map[string]string
}

// The PaginatedFindAll interface can be optionally implemented to fetch a subset of all records.
// Pagination query parameters must be used to limit the result. Pagination URLs will automatically
// be generated by the api. You can use a combination of the following 2 query parameters:
Expand Down Expand Up @@ -88,3 +100,10 @@ type Responder interface {
Result() interface{}
StatusCode() int
}

// The LinksResponder interface may be used when the response object is able to return
// a set of links for the top-level response object.
type LinksResponder interface {
Responder
Links(*http.Request, string) jsonapi.Links
}
34 changes: 34 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,17 @@ type fixtureSource struct {
func (s *fixtureSource) FindAll(req Request) (Responder, error) {
var err error

if _, ok := req.Pagination["custom"]; ok {
return &Response{
Res: []*Post{},
Pagination: Pagination{
Next: map[string]string{"type": "next"},
Prev: map[string]string{"type": "prev"},
First: map[string]string{},
},
}, nil
}

if limit, ok := req.QueryParams["limit"]; ok {
if l, err := strconv.ParseInt(limit[0], 10, 64); err == nil {
if s.pointers {
Expand Down Expand Up @@ -1174,6 +1185,18 @@ var _ = Describe("RestHandler", func() {
api2goReq := buildRequest(c, req)
Expect(api2goReq.QueryParams).To(Equal(map[string][]string{"sort": {"title", "date"}}))
})

It("Extracts pagination parameters correctly", func() {
req, err := http.NewRequest("GET", "/v0/posts?page[volume]=one&page[size]=10", nil)
Expect(err).To(BeNil())
c := &APIContext{}

api2goReq := buildRequest(c, req)
Expect(api2goReq.Pagination).To(Equal(map[string]string{
"volume": "one",
"size": "10",
}))
})
})

Context("When using pagination", func() {
Expand Down Expand Up @@ -1232,6 +1255,17 @@ var _ = Describe("RestHandler", func() {
return result
}

Context("custom pagination", func() {
It("returns the correct links", func() {
links := doRequest("/v1/posts?page[custom]=test")
Expect(links).To(Equal(map[string]string{
"next": "/v1/posts?page[custom]=test&page[type]=next",
"prev": "/v1/posts?page[custom]=test&page[type]=prev",
"first": "/v1/posts?page[custom]=test",
}))
})
})

Context("number & size links", func() {
It("No prev and first on first page, size = 1", func() {
links := doRequest("/v1/posts?page[number]=1&page[size]=1")
Expand Down
1 change: 1 addition & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "net/http"
type Request struct {
PlainRequest *http.Request
QueryParams map[string][]string
Pagination map[string]string
Header http.Header
Context APIContexter
}
47 changes: 44 additions & 3 deletions response.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package api2go

import (
"fmt"
"net/http"
"net/url"

"github.com/manyminds/api2go/jsonapi"
)

// The Response struct implements api2go.Responder and can be used as a default
// implementation for your responses
// you can fill the field `Meta` with all the metadata your application needs
// like license, tokens, etc
type Response struct {
Res interface{}
Code int
Meta map[string]interface{}
Res interface{}
Code int
Meta map[string]interface{}
Pagination Pagination
}

// Metadata returns additional meta data
Expand All @@ -24,3 +33,35 @@ func (r Response) Result() interface{} {
func (r Response) StatusCode() int {
return r.Code
}

func buildLink(base string, r *http.Request, pagination map[string]string) jsonapi.Link {
params := r.URL.Query()
for k, v := range pagination {
qk := fmt.Sprintf("page[%s]", k)
params.Set(qk, v)
}
if len(params) == 0 {
return jsonapi.Link{Href: base}
}
query, _ := url.QueryUnescape(params.Encode())
return jsonapi.Link{Href: fmt.Sprintf("%s?%s", base, query)}
}

// Links returns a jsonapi.Links object to include in the top-level response
func (r Response) Links(req *http.Request, baseURL string) (ret jsonapi.Links) {
ret = make(jsonapi.Links)

if r.Pagination.Next != nil {
ret["next"] = buildLink(baseURL, req, r.Pagination.Next)
}
if r.Pagination.Prev != nil {
ret["prev"] = buildLink(baseURL, req, r.Pagination.Prev)
}
if r.Pagination.First != nil {
ret["first"] = buildLink(baseURL, req, r.Pagination.First)
}
if r.Pagination.Last != nil {
ret["last"] = buildLink(baseURL, req, r.Pagination.Last)
}
return
}

0 comments on commit dc368bb

Please sign in to comment.