feat: game lobby service
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user