feat: add a new API endpoint - /api/v1/versions/{versionCode}/players (#37)
ci/woodpecker/push/govulncheck Pipeline failed Details
ci/woodpecker/push/test Pipeline failed Details

Reviewed-on: twhelp/corev3#37
This commit is contained in:
Dawid Wysokiński 2024-03-27 06:18:37 +00:00
parent 99b187d930
commit c1dfb0fa9b
14 changed files with 1381 additions and 38 deletions

View File

@ -11,13 +11,21 @@ info:
name: MIT
tags:
- name: versions
description: Version related endpoints
- name: servers
description: Server related endpoints
- name: tribes
description: Tribe related endpoints
- name: players
description: Player related endpoints
- name: villages
description: Village related endpoints
- name: ennoblements
description: Ennoblement (conquer) related endpoints
- name: tribe-changes
description: Tribe change related endpoints
- name: snapshots
description: Snapshot (historical records) related endpoints
servers:
- url: "{scheme}://{hostname}/api"
variables:
@ -56,13 +64,32 @@ paths:
$ref: "#/components/responses/GetVersionResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/players:
get:
operationId: listVersionPlayers
tags:
- versions
- players
description: List players associated with the given version
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/PlayerDeletedQueryParam"
- $ref: "#/components/parameters/PlayerSortQueryParam"
- $ref: "#/components/parameters/PlayerNameQueryParam"
responses:
200:
$ref: "#/components/responses/ListPlayersWithServersResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers:
get:
operationId: listServers
tags:
- versions
- servers
description: List servers
description: List servers associated with the given version
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/CursorQueryParam"
@ -140,7 +167,7 @@ paths:
- versions
- servers
- tribes
description: List tribes
description: List tribes associated with the given server
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -173,12 +200,12 @@ paths:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/players:
get:
operationId: listPlayers
operationId: listServerPlayers
tags:
- versions
- servers
- players
description: List players
description: List players associated with the given server
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -237,7 +264,7 @@ paths:
- versions
- servers
- villages
description: List villages
description: List villages associated with the given server
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -313,7 +340,7 @@ paths:
- versions
- servers
- ennoblements
description: List ennoblements
description: List ennoblements associated with the given server
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -597,6 +624,21 @@ components:
createdAt:
type: string
format: date-time
ServerMeta:
type: object
required:
- key
- open
- url
properties:
key:
$ref: "#/components/schemas/ServerKey"
open:
type: boolean
url:
type: string
format: uri
example: https://en138.tribalwars.net
ServerConfig:
type: object
required:
@ -1371,6 +1413,15 @@ components:
deletedAt:
type: string
format: date-time
PlayerWithServer:
allOf:
- $ref: "#/components/schemas/Player"
- type: object
required:
- server
properties:
server:
$ref: "#/components/schemas/ServerMeta"
PlayerMeta:
type: object
required:
@ -1936,6 +1987,21 @@ components:
properties:
data:
$ref: "#/components/schemas/Tribe"
ListPlayersWithServersResponse:
description: ""
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- type: object
required:
- data
properties:
data:
type: array
items:
$ref: "#/components/schemas/PlayerWithServer"
ListPlayersResponse:
description: ""
content:

View File

@ -118,10 +118,7 @@ func (repo *PlayerBunRepository) ListWithRelations(
if err := repo.db.NewSelect().
Model(&players).
Apply(listPlayersParamsApplier{params: params}.apply).
Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Column(bunmodel.TribeMetaColumns...)
}).
Apply(listPlayersParamsApplier{params: params, includeRelations: true}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListPlayersWithRelationsResult{}, fmt.Errorf("couldn't select players from the db: %w", err)
}
@ -155,7 +152,8 @@ func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, i
}
type listPlayersParamsApplier struct {
params domain.ListPlayersParams
params domain.ListPlayersParams
includeRelations bool
}
func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
@ -192,7 +190,38 @@ func (a listPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q.OrderExpr("? ?", column, dir.Bun())
}
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
return q.Limit(a.params.Limit() + 1).
Apply(a.applyCursor).
Apply(a.applyRelations)
}
func (a listPlayersParamsApplier) applyRelations(q *bun.SelectQuery) *bun.SelectQuery {
if a.includeRelations {
q = q.Relation("Tribe", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Column(bunmodel.TribeMetaColumns...)
})
}
if versionCodes := a.params.VersionCodes(); len(versionCodes) > 0 || a.includeRelations {
q = q.Join("INNER JOIN servers as server").JoinOn("server.key = player.server_key")
// according to EXPLAIN ANALYZE (https://www.postgresql.org/docs/current/sql-explain.html)
// this way of filtering servers is much more efficient compared to
// using the WHERE clause
if len(versionCodes) > 0 {
q = q.JoinOn("server.version_code IN (?)", bun.In(versionCodes))
}
if a.includeRelations {
q = q.ColumnExpr("?TableColumns")
for _, col := range bunmodel.ServerMetaColumns {
safeCol := bun.Safe(col)
q = q.ColumnExpr("server.? as server__?", safeCol, safeCol)
}
}
}
return q
}
//nolint:gocyclo

View File

@ -326,6 +326,51 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
},
},
{
name: "OK: versionCodes",
params: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
playerParams := domain.NewListPlayersParams()
resPlayers, err := repos.player.List(ctx, playerParams)
require.NoError(t, err)
require.NotEmpty(t, resPlayers.Players())
randPlayer := resPlayers.Players()[0]
serverParams := domain.NewListServersParams()
require.NoError(t, serverParams.SetKeys([]string{randPlayer.ServerKey()}))
resServers, err := repos.server.List(ctx, serverParams)
require.NoError(t, err)
require.NotEmpty(t, resServers.Servers())
require.NoError(t, playerParams.SetVersionCodes([]string{resServers.Servers()[0].VersionCode()}))
return playerParams
},
assertResult: func(t *testing.T, params domain.ListPlayersParams, res domain.ListPlayersResult) {
t.Helper()
versionCodes := params.VersionCodes()
serverParams := domain.NewListServersParams()
require.NoError(t, serverParams.SetVersionCodes(versionCodes))
resServers, err := repos.server.List(ctx, serverParams)
require.NoError(t, err)
servers := resServers.Servers()
require.NotEmpty(t, servers)
players := res.Players()
assert.NotEmpty(t, res.Players())
for _, p := range players {
assert.True(t, slices.ContainsFunc(servers, func(server domain.Server) bool {
return server.Key() == p.ServerKey() && slices.Contains(versionCodes, server.VersionCode())
}))
}
},
},
{
name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListPlayersParams {
@ -611,6 +656,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.Len(t, resWithRelations.Players(), len(res.Players()))
for i, p := range resWithRelations.Players() {
assert.Equal(t, res.Players()[i], p.Player())
assert.Equal(t, p.Player().ServerKey(), p.Server().Key())
assert.Equal(t, p.Player().TribeID(), p.Tribe().V.ID())
assert.Equal(t, p.Player().TribeID() != 0, p.Tribe().Valid)
}

View File

@ -98,10 +98,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.ListVillagesParams
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
// assertResultWithRelations is optional
name string
params func(t *testing.T) domain.ListVillagesParams
assertResult func(t *testing.T, params domain.ListVillagesParams, res domain.ListVillagesResult)
assertResultWithRelations func(
t *testing.T,
params domain.ListVillagesParams,

View File

@ -94,7 +94,12 @@ func (p Player) ToDomainWithRelations() (domain.PlayerWithRelations, error) {
}
}
return converted.WithRelations(tribe), nil
server, err := p.Server.ToMeta()
if err != nil {
return domain.PlayerWithRelations{}, err
}
return converted.WithRelations(server, tribe), nil
}
func (p Player) ToMeta() (domain.PlayerMeta, error) {

View File

@ -8,6 +8,8 @@ import (
"github.com/uptrace/bun"
)
var ServerMetaColumns = []string{"key", "url", "open"}
type Server struct {
bun.BaseModel `bun:"table:servers,alias:server"`
Key string `bun:"key,nullzero,pk"`
@ -83,6 +85,14 @@ func (s Server) ToDomain() (domain.Server, error) {
return converted, nil
}
func (s Server) ToMeta() (domain.ServerMeta, error) {
converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open)
if err != nil {
return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err)
}
return converted, nil
}
type Servers []Server
func (ss Servers) ToDomain() (domain.Servers, error) {

View File

@ -122,6 +122,7 @@ func NewPlayer(tb TestingTB, opts ...func(cfg *PlayerConfig)) domain.Player {
}
type PlayerWithRelationsConfig struct {
ServerOptions []func(cfg *ServerConfig)
PlayerOptions []func(cfg *PlayerConfig)
TribeOptions []func(cfg *TribeConfig)
}
@ -137,6 +138,12 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
p := NewPlayer(tb, cfg.PlayerOptions...)
cfg.ServerOptions = append([]func(cfg *ServerConfig){
func(cfg *ServerConfig) {
cfg.Key = p.ServerKey()
},
}, cfg.ServerOptions...)
if p.TribeID() > 0 {
cfg.TribeOptions = append([]func(cfg *TribeConfig){
func(cfg *TribeConfig) {
@ -150,8 +157,11 @@ func NewPlayerWithRelations(tb TestingTB, opts ...func(cfg *PlayerWithRelationsC
tribe = NewTribe(tb, cfg.TribeOptions...).Meta()
}
return p.WithRelations(domain.NullTribeMeta{
V: tribe,
Valid: !tribe.IsZero(),
})
return p.WithRelations(
NewServer(tb, cfg.ServerOptions...).Meta(),
domain.NullTribeMeta{
V: tribe,
Valid: !tribe.IsZero(),
},
)
}

View File

@ -183,9 +183,10 @@ func (p Player) DeletedAt() time.Time {
return p.deletedAt
}
func (p Player) WithRelations(tribe NullTribeMeta) PlayerWithRelations {
func (p Player) WithRelations(server ServerMeta, tribe NullTribeMeta) PlayerWithRelations {
return PlayerWithRelations{
player: p,
server: server,
tribe: tribe,
}
}
@ -283,6 +284,7 @@ func (ps Players) Delete(serverKey string, active BasePlayers) ([]int, []CreateT
type PlayerWithRelations struct {
player Player
server ServerMeta
tribe NullTribeMeta
}
@ -290,6 +292,10 @@ func (p PlayerWithRelations) Player() Player {
return p.player
}
func (p PlayerWithRelations) Server() ServerMeta {
return p.server
}
func (p PlayerWithRelations) Tribe() NullTribeMeta {
return p.tribe
}
@ -804,14 +810,15 @@ func (pc PlayerCursor) Encode() string {
}
type ListPlayersParams struct {
ids []int
serverKeys []string
names []string
tribeIDs []int
deleted NullBool
sort []PlayerSort
cursor PlayerCursor
limit int
ids []int
serverKeys []string
versionCodes []string
names []string
tribeIDs []int
deleted NullBool
sort []PlayerSort
cursor PlayerCursor
limit int
}
const (
@ -871,6 +878,27 @@ func (params *ListPlayersParams) SetServerKeys(serverKeys []string) error {
return nil
}
func (params *ListPlayersParams) VersionCodes() []string {
return params.versionCodes
}
func (params *ListPlayersParams) SetVersionCodes(versionCodes []string) error {
for i, vc := range versionCodes {
if err := validateVersionCode(vc); err != nil {
return SliceElementValidationError{
Model: listPlayersParamsModelName,
Field: "versionCodes",
Index: i,
Err: err,
}
}
}
params.versionCodes = versionCodes
return nil
}
func (params *ListPlayersParams) Names() []string {
return params.names
}
@ -958,6 +986,22 @@ func (params *ListPlayersParams) SetSort(sort []PlayerSort) error {
return nil
}
func (params *ListPlayersParams) PrependSort(sort []PlayerSort) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(playerSortMaxLength-len(params.sort), 0)); err != nil {
return ValidationError{
Model: listPlayersParamsModelName,
Field: "sort",
Err: err,
}
}
return params.SetSort(append(sort, params.sort...))
}
func (params *ListPlayersParams) PrependSortString(sort []string, allowed []PlayerSort, maxLength int) error {
if len(sort) == 0 {
return nil

View File

@ -651,6 +651,60 @@ func TestListPlayersParams_SetServerKeys(t *testing.T) {
}
}
func TestListPlayersParams_SetVersionCodes(t *testing.T) {
t.Parallel()
type args struct {
versionCodes []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
versionCodes: []string{
domaintest.RandVersionCode(),
},
},
},
}
for _, versionCodeTest := range newVersionCodeValidationTests() {
tests = append(tests, test{
name: versionCodeTest.name,
args: args{
versionCodes: []string{versionCodeTest.code},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListPlayersParams",
Field: "versionCodes",
Index: 0,
Err: versionCodeTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayersParams()
require.ErrorIs(t, params.SetVersionCodes(tt.args.versionCodes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.versionCodes, params.VersionCodes())
})
}
}
func TestListPlayersParams_SetNames(t *testing.T) {
t.Parallel()
@ -896,6 +950,157 @@ func TestListPlayersParams_SetSort(t *testing.T) {
}
}
func TestListPlayersParams_PrependSort(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListPlayersParams {
t.Helper()
return domain.ListPlayersParams{}
}
type args struct {
sort []domain.PlayerSort
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListPlayersParams
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortIDASC,
domain.PlayerSortODScoreAttASC,
domain.PlayerSortODScoreDefASC,
domain.PlayerSortODScoreTotalASC,
},
},
},
{
name: "OK: custom params",
newParams: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortIDASC,
domain.PlayerSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortODScoreDefASC,
domain.PlayerSortODScoreTotalASC,
},
},
},
{
name: "OK: empty slice",
newParams: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortIDASC,
domain.PlayerSortServerKeyASC,
}))
return params
},
args: args{
sort: nil,
},
},
{
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
newParams: func(t *testing.T) domain.ListPlayersParams {
t.Helper()
params := domain.NewListPlayersParams()
require.NoError(t, params.SetSort([]domain.PlayerSort{
domain.PlayerSortIDASC,
domain.PlayerSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortODScoreAttASC,
domain.PlayerSortODScoreDefASC,
domain.PlayerSortODScoreTotalASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 2,
Current: 3,
},
},
},
{
name: "ERR: len(sort) > 4",
newParams: defaultNewParams,
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortODScoreAttASC,
domain.PlayerSortODScoreDefASC,
domain.PlayerSortODScoreTotalASC,
domain.PlayerSortODScoreTotalASC,
domain.PlayerSortODScoreTotalASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 4,
Current: 5,
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.PlayerSort{
domain.PlayerSortODScoreTotalASC,
domain.PlayerSortODScoreTotalASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayersParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.PlayerSortODScoreTotalASC.String(), domain.PlayerSortODScoreTotalASC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
expectedSort := params.Sort()
require.ErrorIs(t, params.PrependSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, append(tt.args.sort, expectedSort...), params.Sort())
})
}
}
func TestListPlayersParams_PrependSortString(t *testing.T) {
t.Parallel()

View File

@ -199,6 +199,14 @@ func (s Server) ToCursor() (ServerCursor, error) {
return NewServerCursor(s.key, s.open)
}
func (s Server) Meta() ServerMeta {
return ServerMeta{
key: s.key,
url: s.url,
open: s.open,
}
}
func (s Server) Base() BaseServer {
return BaseServer{
key: s.key,
@ -280,6 +288,51 @@ func (ss Servers) CleanUpData() ([]CleanUpDataCmdPayload, error) {
return res, nil
}
type ServerMeta struct {
key string
url *url.URL
open bool
}
const serverMetaModelName = "ServerMeta"
// UnmarshalServerMetaFromDatabase unmarshals ServerMeta from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalServerMetaFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalServerMetaFromDatabase(key string, rawURL string, open bool) (ServerMeta, error) {
if key == "" {
return ServerMeta{}, ValidationError{
Model: serverModelName,
Field: "key",
Err: ErrRequired,
}
}
u, err := parseURL(rawURL)
if err != nil {
return ServerMeta{}, ValidationError{
Model: serverMetaModelName,
Field: "url",
Err: err,
}
}
return ServerMeta{key: key, url: u, open: open}, nil
}
func (s ServerMeta) Key() string {
return s.key
}
func (s ServerMeta) URL() *url.URL {
return s.url
}
func (s ServerMeta) Open() bool {
return s.open
}
type CreateServerParams struct {
base BaseServer
versionCode string

View File

@ -29,12 +29,87 @@ var apiPlayerSortAllowedValues = []domain.PlayerSort{
}
//nolint:gocyclo
func (h *apiHTTPHandler) ListPlayers(
func (h *apiHTTPHandler) ListVersionPlayers(
w http.ResponseWriter,
r *http.Request,
versionCode apimodel.VersionCodePathParam,
params apimodel.ListVersionPlayersParams,
) {
domainParams := domain.NewListPlayersParams()
if err := domainParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(
*params.Sort,
apiPlayerSortAllowedValues,
apiPlayerSortMaxLength,
); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if err := domainParams.PrependSort([]domain.PlayerSort{domain.PlayerSortServerKeyASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
if err := domainParams.SetVersionCodes([]string{versionCode}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
if params.Name != nil {
if err := domainParams.SetNames(*params.Name); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if params.Deleted != nil {
if err := domainParams.SetDeleted(domain.NullBool{
V: *params.Deleted,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
if params.Cursor != nil {
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListPlayersErrorPath).render(w, r, err)
return
}
}
res, err := h.playerSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
h.errorRenderer.render(w, r, err)
return
}
renderJSON(w, r, http.StatusOK, apimodel.NewListPlayersWithServersResponse(res))
}
//nolint:gocyclo
func (h *apiHTTPHandler) ListServerPlayers(
w http.ResponseWriter,
r *http.Request,
_ apimodel.VersionCodePathParam,
serverKey apimodel.ServerKeyPathParam,
params apimodel.ListPlayersParams,
params apimodel.ListServerPlayersParams,
) {
domainParams := domain.NewListPlayersParams()

View File

@ -19,9 +19,754 @@ import (
"github.com/stretchr/testify/require"
)
const endpointListPlayers = "/v2/versions/%s/servers/%s/players"
const endpointListVersionPlayers = "/v2/versions/%s/players"
func TestListPlayers(t *testing.T) {
func TestListVersionPlayers(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
players := getAllPlayers(t, handler)
var version apimodel.Version
for _, p := range players {
if p.DeletedAt != nil {
version = p.Server.Version
break
}
}
require.NotZero(t, version)
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: without params",
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
return cmp.Or(
cmp.Compare(a.Server.Key, b.Server.Key),
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: limit=1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: limit=1 cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
resp := doCustomRequest(handler, req.Clone(req.Context()))
defer resp.Body.Close()
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
require.NotEmpty(t, body.Cursor.Next)
q.Set("cursor", body.Cursor.Next)
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: sort=[deletedAt:DESC,points:ASC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "deletedAt:DESC")
q.Add("sort", "points:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
var aDeletedAt time.Time
if a.DeletedAt != nil {
aDeletedAt = *a.DeletedAt
}
var bDeletedAt time.Time
if b.DeletedAt != nil {
bDeletedAt = *b.DeletedAt
}
return cmp.Or(
cmp.Compare(a.Server.Key, b.Server.Key),
aDeletedAt.Compare(bDeletedAt)*-1,
cmp.Compare(a.Points, b.Points),
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: sort=[odScoreAtt:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "odScoreAtt:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.PlayerWithServer) int {
return cmp.Or(
cmp.Compare(a.Server.Key, b.Server.Key),
cmp.Compare(a.OpponentsDefeated.ScoreAtt, b.OpponentsDefeated.ScoreAtt)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: name",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
for _, p := range players {
if p.Server.Version.Code == version.Code {
q.Add("name", p.Name)
}
if len(q["name"]) == 2 {
break
}
}
require.NotEmpty(t, q["name"])
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
names := req.URL.Query()["name"]
assert.Len(t, body.Data, len(names))
for _, p := range body.Data {
assert.True(t, slices.Contains(names, p.Name))
}
},
},
{
name: "OK: deleted=false",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("deleted", "false")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.NotZero(t, body.Data)
for _, p := range body.Data {
assert.Nil(t, p.DeletedAt)
}
},
},
{
name: "OK: deleted=true",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("deleted", "true")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListPlayersWithServersResponse](t, resp.Body)
assert.NotZero(t, body.Data)
for _, p := range body.Data {
assert.NotNil(t, p.DeletedAt)
}
},
},
{
name: "ERR: limit is not a string",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "asd")
req.URL.RawQuery = q.Encode()
},
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)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
Message: fmt.Sprintf(
"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
req.URL.Query().Get("limit"),
),
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: limit < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "0")
req.URL.RawQuery = q.Encode()
},
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)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
Min: 1,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.PlayerListMaxLimit),
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", strconv.Itoa(domain.PlayerListMaxLimit+1))
req.URL.RawQuery = q.Encode()
},
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)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
Max: domain.PlayerListMaxLimit,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", "")
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) > 1000",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(1001))
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: invalid cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(sort) > 2",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:DESC")
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "points:ASC")
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 0,
Max: 2,
Current: len(req.URL.Query()["sort"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid sort",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "test:DESC")
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.UnsupportedSortStringError{
Sort: req.URL.Query()["sort"][1],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"sort": domainErr.Sort,
},
Path: []string{"$query", "sort", "1"},
},
},
}, body)
},
},
{
name: "ERR: sort conflict",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "odScoreAtt:ASC")
q.Add("sort", "odScoreAtt:DESC")
req.URL.RawQuery = q.Encode()
},
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)
q := req.URL.Query()
domainErr := domain.SortConflictError{
Sort: [2]string{q["sort"][0], q["sort"][1]},
}
paramSort := make([]any, len(domainErr.Sort))
for i, s := range domainErr.Sort {
paramSort[i] = s
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"sort": paramSort,
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: len(name) > 100",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
for range 101 {
q.Add("name", gofakeit.LetterN(50))
}
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 0,
Max: 100,
Current: len(req.URL.Query()["name"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "name"},
},
},
}, body)
},
},
{
name: "ERR: len(name[1]) < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("name", gofakeit.LetterN(50))
q.Add("name", "")
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 150,
Current: len(req.URL.Query()["name"][1]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "name", "1"},
},
},
}, body)
},
},
{
name: "ERR: len(name[1]) > 150",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("name", gofakeit.LetterN(50))
q.Add("name", gofakeit.LetterN(151))
req.URL.RawQuery = q.Encode()
},
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)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 150,
Current: len(req.URL.Query()["name"][1]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "name", "1"},
},
},
}, body)
},
},
{
name: "ERR: deleted is not a valid boolean",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("deleted", "asd")
req.URL.RawQuery = q.Encode()
},
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)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
Message: fmt.Sprintf(
"error binding string parameter: strconv.ParseBool: parsing \"%s\": invalid syntax",
req.URL.Query().Get("deleted"),
),
Path: []string{"$query", "deleted"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(endpointListVersionPlayers, randInvalidVersionCode(t, handler))
},
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, 5)
domainErr := domain.VersionNotFoundError{
VersionCode: pathSegments[3],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"code": domainErr.VersionCode,
},
},
},
}, body)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf(endpointListVersionPlayers, version.Code),
nil,
)
if tt.reqModifier != nil {
tt.reqModifier(t, req)
}
resp := doCustomRequest(handler, req)
defer resp.Body.Close()
tt.assertResp(t, req, resp)
})
}
}
const endpointListServerPlayers = "/v2/versions/%s/servers/%s/players"
func TestListServerPlayers(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
@ -710,7 +1455,7 @@ func TestListPlayers(t *testing.T) {
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(endpointListPlayers, randInvalidVersionCode(t, handler), server.Key)
req.URL.Path = fmt.Sprintf(endpointListServerPlayers, randInvalidVersionCode(t, handler), server.Key)
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
@ -741,7 +1486,7 @@ func TestListPlayers(t *testing.T) {
name: "ERR: server not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(endpointListPlayers, server.Version.Code, domaintest.RandServerKey())
req.URL.Path = fmt.Sprintf(endpointListServerPlayers, server.Version.Code, domaintest.RandServerKey())
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
@ -776,7 +1521,7 @@ func TestListPlayers(t *testing.T) {
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf(endpointListPlayers, server.Version.Code, server.Key),
fmt.Sprintf(endpointListServerPlayers, server.Version.Code, server.Key),
nil,
)
if tt.reqModifier != nil {
@ -1546,7 +2291,7 @@ func getAllPlayers(tb testing.TB, h http.Handler) []playerWithServer {
var players []playerWithServer
for _, s := range servers {
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListPlayers, s.Version.Code, s.Key), nil)
resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListServerPlayers, s.Version.Code, s.Key), nil)
require.Equal(tb, http.StatusOK, resp.StatusCode)
for _, p := range decodeJSON[apimodel.ListPlayersResponse](tb, resp.Body).Data {

View File

@ -46,6 +46,36 @@ func NewPlayer(withRelations domain.PlayerWithRelations) Player {
return converted
}
func NewPlayerWithServer(withRelations domain.PlayerWithRelations) PlayerWithServer {
p := withRelations.Player()
converted := PlayerWithServer{
BestRank: p.BestRank(),
BestRankAt: p.BestRankAt(),
CreatedAt: p.CreatedAt(),
Id: p.ID(),
LastActivityAt: p.LastActivityAt(),
MostPoints: p.MostPoints(),
MostPointsAt: p.MostPointsAt(),
MostVillages: p.MostVillages(),
MostVillagesAt: p.MostVillagesAt(),
Name: p.Name(),
NumVillages: p.NumVillages(),
OpponentsDefeated: NewPlayerOpponentsDefeated(p.OD()),
Points: p.Points(),
ProfileUrl: p.ProfileURL().String(),
Rank: p.Rank(),
Tribe: NewNullTribeMeta(withRelations.Tribe()),
Server: NewServerMeta(withRelations.Server()),
}
if deletedAt := p.DeletedAt(); !deletedAt.IsZero() {
converted.DeletedAt = &deletedAt
}
return converted
}
func NewPlayerMeta(withRelations domain.PlayerMetaWithRelations) PlayerMeta {
p := withRelations.Player()
return PlayerMeta{
@ -89,3 +119,21 @@ func NewGetPlayerResponse(p domain.PlayerWithRelations) GetPlayerResponse {
Data: NewPlayer(p),
}
}
func NewListPlayersWithServersResponse(res domain.ListPlayersWithRelationsResult) ListPlayersWithServersResponse {
players := res.Players()
resp := ListPlayersWithServersResponse{
Data: make([]PlayerWithServer, 0, len(players)),
Cursor: Cursor{
Next: res.Next().Encode(),
Self: res.Self().Encode(),
},
}
for _, p := range players {
resp.Data = append(resp.Data, NewPlayerWithServer(p))
}
return resp
}

View File

@ -37,6 +37,14 @@ func NewServer(s domain.Server) Server {
return converted
}
func NewServerMeta(s domain.ServerMeta) ServerMeta {
return ServerMeta{
Key: s.Key(),
Open: s.Open(),
Url: s.URL().String(),
}
}
func NewListServersResponse(res domain.ListServersResult) ListServersResponse {
servers := res.Servers()