Files
galaxy-game/lobby/internal/service/shared/page.go
T
2026-04-25 23:20:55 +02:00

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
}