Skip to content

Commit

Permalink
feat: offset pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
francesconi committed Aug 21, 2024
1 parent 1c2890c commit b678269
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 0 deletions.
79 changes: 79 additions & 0 deletions pagination/offsetpagination/paginator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package offsetpagination

import (
"net/http"
"strconv"
)

const (
defaultPageSize int = 100
)

type (
Paginator[T any] struct {
page int
pageSize, maxPageSize int
}
Result[T any] struct {
Items []T `json:"items"`
NextPage *int `json:"next_page,omitempty"`
}
Option[T any] func(*Paginator[T])
)

func New[T any](page, pageSize int, opts ...Option[T]) Paginator[T] {
p := Paginator[T]{
page: page,
pageSize: pageSize,
}

for _, opt := range opts {
opt(&p)
}

if p.page <= 0 {
p.page = 1
}
if p.pageSize <= 0 {
p.pageSize = defaultPageSize
}
if p.maxPageSize > 0 && p.pageSize > p.maxPageSize {
p.pageSize = p.maxPageSize
}

return p
}

func (p *Paginator[T]) Offset() int {
return (p.page - 1) * p.pageSize
}

func (p *Paginator[T]) Limit() int {
return p.pageSize + 1
}

func (p *Paginator[T]) Paginate(items []T) Result[T] {
if len(items) > p.pageSize {
nextPage := p.page + 1
return Result[T]{
Items: items[:p.pageSize],
NextPage: &nextPage,
}
}
return Result[T]{
Items: items,
}
}

func Parse[T any](r *http.Request, opts ...Option[T]) Paginator[T] {
q := r.URL.Query()
page, _ := strconv.Atoi(q.Get("page"))
pageSize, _ := strconv.Atoi(q.Get("page_size"))
return New[T](page, pageSize, opts...)
}

func WithMaxPageSize[T any](maxPageSize int) Option[T] {
return func(opt *Paginator[T]) {
opt.maxPageSize = maxPageSize
}
}
107 changes: 107 additions & 0 deletions pagination/offsetpagination/paginator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package offsetpagination

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/HGV/x/pointerx"
"github.com/stretchr/testify/assert"
)

func TestNew(t *testing.T) {
tests := []struct {
p Paginator[any]
expected Paginator[any]
}{
{
p: New[any](0, 0),
expected: Paginator[any]{
page: 1,
pageSize: defaultPageSize,
},
},
{
p: New[any](-1, -100),
expected: Paginator[any]{
page: 1,
pageSize: defaultPageSize,
},
},
{
p: New[any](3, 250),
expected: Paginator[any]{
page: 3,
pageSize: 250,
},
},
{
p: New[any](3, 250, WithMaxPageSize[any](100)),
expected: Paginator[any]{
page: 3,
pageSize: 100,
maxPageSize: 100,
},
},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
assert.Equal(t, tt.expected, tt.p)
})
}
}

func TestOffsetAndLimit(t *testing.T) {
tests := []struct {
p Paginator[any]
expectedOffset int
expectedLimit int
}{
{
p: New[any](1, 250),
expectedOffset: 0,
expectedLimit: 251,
},
{
p: New[any](3, 250),
expectedOffset: 500,
expectedLimit: 251,
},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
assert.Equal(t, tt.expectedOffset, tt.p.Offset())
assert.Equal(t, tt.expectedLimit, tt.p.Limit())
})
}
}

func TestPaginate(t *testing.T) {
p := New[int](1, 250)

items := make([]int, 1000)
for i := 0; i < len(items); i++ {
items[i] = i
}

t.Run("should have a next page", func(t *testing.T) {
result := p.Paginate(items)
assert.Equal(t, len(result.Items), 250)
assert.Equal(t, result.NextPage, pointerx.Ptr(2))
})

t.Run("should not have a next page", func(t *testing.T) {
result := p.Paginate(items[790:])
assert.Equal(t, len(result.Items), 210)
assert.Nil(t, result.NextPage)
})
}

func TestParse(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/items?page=3&page_size=250", nil)
p := Parse[any](r)
assert.Equal(t, p.page, 3)
assert.Equal(t, p.pageSize, 250)
}

0 comments on commit b678269

Please sign in to comment.