feat: backend service
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package backendclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"galaxy/gateway/internal/downstream"
|
||||
lobbymodel "galaxy/model/lobby"
|
||||
"galaxy/transcoder"
|
||||
)
|
||||
|
||||
const (
|
||||
lobbyResultCodeOK = "ok"
|
||||
defaultLobbyErrorCodeInvalid = "invalid_request"
|
||||
defaultLobbyErrorCodeNoSubj = "subject_not_found"
|
||||
defaultLobbyErrorCodeForbid = "forbidden"
|
||||
defaultLobbyErrorCodeConfl = "conflict"
|
||||
defaultLobbyErrorCodeIntErr = "internal_error"
|
||||
)
|
||||
|
||||
var stableLobbyErrorMessages = map[string]string{
|
||||
defaultLobbyErrorCodeInvalid: "request is invalid",
|
||||
defaultLobbyErrorCodeNoSubj: "subject not found",
|
||||
defaultLobbyErrorCodeForbid: "operation is forbidden for the calling user",
|
||||
defaultLobbyErrorCodeConfl: "request conflicts with current state",
|
||||
defaultLobbyErrorCodeIntErr: "internal server error",
|
||||
}
|
||||
|
||||
// ExecuteLobbyCommand routes one authenticated lobby command into
|
||||
// backend's `/api/v1/user/lobby/*` endpoints.
|
||||
func (c *RESTClient) ExecuteLobbyCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
|
||||
if c == nil || c.httpClient == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute lobby command: nil client")
|
||||
}
|
||||
if ctx == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute lobby command: nil context")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return downstream.UnaryResult{}, err
|
||||
}
|
||||
if strings.TrimSpace(command.UserID) == "" {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute lobby command: user_id must not be empty")
|
||||
}
|
||||
|
||||
switch command.MessageType {
|
||||
case lobbymodel.MessageTypeMyGamesList:
|
||||
if _, err := transcoder.PayloadToMyGamesListRequest(command.PayloadBytes); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
|
||||
}
|
||||
return c.executeLobbyMyGames(ctx, command.UserID)
|
||||
case lobbymodel.MessageTypeOpenEnrollment:
|
||||
req, err := transcoder.PayloadToOpenEnrollmentRequest(command.PayloadBytes)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command %q: %w", command.MessageType, err)
|
||||
}
|
||||
return c.executeLobbyOpenEnrollment(ctx, command.UserID, req)
|
||||
default:
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute lobby command: unsupported message type %q", command.MessageType)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeLobbyMyGames(ctx context.Context, userID string) (downstream.UnaryResult, error) {
|
||||
body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/lobby/my/games", userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute lobby.my.games.list: %w", err)
|
||||
}
|
||||
if status == http.StatusOK {
|
||||
var response lobbymodel.MyGamesListResponse
|
||||
if err := decodeStrictJSON(body, &response); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
|
||||
}
|
||||
payloadBytes, err := transcoder.MyGamesListResponseToPayload(&response)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
|
||||
}
|
||||
return downstream.UnaryResult{
|
||||
ResultCode: lobbyResultCodeOK,
|
||||
PayloadBytes: payloadBytes,
|
||||
}, nil
|
||||
}
|
||||
return projectLobbyErrorResponse(status, body)
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeLobbyOpenEnrollment(ctx context.Context, userID string, req *lobbymodel.OpenEnrollmentRequest) (downstream.UnaryResult, error) {
|
||||
if req == nil || strings.TrimSpace(req.GameID) == "" {
|
||||
return downstream.UnaryResult{}, errors.New("execute lobby.game.open-enrollment: game_id must not be empty")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/lobby/games/" + url.PathEscape(req.GameID) + "/open-enrollment"
|
||||
body, status, err := c.do(ctx, http.MethodPost, target, userID, struct{}{})
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute lobby.game.open-enrollment: %w", err)
|
||||
}
|
||||
if status == http.StatusOK {
|
||||
// Backend returns the full LobbyGameDetail; gateway projects the
|
||||
// minimal {game_id, status} pair onto the existing wire shape.
|
||||
var detail struct {
|
||||
GameID string `json:"game_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&detail); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
|
||||
}
|
||||
payloadBytes, err := transcoder.OpenEnrollmentResponseToPayload(&lobbymodel.OpenEnrollmentResponse{
|
||||
GameID: detail.GameID,
|
||||
Status: detail.Status,
|
||||
})
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
|
||||
}
|
||||
return downstream.UnaryResult{
|
||||
ResultCode: lobbyResultCodeOK,
|
||||
PayloadBytes: payloadBytes,
|
||||
}, nil
|
||||
}
|
||||
return projectLobbyErrorResponse(status, body)
|
||||
}
|
||||
|
||||
func projectLobbyErrorResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
|
||||
switch {
|
||||
case statusCode == http.StatusServiceUnavailable:
|
||||
return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
|
||||
case statusCode >= 400 && statusCode <= 599:
|
||||
errResp, err := decodeLobbyError(statusCode, payload)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err)
|
||||
}
|
||||
payloadBytes, err := transcoder.LobbyErrorResponseToPayload(errResp)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err)
|
||||
}
|
||||
return downstream.UnaryResult{
|
||||
ResultCode: errResp.Error.Code,
|
||||
PayloadBytes: payloadBytes,
|
||||
}, nil
|
||||
default:
|
||||
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeLobbyError(statusCode int, payload []byte) (*lobbymodel.ErrorResponse, error) {
|
||||
var response lobbymodel.ErrorResponse
|
||||
decoder := json.NewDecoder(bytes.NewReader(payload))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
||||
if err == nil {
|
||||
return nil, errors.New("unexpected trailing JSON input")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
response.Error.Code = normalizeLobbyErrorCode(statusCode, response.Error.Code)
|
||||
response.Error.Message = normalizeLobbyErrorMessage(response.Error.Code, response.Error.Message)
|
||||
if strings.TrimSpace(response.Error.Code) == "" {
|
||||
return nil, errors.New("missing error code")
|
||||
}
|
||||
if strings.TrimSpace(response.Error.Message) == "" {
|
||||
return nil, errors.New("missing error message")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func normalizeLobbyErrorCode(statusCode int, code string) string {
|
||||
if trimmed := strings.TrimSpace(code); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return defaultLobbyErrorCodeInvalid
|
||||
case http.StatusForbidden:
|
||||
return defaultLobbyErrorCodeForbid
|
||||
case http.StatusNotFound:
|
||||
return defaultLobbyErrorCodeNoSubj
|
||||
case http.StatusConflict:
|
||||
return defaultLobbyErrorCodeConfl
|
||||
default:
|
||||
return defaultLobbyErrorCodeIntErr
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeLobbyErrorMessage(code, message string) string {
|
||||
if trimmed := strings.TrimSpace(message); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
if stable, ok := stableLobbyErrorMessages[code]; ok {
|
||||
return stable
|
||||
}
|
||||
return stableLobbyErrorMessages[defaultLobbyErrorCodeIntErr]
|
||||
}
|
||||
Reference in New Issue
Block a user