Add Paginate middleware

This commit is contained in:
Anton Zadvorny 2023-06-26 03:26:48 +06:00
parent edb7e9ec96
commit 1676eb9d42
2 changed files with 227 additions and 0 deletions

159
paginate/paginate.go Normal file
View 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
View 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)
}