feat: game lobby service
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
// Package idgen provides the default crypto/rand-backed implementation of
|
||||
// ports.IDGenerator for Game Lobby Service.
|
||||
package idgen
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
)
|
||||
|
||||
// gameIDTokenBytes stores the number of random bytes consumed per
|
||||
// NewGameID call. Ten bytes produce a 16-character base32 suffix, which
|
||||
// gives 80 bits of entropy — well above the birthday-collision bound for the
|
||||
// expected Game Lobby record volume.
|
||||
const gameIDTokenBytes = 10
|
||||
|
||||
// applicationIDTokenBytes mirrors gameIDTokenBytes for application records.
|
||||
// 80 bits of entropy is well above the birthday-collision bound for the
|
||||
// expected application volume.
|
||||
const applicationIDTokenBytes = 10
|
||||
|
||||
// inviteIDTokenBytes mirrors gameIDTokenBytes for invite records.
|
||||
const inviteIDTokenBytes = 10
|
||||
|
||||
// membershipIDTokenBytes mirrors gameIDTokenBytes for membership records.
|
||||
const membershipIDTokenBytes = 10
|
||||
|
||||
// base32NoPadding is the standard RFC 4648 base32 alphabet without padding,
|
||||
// matching the identifier shape used by `galaxy/user/internal/adapters/local`.
|
||||
var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
|
||||
// Generator is the default opaque-identifier generator for Game Lobby
|
||||
// records. Zero value is ready for use and draws randomness from
|
||||
// crypto/rand.Reader.
|
||||
type Generator struct {
|
||||
// reader stores the cryptographic randomness source. A nil reader falls
|
||||
// back to crypto/rand.Reader.
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// Option configures an optional Generator setting.
|
||||
type Option func(*Generator)
|
||||
|
||||
// WithRandomSource overrides the cryptographic randomness source. It is
|
||||
// intended for deterministic tests; production code relies on the default
|
||||
// crypto/rand.Reader.
|
||||
func WithRandomSource(reader io.Reader) Option {
|
||||
return func(gen *Generator) {
|
||||
gen.reader = reader
|
||||
}
|
||||
}
|
||||
|
||||
// NewGenerator constructs one Generator with the supplied options applied.
|
||||
func NewGenerator(opts ...Option) *Generator {
|
||||
gen := &Generator{}
|
||||
for _, opt := range opts {
|
||||
opt(gen)
|
||||
}
|
||||
|
||||
return gen
|
||||
}
|
||||
|
||||
// NewGameID returns one newly generated opaque game identifier with the
|
||||
// frozen `game-*` prefix.
|
||||
func (gen *Generator) NewGameID() (common.GameID, error) {
|
||||
token, err := gen.randomToken(gameIDTokenBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate game id: %w", err)
|
||||
}
|
||||
|
||||
gameID := common.GameID("game-" + token)
|
||||
if err := gameID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("generate game id: %w", err)
|
||||
}
|
||||
|
||||
return gameID, nil
|
||||
}
|
||||
|
||||
// NewApplicationID returns one newly generated opaque application
|
||||
// identifier with the frozen `application-*` prefix.
|
||||
func (gen *Generator) NewApplicationID() (common.ApplicationID, error) {
|
||||
token, err := gen.randomToken(applicationIDTokenBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate application id: %w", err)
|
||||
}
|
||||
|
||||
applicationID := common.ApplicationID("application-" + token)
|
||||
if err := applicationID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("generate application id: %w", err)
|
||||
}
|
||||
|
||||
return applicationID, nil
|
||||
}
|
||||
|
||||
// NewInviteID returns one newly generated opaque invite identifier with the
|
||||
// frozen `invite-*` prefix.
|
||||
func (gen *Generator) NewInviteID() (common.InviteID, error) {
|
||||
token, err := gen.randomToken(inviteIDTokenBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate invite id: %w", err)
|
||||
}
|
||||
|
||||
inviteID := common.InviteID("invite-" + token)
|
||||
if err := inviteID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("generate invite id: %w", err)
|
||||
}
|
||||
|
||||
return inviteID, nil
|
||||
}
|
||||
|
||||
// NewMembershipID returns one newly generated opaque membership identifier
|
||||
// with the frozen `membership-*` prefix.
|
||||
func (gen *Generator) NewMembershipID() (common.MembershipID, error) {
|
||||
token, err := gen.randomToken(membershipIDTokenBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate membership id: %w", err)
|
||||
}
|
||||
|
||||
membershipID := common.MembershipID("membership-" + token)
|
||||
if err := membershipID.Validate(); err != nil {
|
||||
return "", fmt.Errorf("generate membership id: %w", err)
|
||||
}
|
||||
|
||||
return membershipID, nil
|
||||
}
|
||||
|
||||
// randomToken returns one lowercase base32 token of the specified byte
|
||||
// entropy.
|
||||
func (gen *Generator) randomToken(byteCount int) (string, error) {
|
||||
buffer := make([]byte, byteCount)
|
||||
reader := gen.reader
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
if _, err := io.ReadFull(reader, buffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.ToLower(base32NoPadding.EncodeToString(buffer)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user