feat: game lobby service
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/domain/application"
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
)
|
||||
|
||||
// ApplicationStore stores application records and their secondary indexes.
|
||||
// Adapters are responsible for maintaining the per-game set, per-user set,
|
||||
// and the single-active lookup key together with the record.
|
||||
type ApplicationStore interface {
|
||||
// Save persists a new submitted application record. The adapter must
|
||||
// enforce the single-active constraint — only one non-rejected
|
||||
// application per (applicant_user_id, game_id) pair may exist at a
|
||||
// time — and return application.ErrConflict if the constraint is
|
||||
// violated. Save rejects records whose status is not submitted.
|
||||
Save(ctx context.Context, record application.Application) error
|
||||
|
||||
// Get returns the record identified by applicationID. It returns
|
||||
// application.ErrNotFound when no record exists.
|
||||
Get(ctx context.Context, applicationID common.ApplicationID) (application.Application, error)
|
||||
|
||||
// GetByGame returns every application attached to gameID. The order
|
||||
// is adapter-defined; callers may reorder as needed.
|
||||
GetByGame(ctx context.Context, gameID common.GameID) ([]application.Application, error)
|
||||
|
||||
// GetByUser returns every application submitted by applicantUserID.
|
||||
// The order is adapter-defined; callers may reorder as needed.
|
||||
GetByUser(ctx context.Context, applicantUserID string) ([]application.Application, error)
|
||||
|
||||
// UpdateStatus applies one status transition in a compare-and-swap
|
||||
// fashion. The adapter must first call application.Transition to
|
||||
// reject invalid pairs without touching the store; on success it must
|
||||
// verify that the current status equals input.ExpectedFrom, update
|
||||
// the primary record, and clear the single-active lookup key when
|
||||
// transitioning to rejected. Adapters set DecidedAt to input.At.
|
||||
UpdateStatus(ctx context.Context, input UpdateApplicationStatusInput) error
|
||||
}
|
||||
|
||||
// UpdateApplicationStatusInput stores the arguments required to apply one
|
||||
// status transition through an ApplicationStore.
|
||||
type UpdateApplicationStatusInput struct {
|
||||
// ApplicationID identifies the record to mutate.
|
||||
ApplicationID common.ApplicationID
|
||||
|
||||
// ExpectedFrom stores the status the caller believes the record
|
||||
// currently has. A mismatch results in application.ErrConflict.
|
||||
ExpectedFrom application.Status
|
||||
|
||||
// To stores the destination status.
|
||||
To application.Status
|
||||
|
||||
// At stores the wall-clock used for DecidedAt.
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid status
|
||||
// transition request.
|
||||
func (input UpdateApplicationStatusInput) Validate() error {
|
||||
if err := input.ApplicationID.Validate(); err != nil {
|
||||
return fmt.Errorf("update application status: application id: %w", err)
|
||||
}
|
||||
if !input.ExpectedFrom.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update application status: expected from status %q is unsupported",
|
||||
input.ExpectedFrom,
|
||||
)
|
||||
}
|
||||
if !input.To.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update application status: to status %q is unsupported",
|
||||
input.To,
|
||||
)
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update application status: at must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NormalizedApplicantUserID trims surrounding whitespace so adapter
|
||||
// keyspace lookups match the form the domain persists.
|
||||
func NormalizedApplicantUserID(userID string) string {
|
||||
return strings.TrimSpace(userID)
|
||||
}
|
||||
Reference in New Issue
Block a user