Add Paginate middleware
This commit is contained in:
parent
edb7e9ec96
commit
1676eb9d42
159
paginate/paginate.go
Normal file
159
paginate/paginate.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package paginate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.pkg.cx/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
var (
|
||||||
|
ErrPaginationDefaults = errors.New("pagination defaults are nil")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context keys
|
||||||
|
var (
|
||||||
|
PaginationCtxKey = &middleware.CtxKey{Pkg: "go.pkg.cx/middleware/paginate", Name: "Pagination"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultOptions represents default paginate middleware options
|
||||||
|
var DefaultOptions = Options(
|
||||||
|
WithFindPaginationFn(PaginationFromQuery("page", "pageSize")),
|
||||||
|
SetPaginationDefaults(1, 50),
|
||||||
|
SetValidatePaginationFn(allowAll),
|
||||||
|
SetResponseHandler(RespondWithBadRequest),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pagination represents pagination info
|
||||||
|
type Pagination struct {
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options turns a list of option instances into an option
|
||||||
|
func Options(opts ...Option) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option configures paginate middleware
|
||||||
|
type Option func(p *paginate)
|
||||||
|
|
||||||
|
// WithFindPaginationFn adds pagination find function to the list
|
||||||
|
func WithFindPaginationFn(fn func(r *http.Request, p *Pagination) *Pagination) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
p.findPaginationFns = append(p.findPaginationFns, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFindPaginationFns sets pagination find functions list
|
||||||
|
func SetFindPaginationFns(fns ...func(r *http.Request, p *Pagination) *Pagination) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
p.findPaginationFns = fns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPaginationDefaults sets pagination defaults function
|
||||||
|
func SetPaginationDefaults(page int, pageSize int) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
p.paginationDefaultsFn = func() *Pagination {
|
||||||
|
return &Pagination{Page: page, PageSize: pageSize}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValidatePaginationFn sets pagination validation function
|
||||||
|
func SetValidatePaginationFn(fn func(p *Pagination) error) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
p.validatePaginationFn = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResponseHandler sets response handler
|
||||||
|
func SetResponseHandler(fn middleware.ResponseHandle) Option {
|
||||||
|
return func(p *paginate) {
|
||||||
|
p.responseHandler = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type paginate struct {
|
||||||
|
findPaginationFns []func(r *http.Request, p *Pagination) *Pagination
|
||||||
|
paginationDefaultsFn func() *Pagination
|
||||||
|
validatePaginationFn func(p *Pagination) error
|
||||||
|
responseHandler middleware.ResponseHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns paginate middleware
|
||||||
|
func Middleware(opts ...Option) func(next http.Handler) http.Handler {
|
||||||
|
p := &paginate{}
|
||||||
|
opts = append([]Option{DefaultOptions}, opts...)
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pagination := p.paginationDefaultsFn()
|
||||||
|
if pagination == nil {
|
||||||
|
p.responseHandler(w, r, ErrPaginationDefaults)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range p.findPaginationFns {
|
||||||
|
if nextPagination := fn(r, pagination); nextPagination != nil {
|
||||||
|
pagination = nextPagination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.validatePaginationFn(pagination); err != nil {
|
||||||
|
p.responseHandler(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, PaginationCtxKey, pagination)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationFromContext returns pagination from context
|
||||||
|
func PaginationFromContext(ctx context.Context) *Pagination {
|
||||||
|
if pagination, ok := ctx.Value(PaginationCtxKey).(*Pagination); ok {
|
||||||
|
return pagination
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginationFromQuery returns pagination from query params
|
||||||
|
func PaginationFromQuery(pageParam string, pageSizeParam string) func(r *http.Request, p *Pagination) *Pagination {
|
||||||
|
return func(r *http.Request, p *Pagination) *Pagination {
|
||||||
|
if page, err := strconv.Atoi(r.URL.Query().Get(pageParam)); err == nil {
|
||||||
|
p.Page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageSize, err := strconv.Atoi(r.URL.Query().Get(pageSizeParam)); err == nil {
|
||||||
|
p.PageSize = pageSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondWithBadRequest is a default response handler
|
||||||
|
func RespondWithBadRequest(w http.ResponseWriter, _ *http.Request, _ error) {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowAll(_ *Pagination) error {
|
||||||
|
return nil
|
||||||
|
}
|
68
paginate/paginate_test.go
Normal file
68
paginate/paginate_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package paginate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPaginationFromQuery(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "/?page=2&pageSize=10", http.NoBody)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
pagination := PaginationFromQuery("page", "pageSize")(req, &Pagination{})
|
||||||
|
assert.Equal(t, &Pagination{Page: 2, PageSize: 10}, pagination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidPagination(t *testing.T) {
|
||||||
|
opts := []Option{
|
||||||
|
SetPaginationDefaults(1, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pagination := PaginationFromContext(r.Context())
|
||||||
|
assert.NotNil(t, pagination)
|
||||||
|
assert.Equal(t, 2, pagination.Page)
|
||||||
|
assert.Equal(t, 10, pagination.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := Middleware(opts...)(handler)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/?page=2&pageSize=10", http.NoBody)
|
||||||
|
middleware.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidPagination(t *testing.T) {
|
||||||
|
opts := []Option{
|
||||||
|
SetPaginationDefaults(1, 10),
|
||||||
|
SetValidatePaginationFn(func(p *Pagination) error {
|
||||||
|
if p.Page < 1 || p.PageSize < 1 {
|
||||||
|
return errors.New("invalid pagination")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pagination := PaginationFromContext(r.Context())
|
||||||
|
assert.NotNil(t, pagination)
|
||||||
|
assert.Equal(t, -1, pagination.Page)
|
||||||
|
assert.Equal(t, 10, pagination.PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := Middleware(opts...)(handler)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", "/?page=-1&pageSize=10", http.NoBody)
|
||||||
|
middleware.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user