100 lines
3.3 KiB
Go
100 lines
3.3 KiB
Go
package shared
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
// DefaultPageSize is the default page_size returned when callers omit the
|
|
// query parameter on list endpoints. It mirrors the OpenAPI
|
|
// schema default declared in lobby/api/public-openapi.yaml.
|
|
const DefaultPageSize = 50
|
|
|
|
// MaxPageSize bounds the page_size parameter on list endpoints.
|
|
// It mirrors the OpenAPI schema maximum declared in
|
|
// lobby/api/public-openapi.yaml.
|
|
const MaxPageSize = 200
|
|
|
|
// Page describes a service-level pagination request derived from the
|
|
// transport-layer page_size + page_token parameters. The service is
|
|
// expected to assemble the full in-memory candidate slice, sort it
|
|
// deterministically, and then call Window to compute the slice indices
|
|
// to return alongside the optional continuation token.
|
|
type Page struct {
|
|
// Size stores the maximum number of items returned in one page.
|
|
Size int
|
|
|
|
// Offset stores the zero-based position of the first item to return.
|
|
Offset int
|
|
}
|
|
|
|
// ParsePage decodes the raw transport values into a Page. Both arguments
|
|
// are interpreted exactly as received from the request: the empty
|
|
// rawSize string falls back to DefaultPageSize, and the empty rawToken
|
|
// string represents an initial fetch starting at offset zero. Any
|
|
// validation failure returns an error whose message starts with
|
|
// "invalid " so the public-port writeErrorFromService helper translates
|
|
// it into the OpenAPI-shaped invalid_request envelope.
|
|
func ParsePage(rawSize, rawToken string) (Page, error) {
|
|
page := Page{Size: DefaultPageSize}
|
|
if rawSize != "" {
|
|
n, err := strconv.Atoi(rawSize)
|
|
if err != nil {
|
|
return Page{}, fmt.Errorf("invalid page_size: %s is not an integer", rawSize)
|
|
}
|
|
if n < 1 || n > MaxPageSize {
|
|
return Page{}, fmt.Errorf(
|
|
"invalid page_size: %d must be between 1 and %d",
|
|
n, MaxPageSize,
|
|
)
|
|
}
|
|
page.Size = n
|
|
}
|
|
if rawToken != "" {
|
|
decoded, err := base64.RawURLEncoding.DecodeString(rawToken)
|
|
if err != nil {
|
|
return Page{}, fmt.Errorf("invalid page_token: not a base64url value")
|
|
}
|
|
n, err := strconv.Atoi(string(decoded))
|
|
if err != nil {
|
|
return Page{}, fmt.Errorf("invalid page_token: payload is not an integer")
|
|
}
|
|
if n < 0 {
|
|
return Page{}, fmt.Errorf("invalid page_token: payload must not be negative")
|
|
}
|
|
page.Offset = n
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
// EncodeToken returns the opaque continuation token that callers pass
|
|
// back as page_token on the next request to fetch the next page. The
|
|
// encoding is RFC 4648 §5 base64url without padding so the value is safe
|
|
// to embed in URLs without further escaping.
|
|
func EncodeToken(offset int) string {
|
|
return base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(offset)))
|
|
}
|
|
|
|
// Window computes the slice indices and next-page metadata for a
|
|
// candidate slice of length n given page. start is clamped to [0, n]
|
|
// and end to [start, min(n, start+page.Size)]; nextOffset is set to end
|
|
// when more candidates remain after end, and zero otherwise. hasMore
|
|
// reports whether the caller should emit a non-empty next_page_token.
|
|
func Window(n int, page Page) (start, end, nextOffset int, hasMore bool) {
|
|
if n < 0 {
|
|
n = 0
|
|
}
|
|
size := page.Size
|
|
if size <= 0 {
|
|
size = DefaultPageSize
|
|
}
|
|
start = max(page.Offset, 0)
|
|
start = min(start, n)
|
|
end = min(start+size, n)
|
|
if end < n {
|
|
return start, end, end, true
|
|
}
|
|
return start, end, 0, false
|
|
}
|