// Package pagination defines bounded offset pagination with camelCase fields. package pagination import ( "errors" "net/url" "strconv" ) type Config struct { DefaultSize int `json:"defaultSize"` MaxSize int `json:"maxSize"` MaxOffset int64 `json:"maxOffset"` } type Request struct { Page int64 `json:"page"` PageSize int `json:"pageSize"` } func (r Request) Offset() int64 { return (r.Page - 1) * int64(r.PageSize) } func Parse(query url.Values, cfg Config) (Request, error) { if cfg.DefaultSize == 0 { cfg.DefaultSize = 20 } if cfg.MaxSize == 0 { cfg.MaxSize = 100 } if cfg.MaxOffset == 0 { cfg.MaxOffset = 1000000 } if cfg.DefaultSize < 1 || cfg.MaxSize < cfg.DefaultSize || cfg.MaxSize > 10000 || cfg.MaxOffset < 0 { return Request{}, errors.New("pagination: invalid configuration") } page, size := int64(1), int64(cfg.DefaultSize) for key, target := range map[string]*int64{"page": &page, "pageSize": &size} { if v, ok := query[key]; ok { if len(v) != 1 { return Request{}, errors.New("pagination: duplicate parameter") } n, err := strconv.ParseInt(v[0], 10, 64) if err != nil || n < 1 { return Request{}, errors.New("pagination: positive integer required") } *target = n } } if size > int64(cfg.MaxSize) || (page-1) > cfg.MaxOffset/size { return Request{}, errors.New("pagination: size or offset exceeds limit") } return Request{Page: page, PageSize: int(size)}, nil } type Result[T any] struct { Items []T `json:"items"` Page int64 `json:"page"` PageSize int `json:"pageSize"` Total int64 `json:"total"` TotalPages int64 `json:"totalPages"` } func NewResult[T any](r Request, items []T, total int64) (Result[T], error) { if r.Page < 1 || r.PageSize < 1 || total < 0 || len(items) > r.PageSize || int64(len(items)) > total { return Result[T]{}, errors.New("pagination: invalid result") } if items == nil { items = []T{} } pages := total / int64(r.PageSize) if total%int64(r.PageSize) != 0 { pages++ } return Result[T]{Items: items, Page: r.Page, PageSize: r.PageSize, Total: total, TotalPages: pages}, nil }