Skip to content

Commit dc368bb

Browse files
icgoodwwwdata
authored andcommitted
Support for custom pagination logic (#281)
* check pagination interface before query params
1 parent 3bccfef commit dc368bb

File tree

5 files changed

+146
-37
lines changed

5 files changed

+146
-37
lines changed

api.go

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ const (
2222
defaultContentTypHeader = "application/vnd.api+json"
2323
)
2424

25-
var queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`)
25+
var (
26+
queryPageRegex = regexp.MustCompile(`^page\[(\w+)\]$`)
27+
queryFieldsRegex = regexp.MustCompile(`^fields\[(\w+)\]$`)
28+
)
2629

2730
type information struct {
2831
prefix string
@@ -404,9 +407,15 @@ func (api *API) addResource(prototype jsonapi.MarshalIdentifier, source CRUD) *r
404407
func buildRequest(c APIContexter, r *http.Request) Request {
405408
req := Request{PlainRequest: r}
406409
params := make(map[string][]string)
410+
pagination := make(map[string]string)
407411
for key, values := range r.URL.Query() {
408412
params[key] = strings.Split(values[0], ",")
413+
pageMatches := queryPageRegex.FindStringSubmatch(key)
414+
if len(pageMatches) > 1 {
415+
pagination[pageMatches[1]] = values[0]
416+
}
409417
}
418+
req.Pagination = pagination
410419
req.QueryParams = params
411420
req.Header = r.Header
412421
req.Context = c
@@ -427,25 +436,24 @@ func (res *resource) marshalResponse(resp interface{}, w http.ResponseWriter, st
427436
}
428437

429438
func (res *resource) handleIndex(c APIContexter, w http.ResponseWriter, r *http.Request, info information) error {
430-
pagination := newPaginationQueryParams(r)
431-
if pagination.isValid() {
432-
source, ok := res.source.(PaginatedFindAll)
433-
if !ok {
434-
return NewHTTPError(nil, "Resource does not implement the PaginatedFindAll interface", http.StatusNotFound)
435-
}
439+
if source, ok := res.source.(PaginatedFindAll); ok {
440+
pagination := newPaginationQueryParams(r)
436441

437-
count, response, err := source.PaginatedFindAll(buildRequest(c, r))
438-
if err != nil {
439-
return err
440-
}
442+
if pagination.isValid() {
443+
count, response, err := source.PaginatedFindAll(buildRequest(c, r))
444+
if err != nil {
445+
return err
446+
}
441447

442-
paginationLinks, err := pagination.getLinks(r, count, info)
443-
if err != nil {
444-
return err
445-
}
448+
paginationLinks, err := pagination.getLinks(r, count, info)
449+
if err != nil {
450+
return err
451+
}
446452

447-
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
453+
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
454+
}
448455
}
456+
449457
source, ok := res.source.(FindAll)
450458
if !ok {
451459
return NewHTTPError(nil, "Resource does not implement the FindAll interface", http.StatusNotFound)
@@ -506,26 +514,23 @@ func (res *resource) handleLinked(c APIContexter, api *API, w http.ResponseWrite
506514
request.QueryParams[res.name+"ID"] = []string{id}
507515
request.QueryParams[res.name+"Name"] = []string{linked.Name}
508516

509-
// check for pagination, otherwise normal FindAll
510-
pagination := newPaginationQueryParams(r)
511-
if pagination.isValid() {
512-
source, ok := resource.source.(PaginatedFindAll)
513-
if !ok {
514-
return NewHTTPError(nil, "Resource does not implement the PaginatedFindAll interface", http.StatusNotFound)
515-
}
517+
if source, ok := resource.source.(PaginatedFindAll); ok {
518+
// check for pagination, otherwise normal FindAll
519+
pagination := newPaginationQueryParams(r)
520+
if pagination.isValid() {
521+
var count uint
522+
count, response, err := source.PaginatedFindAll(request)
523+
if err != nil {
524+
return err
525+
}
516526

517-
var count uint
518-
count, response, err := source.PaginatedFindAll(request)
519-
if err != nil {
520-
return err
521-
}
527+
paginationLinks, err := pagination.getLinks(r, count, info)
528+
if err != nil {
529+
return err
530+
}
522531

523-
paginationLinks, err := pagination.getLinks(r, count, info)
524-
if err != nil {
525-
return err
532+
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
526533
}
527-
528-
return res.respondWithPagination(response, info, http.StatusOK, paginationLinks, w, r)
529534
}
530535

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

925+
if objWithLinks, ok := obj.(LinksResponder); ok {
926+
baseURL := strings.Trim(info.GetBaseURL(), "/")
927+
requestURL := fmt.Sprintf("%s%s", baseURL, r.URL.Path)
928+
links := objWithLinks.Links(r, requestURL)
929+
if len(links) > 0 {
930+
data.Links = links
931+
}
932+
}
933+
920934
return res.marshalResponse(data, w, status, r)
921935
}
922936

api_interfaces.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package api2go
22

3-
import "net/http"
3+
import (
4+
"net/http"
5+
6+
"github.com/manyminds/api2go/jsonapi"
7+
)
48

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

40+
// Pagination represents information needed to return pagination links
41+
type Pagination struct {
42+
Next map[string]string
43+
Prev map[string]string
44+
First map[string]string
45+
Last map[string]string
46+
}
47+
3648
// The PaginatedFindAll interface can be optionally implemented to fetch a subset of all records.
3749
// Pagination query parameters must be used to limit the result. Pagination URLs will automatically
3850
// be generated by the api. You can use a combination of the following 2 query parameters:
@@ -88,3 +100,10 @@ type Responder interface {
88100
Result() interface{}
89101
StatusCode() int
90102
}
103+
104+
// The LinksResponder interface may be used when the response object is able to return
105+
// a set of links for the top-level response object.
106+
type LinksResponder interface {
107+
Responder
108+
Links(*http.Request, string) jsonapi.Links
109+
}

api_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,17 @@ type fixtureSource struct {
219219
func (s *fixtureSource) FindAll(req Request) (Responder, error) {
220220
var err error
221221

222+
if _, ok := req.Pagination["custom"]; ok {
223+
return &Response{
224+
Res: []*Post{},
225+
Pagination: Pagination{
226+
Next: map[string]string{"type": "next"},
227+
Prev: map[string]string{"type": "prev"},
228+
First: map[string]string{},
229+
},
230+
}, nil
231+
}
232+
222233
if limit, ok := req.QueryParams["limit"]; ok {
223234
if l, err := strconv.ParseInt(limit[0], 10, 64); err == nil {
224235
if s.pointers {
@@ -1174,6 +1185,18 @@ var _ = Describe("RestHandler", func() {
11741185
api2goReq := buildRequest(c, req)
11751186
Expect(api2goReq.QueryParams).To(Equal(map[string][]string{"sort": {"title", "date"}}))
11761187
})
1188+
1189+
It("Extracts pagination parameters correctly", func() {
1190+
req, err := http.NewRequest("GET", "/v0/posts?page[volume]=one&page[size]=10", nil)
1191+
Expect(err).To(BeNil())
1192+
c := &APIContext{}
1193+
1194+
api2goReq := buildRequest(c, req)
1195+
Expect(api2goReq.Pagination).To(Equal(map[string]string{
1196+
"volume": "one",
1197+
"size": "10",
1198+
}))
1199+
})
11771200
})
11781201

11791202
Context("When using pagination", func() {
@@ -1232,6 +1255,17 @@ var _ = Describe("RestHandler", func() {
12321255
return result
12331256
}
12341257

1258+
Context("custom pagination", func() {
1259+
It("returns the correct links", func() {
1260+
links := doRequest("/v1/posts?page[custom]=test")
1261+
Expect(links).To(Equal(map[string]string{
1262+
"next": "/v1/posts?page[custom]=test&page[type]=next",
1263+
"prev": "/v1/posts?page[custom]=test&page[type]=prev",
1264+
"first": "/v1/posts?page[custom]=test",
1265+
}))
1266+
})
1267+
})
1268+
12351269
Context("number & size links", func() {
12361270
It("No prev and first on first page, size = 1", func() {
12371271
links := doRequest("/v1/posts?page[number]=1&page[size]=1")

request.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "net/http"
66
type Request struct {
77
PlainRequest *http.Request
88
QueryParams map[string][]string
9+
Pagination map[string]string
910
Header http.Header
1011
Context APIContexter
1112
}

response.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package api2go
22

3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
8+
"github.com/manyminds/api2go/jsonapi"
9+
)
10+
311
// The Response struct implements api2go.Responder and can be used as a default
412
// implementation for your responses
513
// you can fill the field `Meta` with all the metadata your application needs
614
// like license, tokens, etc
715
type Response struct {
8-
Res interface{}
9-
Code int
10-
Meta map[string]interface{}
16+
Res interface{}
17+
Code int
18+
Meta map[string]interface{}
19+
Pagination Pagination
1120
}
1221

1322
// Metadata returns additional meta data
@@ -24,3 +33,35 @@ func (r Response) Result() interface{} {
2433
func (r Response) StatusCode() int {
2534
return r.Code
2635
}
36+
37+
func buildLink(base string, r *http.Request, pagination map[string]string) jsonapi.Link {
38+
params := r.URL.Query()
39+
for k, v := range pagination {
40+
qk := fmt.Sprintf("page[%s]", k)
41+
params.Set(qk, v)
42+
}
43+
if len(params) == 0 {
44+
return jsonapi.Link{Href: base}
45+
}
46+
query, _ := url.QueryUnescape(params.Encode())
47+
return jsonapi.Link{Href: fmt.Sprintf("%s?%s", base, query)}
48+
}
49+
50+
// Links returns a jsonapi.Links object to include in the top-level response
51+
func (r Response) Links(req *http.Request, baseURL string) (ret jsonapi.Links) {
52+
ret = make(jsonapi.Links)
53+
54+
if r.Pagination.Next != nil {
55+
ret["next"] = buildLink(baseURL, req, r.Pagination.Next)
56+
}
57+
if r.Pagination.Prev != nil {
58+
ret["prev"] = buildLink(baseURL, req, r.Pagination.Prev)
59+
}
60+
if r.Pagination.First != nil {
61+
ret["first"] = buildLink(baseURL, req, r.Pagination.First)
62+
}
63+
if r.Pagination.Last != nil {
64+
ret["last"] = buildLink(baseURL, req, r.Pagination.Last)
65+
}
66+
return
67+
}

0 commit comments

Comments
 (0)