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 }