Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions pagination/offsetpagination/paginator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package offsetpagination

import (
"errors"
"net/url"
"strconv"
)

const (
defaultPageSize int = 50
maxPageSize int = 100
)

type (
Paged interface {
Offset() int
Limit() int
}
Paginator[T any] struct {
page int
pageSize int
}
Result[T any] struct {
Items []T `json:"items"`
NextPage int `json:"next_page,omitempty"`
}
)

var _ Paged = new(Paginator[any])

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

if p.page <= 0 {
p.page = 1
}
if p.pageSize <= 0 {
p.pageSize = defaultPageSize
}
if p.pageSize > maxPageSize {
p.pageSize = 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 {
return Result[T]{
Items: items[:p.pageSize],
NextPage: p.page + 1,
}
}
return Result[T]{
Items: items,
}
}

func (r Result[T]) HasNextPage() bool {
return r.NextPage > 0
}

var (
ErrInvalidPage = errors.New("query parameter `page` must be an integer, got string")
ErrPageTooSmall = errors.New("query parameter `page` must be non-negative")
ErrInvalidPageSize = errors.New("query parameter `page_size` must be an integer, got string")
ErrPageSizeTooSmall = errors.New("query parameter `page_size` must be non-negative")
)

func Parse[T any](q url.Values) (*Paginator[T], error) {
var page, pageSize int
var err error

if pageParam := q.Get("page"); pageParam != "" {
page, err = strconv.Atoi(pageParam)
if err != nil {
return nil, ErrInvalidPage
}
if page <= 0 {
return nil, ErrPageTooSmall
}
}

if pageSizeParam := q.Get("page_size"); pageSizeParam != "" {
pageSize, err = strconv.Atoi(pageSizeParam)
if err != nil {
return nil, ErrInvalidPageSize
}
if pageSize <= 0 {
return nil, ErrPageSizeTooSmall
}
}

p := New[T](page, pageSize)
return &p, nil
}
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/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, 100),
expected: Paginator[any]{
page: 3,
pageSize: 100,
},
},
{
p: New[any](3, 300),
expected: Paginator[any]{
page: 3,
pageSize: 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, 100),
expectedOffset: 0,
expectedLimit: 101,
},
{
p: New[any](3, 100),
expectedOffset: 200,
expectedLimit: 101,
},
}

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, 75)

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), 75)
assert.Equal(t, result.NextPage, 2)
assert.True(t, result.HasNextPage())
})

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

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