feat: new API endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/players/{playerId} (#10)
Reviewed-on: twhelp/corev3#10
This commit is contained in:
parent
bf2b3b178c
commit
572a85bc3c
|
@ -188,6 +188,24 @@ paths:
|
|||
$ref: "#/components/responses/ListPlayersResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
/v2/versions/{versionCode}/servers/{serverKey}/players/{playerId}:
|
||||
get:
|
||||
operationId: getPlayer
|
||||
tags:
|
||||
- versions
|
||||
- servers
|
||||
- players
|
||||
description: Get a player
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||
- $ref: "#/components/parameters/PlayerIdPathParam"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/GetPlayerResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ErrorResponse"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Error:
|
||||
|
@ -1171,6 +1189,13 @@ components:
|
|||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
PlayerIdPathParam:
|
||||
in: path
|
||||
name: playerId
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
ListVersionsResponse:
|
||||
description: ""
|
||||
|
@ -1298,6 +1323,17 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Player"
|
||||
GetPlayerResponse:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/Player"
|
||||
ErrorResponse:
|
||||
description: Default error response.
|
||||
content:
|
||||
|
|
|
@ -190,3 +190,32 @@ func (svc *PlayerService) ListWithRelations(
|
|||
) (domain.ListPlayersWithRelationsResult, error) {
|
||||
return svc.repo.ListWithRelations(ctx, params)
|
||||
}
|
||||
|
||||
func (svc *PlayerService) GetWithRelations(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
serverKey string,
|
||||
) (domain.PlayerWithRelations, error) {
|
||||
params := domain.NewListPlayersParams()
|
||||
if err := params.SetIDs([]int{id}); err != nil {
|
||||
return domain.PlayerWithRelations{}, err
|
||||
}
|
||||
if err := params.SetServerKeys([]string{serverKey}); err != nil {
|
||||
return domain.PlayerWithRelations{}, err
|
||||
}
|
||||
|
||||
res, err := svc.repo.ListWithRelations(ctx, params)
|
||||
if err != nil {
|
||||
return domain.PlayerWithRelations{}, err
|
||||
}
|
||||
|
||||
players := res.Players()
|
||||
if len(players) == 0 {
|
||||
return domain.PlayerWithRelations{}, domain.PlayerNotFoundError{
|
||||
ID: id,
|
||||
ServerKey: serverKey,
|
||||
}
|
||||
}
|
||||
|
||||
return players[0], nil
|
||||
}
|
||||
|
|
|
@ -957,3 +957,29 @@ func (res ListPlayersWithRelationsResult) Self() PlayerCursor {
|
|||
func (res ListPlayersWithRelationsResult) Next() PlayerCursor {
|
||||
return res.next
|
||||
}
|
||||
|
||||
type PlayerNotFoundError struct {
|
||||
ID int
|
||||
ServerKey string
|
||||
}
|
||||
|
||||
var _ ErrorWithParams = PlayerNotFoundError{}
|
||||
|
||||
func (e PlayerNotFoundError) Error() string {
|
||||
return fmt.Sprintf("player with id %d and server key %s not found", e.ID, e.ServerKey)
|
||||
}
|
||||
|
||||
func (e PlayerNotFoundError) Type() ErrorType {
|
||||
return ErrorTypeNotFound
|
||||
}
|
||||
|
||||
func (e PlayerNotFoundError) Code() string {
|
||||
return "player-not-found"
|
||||
}
|
||||
|
||||
func (e PlayerNotFoundError) Params() map[string]any {
|
||||
return map[string]any{
|
||||
"ID": e.ID,
|
||||
"ServerKey": e.ServerKey,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ func NewAPIHTTPHandler(
|
|||
h.versionMiddleware,
|
||||
h.serverMiddleware,
|
||||
h.tribeMiddleware,
|
||||
h.playerMiddleware,
|
||||
},
|
||||
ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
h.errorRenderer.render(w, r, err)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package port
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
//nolint:gocyclo
|
||||
|
@ -75,6 +78,59 @@ func (h *apiHTTPHandler) ListPlayers(
|
|||
renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersResponse(res))
|
||||
}
|
||||
|
||||
func (h *apiHTTPHandler) GetPlayer(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
_ apimodel.VersionCodePathParam,
|
||||
_ apimodel.ServerKeyPathParam,
|
||||
_ apimodel.PlayerIdPathParam,
|
||||
) {
|
||||
player, _ := playerFromContext(r.Context())
|
||||
renderJSON(w, r, http.StatusOK, apimodel.NewGetPlayerResponse(player))
|
||||
}
|
||||
|
||||
func (h *apiHTTPHandler) playerMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
routeCtx := chi.RouteContext(ctx)
|
||||
server, serverOK := serverFromContext(ctx)
|
||||
playerIDIdx := slices.Index(routeCtx.URLParams.Keys, "playerId")
|
||||
|
||||
if !serverOK || playerIDIdx < 0 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
playerID, err := strconv.Atoi(routeCtx.URLParams.Values[playerIDIdx])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
player, err := h.playerSvc.GetWithRelations(
|
||||
ctx,
|
||||
playerID,
|
||||
server.Key(),
|
||||
)
|
||||
if err != nil {
|
||||
h.errorRenderer.withErrorPathFormatter(formatGetPlayerErrorPath).render(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(playerToContext(ctx, player)))
|
||||
})
|
||||
}
|
||||
|
||||
type playerCtxKey struct{}
|
||||
|
||||
func playerToContext(ctx context.Context, p domain.PlayerWithRelations) context.Context {
|
||||
return context.WithValue(ctx, playerCtxKey{}, p)
|
||||
}
|
||||
|
||||
func playerFromContext(ctx context.Context) (domain.PlayerWithRelations, bool) {
|
||||
p, ok := ctx.Value(playerCtxKey{}).(domain.PlayerWithRelations)
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func formatListPlayersErrorPath(segments []errorPathSegment) []string {
|
||||
if segments[0].model != "ListPlayersParams" {
|
||||
return nil
|
||||
|
@ -103,3 +159,16 @@ func formatListPlayersErrorPath(segments []errorPathSegment) []string {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func formatGetPlayerErrorPath(segments []errorPathSegment) []string {
|
||||
if segments[0].model != "ListPlayersParams" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch segments[0].field {
|
||||
case "ids":
|
||||
return []string{"$path", "playerId"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -752,6 +752,196 @@ func TestListPlayers(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
const endpointGetPlayer = "/v2/versions/%s/servers/%s/players/%d"
|
||||
|
||||
func TestGetPlayer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := newAPIHTTPHandler(t)
|
||||
player := randPlayer(t, handler)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reqModifier func(t *testing.T, req *http.Request)
|
||||
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.GetPlayerResponse](t, resp.Body)
|
||||
assert.Equal(t, player.Player, body.Data)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: id < 1",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(
|
||||
endpointGetPlayer,
|
||||
player.Server.Version.Code,
|
||||
player.Server.Key,
|
||||
domaintest.RandID()*-1,
|
||||
)
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
pathSegments := strings.Split(req.URL.Path, "/")
|
||||
require.Len(t, pathSegments, 8)
|
||||
id, err := strconv.Atoi(pathSegments[7])
|
||||
require.NoError(t, err)
|
||||
domainErr := domain.MinGreaterEqualError{
|
||||
Min: 1,
|
||||
Current: id,
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"min": float64(domainErr.Min),
|
||||
"current": float64(domainErr.Current),
|
||||
},
|
||||
Path: []string{"$path", "playerId"},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: version not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(endpointGetPlayer, domaintest.RandVersionCode(), player.Server.Key, player.Id)
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
pathSegments := strings.Split(req.URL.Path, "/")
|
||||
require.Len(t, pathSegments, 8)
|
||||
domainErr := domain.VersionNotFoundError{
|
||||
VersionCode: pathSegments[3],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"code": domainErr.VersionCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: server not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(endpointGetPlayer, player.Server.Version.Code, domaintest.RandServerKey(), player.Id)
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
pathSegments := strings.Split(req.URL.Path, "/")
|
||||
require.Len(t, pathSegments, 8)
|
||||
domainErr := domain.ServerNotFoundError{
|
||||
Key: pathSegments[5],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"key": domainErr.Key,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: player not found",
|
||||
reqModifier: func(t *testing.T, req *http.Request) {
|
||||
t.Helper()
|
||||
req.URL.Path = fmt.Sprintf(
|
||||
endpointGetPlayer,
|
||||
player.Server.Version.Code,
|
||||
player.Server.Key,
|
||||
domaintest.RandID(),
|
||||
)
|
||||
},
|
||||
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
|
||||
t.Helper()
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// body
|
||||
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
|
||||
pathSegments := strings.Split(req.URL.Path, "/")
|
||||
require.Len(t, pathSegments, 8)
|
||||
id, err := strconv.Atoi(pathSegments[7])
|
||||
require.NoError(t, err)
|
||||
domainErr := domain.PlayerNotFoundError{
|
||||
ID: id,
|
||||
ServerKey: pathSegments[5],
|
||||
}
|
||||
assert.Equal(t, apimodel.ErrorResponse{
|
||||
Errors: []apimodel.Error{
|
||||
{
|
||||
Code: domainErr.Code(),
|
||||
Message: domainErr.Error(),
|
||||
Params: map[string]any{
|
||||
"id": float64(domainErr.ID),
|
||||
"serverKey": domainErr.ServerKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(endpointGetPlayer, player.Server.Version.Code, player.Server.Key, player.Id),
|
||||
nil,
|
||||
)
|
||||
if tt.reqModifier != nil {
|
||||
tt.reqModifier(t, req)
|
||||
}
|
||||
|
||||
resp := doCustomRequest(handler, req)
|
||||
defer resp.Body.Close()
|
||||
tt.assertResp(t, req, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type playerWithServer struct {
|
||||
apimodel.Player
|
||||
Server serverWithVersion
|
||||
|
@ -782,3 +972,9 @@ func getAllPlayers(tb testing.TB, h http.Handler) []playerWithServer {
|
|||
|
||||
return players
|
||||
}
|
||||
|
||||
func randPlayer(tb testing.TB, h http.Handler) playerWithServer {
|
||||
tb.Helper()
|
||||
players := getAllPlayers(tb, h)
|
||||
return players[gofakeit.IntRange(0, len(players)-1)]
|
||||
}
|
||||
|
|
|
@ -63,3 +63,9 @@ func NewListPlayersResponse(res domain.ListPlayersWithRelationsResult) ListPlaye
|
|||
|
||||
return resp
|
||||
}
|
||||
|
||||
func NewGetPlayerResponse(p domain.PlayerWithRelations) GetPlayerResponse {
|
||||
return GetPlayerResponse{
|
||||
Data: NewPlayer(p),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue