feat: tribe change - add 2 new api endpoints (#27)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#27
This commit is contained in:
Dawid Wysokiński 2024-03-13 07:23:22 +00:00
parent 4bae71da47
commit f15b17dee0
10 changed files with 2013 additions and 14 deletions

View File

@ -16,6 +16,7 @@ tags:
- name: players
- name: villages
- name: ennoblements
- name: tribe-changes
servers:
- url: "{scheme}://{hostname}/api"
variables:
@ -92,7 +93,7 @@ paths:
tags:
- versions
- servers
description: Get server config
description: Get the given server's config
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -107,7 +108,7 @@ paths:
tags:
- versions
- servers
description: Get building info
description: Get the given server's building info
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -122,7 +123,7 @@ paths:
tags:
- versions
- servers
description: Get unit info
description: Get the given server's unit info
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -215,7 +216,7 @@ paths:
- servers
- tribes
- players
description: List tribe members
description: List the given tribe's members
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -272,7 +273,7 @@ paths:
- servers
- tribes
- villages
description: List tribe villages
description: List the given tribe's villages
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -292,7 +293,7 @@ paths:
- servers
- players
- villages
description: List player villages
description: List the given player's villages
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -333,7 +334,7 @@ paths:
- servers
- players
- ennoblements
description: List player ennoblements
description: List the given player's ennoblements
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -356,7 +357,7 @@ paths:
- servers
- tribes
- ennoblements
description: List tribe ennoblements
description: List the given tribe's ennoblements
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -379,7 +380,7 @@ paths:
- servers
- villages
- ennoblements
description: List village ennoblements
description: List the given village's ennoblements
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
@ -394,6 +395,52 @@ paths:
$ref: "#/components/responses/ListEnnoblementsResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/players/{playerId}/tribe-changes:
get:
operationId: listPlayerTribeChanges
tags:
- versions
- servers
- players
- tribe-changes
description: List the given player's tribe changes
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
- $ref: "#/components/parameters/PlayerIdPathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/TribeChangeSortQueryParam"
- $ref: "#/components/parameters/SinceQueryParam"
- $ref: "#/components/parameters/BeforeQueryParam"
responses:
200:
$ref: "#/components/responses/ListTribeChangesResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/tribe-changes:
get:
operationId: listTribeTribeChanges
tags:
- versions
- servers
- tribes
- tribe-changes
description: List tribe membership changes
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
- $ref: "#/components/parameters/TribeIdPathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/TribeChangeSortQueryParam"
- $ref: "#/components/parameters/SinceQueryParam"
- $ref: "#/components/parameters/BeforeQueryParam"
responses:
200:
$ref: "#/components/responses/ListTribeChangesResponse"
default:
$ref: "#/components/responses/ErrorResponse"
components:
schemas:
Error:
@ -1364,6 +1411,22 @@ components:
createdAt:
type: string
format: date-time
TribeChange:
type: object
required:
- id
- player
- createdAt
properties:
id:
$ref: "#/components/schemas/IntId"
player:
$ref: "#/components/schemas/PlayerMeta"
newTribe:
$ref: "#/components/schemas/TribeMeta"
createdAt:
type: string
format: date-time
Cursor:
type: object
x-go-type-skip-optional-pointer: true
@ -1512,6 +1575,20 @@ components:
- createdAt:ASC
- createdAt:DESC
maxItems: 1
TribeChangeSortQueryParam:
name: sort
in: query
description: Order matters!
schema:
type: array
default:
- createdAt:ASC
items:
type: string
enum:
- createdAt:ASC
- createdAt:DESC
maxItems: 1
SinceQueryParam:
name: since
in: query
@ -1735,6 +1812,21 @@ components:
type: array
items:
$ref: "#/components/schemas/Ennoblement"
ListTribeChangesResponse:
description: ""
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginationResponse"
- type: object
required:
- data
properties:
data:
type: array
items:
$ref: "#/components/schemas/TribeChange"
ErrorResponse:
description: Default error response.
content:

View File

@ -111,12 +111,14 @@ var cmdServe = &cli.Command{
playerRepo := adapter.NewPlayerBunRepository(bunDB)
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
// services
versionSvc := app.NewVersionService(versionRepo)
serverSvc := app.NewServerService(serverRepo, nil, nil)
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
tribeChangeSvc := app.NewTribeChangeService(tribeChangeRepo)
playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil)
villageSvc := app.NewVillageService(villageRepo, nil, nil)
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
@ -148,6 +150,7 @@ var cmdServe = &cli.Command{
playerSvc,
villageSvc,
ennoblementSvc,
tribeChangeSvc,
port.WithOpenAPIConfig(oapiCfg),
))

View File

@ -5,6 +5,7 @@ import (
"fmt"
"math"
"slices"
"strings"
"time"
)
@ -263,6 +264,23 @@ const (
TribeChangeSortServerKeyDESC
)
func newTribeChangeSortFromString(s string) (TribeChangeSort, error) {
allowed := []TribeChangeSort{
TribeChangeSortCreatedAtASC,
TribeChangeSortCreatedAtDESC,
}
for _, a := range allowed {
if strings.EqualFold(a.String(), s) {
return a, nil
}
}
return 0, UnsupportedSortStringError{
Sort: s,
}
}
// IsInConflict returns true if two sorts can't be used together (e.g. TribeChangeSortIDASC and TribeChangeSortIDDESC).
func (s TribeChangeSort) IsInConflict(s2 TribeChangeSort) bool {
ss := []TribeChangeSort{s, s2}
@ -558,6 +576,37 @@ func (params *ListTribeChangesParams) SetLimit(limit int) error {
return nil
}
func (params *ListTribeChangesParams) PrependSortString(sort []string) error {
if err := validateSliceLen(
sort,
tribeChangeSortMinLength,
max(tribeChangeSortMaxLength-len(params.sort), 0),
); err != nil {
return ValidationError{
Model: listTribeChangesParamsModelName,
Field: "sort",
Err: err,
}
}
toPrepend := make([]TribeChangeSort, 0, len(sort))
for i, s := range sort {
converted, err := newTribeChangeSortFromString(s)
if err != nil {
return SliceElementValidationError{
Model: listTribeChangesParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
toPrepend = append(toPrepend, converted)
}
return params.SetSort(append(toPrepend, params.sort...))
}
type ListTribeChangesResult struct {
tribeChanges TribeChanges
self TribeChangeCursor

View File

@ -625,6 +625,143 @@ func TestListTribeChangesParams_SetSort(t *testing.T) {
}
}
func TestListTribeChangesParams_PrependSortString(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListTribeChangesParams {
t.Helper()
return domain.ListTribeChangesParams{}
}
type args struct {
sort []string
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListTribeChangesParams
args args
expectedSort []domain.TribeChangeSort
expectedErr error
}{
{
name: "OK: [createdAt:ASC]",
args: args{
sort: []string{
"createdAt:ASC",
},
},
expectedSort: []domain.TribeChangeSort{
domain.TribeChangeSortCreatedAtASC,
},
},
{
name: "OK: [createdAt:DESC]",
args: args{
sort: []string{
"createdAt:DESC",
},
},
expectedSort: []domain.TribeChangeSort{
domain.TribeChangeSortCreatedAtDESC,
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 0,
},
},
},
{
name: "ERR: custom params + len(sort) > 1",
newParams: func(t *testing.T) domain.ListTribeChangesParams {
t.Helper()
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetSort([]domain.TribeChangeSort{
domain.TribeChangeSortServerKeyASC,
domain.TribeChangeSortIDASC,
}))
return params
},
args: args{
sort: []string{
"createdAt:ASC",
"createdAt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: unsupported sort string",
newParams: defaultNewParams,
args: args{
sort: []string{
"createdAt:",
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListTribeChangesParams",
Field: "sort",
Index: 0,
Err: domain.UnsupportedSortStringError{
Sort: "createdAt:",
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"createdAt:ASC",
"createdAt:DESC",
},
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.TribeChangeSortCreatedAtASC.String(), domain.TribeChangeSortCreatedAtDESC.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)
require.ErrorIs(t, params.PrependSortString(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedSort, params.Sort())
})
}
}
func TestListTribeChangesParams_SetEncodedCursor(t *testing.T) {
t.Parallel()

View File

@ -19,6 +19,7 @@ type apiHTTPHandler struct {
playerSvc *app.PlayerService
villageSvc *app.VillageService
ennoblementSvc *app.EnnoblementService
tribeChangeSvc *app.TribeChangeService
errorRenderer apiErrorRenderer
openAPISchema func() (*openapi3.T, error)
}
@ -36,6 +37,7 @@ func NewAPIHTTPHandler(
playerSvc *app.PlayerService,
villageSvc *app.VillageService,
ennoblementSvc *app.EnnoblementService,
tribeChangeSvc *app.TribeChangeService,
opts ...APIHTTPHandlerOption,
) http.Handler {
cfg := newAPIHTTPHandlerConfig(opts...)
@ -47,6 +49,7 @@ func NewAPIHTTPHandler(
playerSvc: playerSvc,
villageSvc: villageSvc,
ennoblementSvc: ennoblementSvc,
tribeChangeSvc: tribeChangeSvc,
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
return getOpenAPISchema(cfg.openAPI)
}),

View File

@ -173,7 +173,7 @@ func TestListEnnoblements(t *testing.T) {
},
},
{
name: "OK: after",
name: "OK: before",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
@ -820,7 +820,7 @@ func TestListPlayerEnnoblements(t *testing.T) {
},
},
{
name: "OK: after",
name: "OK: before",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
@ -1520,7 +1520,7 @@ func TestListTribeEnnoblements(t *testing.T) {
},
},
{
name: "OK: after",
name: "OK: before",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
@ -2212,7 +2212,7 @@ func TestListVillageEnnoblements(t *testing.T) {
},
},
{
name: "OK: after",
name: "OK: before",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()

View File

@ -39,6 +39,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
playerRepo := adapter.NewPlayerBunRepository(bunDB)
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
return port.NewAPIHTTPHandler(
app.NewVersionService(versionRepo),
@ -47,6 +48,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
app.NewPlayerService(playerRepo, nil, nil, nil),
app.NewVillageService(villageRepo, nil, nil),
app.NewEnnoblementService(ennoblementRepo, nil, nil),
app.NewTribeChangeService(tribeChangeRepo),
cfg.options...,
)
}

View File

@ -0,0 +1,192 @@
package port
import (
"net/http"
"strconv"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
)
//nolint:gocyclo
func (h *apiHTTPHandler) ListPlayerTribeChanges(
w http.ResponseWriter,
r *http.Request,
_ apimodel.VersionCodePathParam,
serverKey apimodel.ServerKeyPathParam,
playerID apimodel.PlayerIdPathParam,
params apimodel.ListPlayerTribeChangesParams,
) {
domainParams := domain.NewListTribeChangesParams()
if err := domainParams.SetSort([]domain.TribeChangeSort{domain.TribeChangeSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if err := domainParams.SetPlayerIDs([]int{playerID}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(*params.Sort); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
} else {
if err := domainParams.PrependSortString([]string{domain.TribeChangeSortCreatedAtASC.String()}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if params.Before != nil {
if err := domainParams.SetBefore(domain.NullTime{
V: *params.Before,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Since != nil {
if err := domainParams.SetSince(domain.NullTime{
V: *params.Since,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Cursor != nil {
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
res, err := h.tribeChangeSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
h.errorRenderer.render(w, r, err)
return
}
renderJSON(w, r, http.StatusOK, apimodel.NewListTribeChangesResponse(res))
}
//nolint:gocyclo
func (h *apiHTTPHandler) ListTribeTribeChanges(
w http.ResponseWriter,
r *http.Request,
_ apimodel.VersionCodePathParam,
serverKey apimodel.ServerKeyPathParam,
tribeID apimodel.TribeIdPathParam,
params apimodel.ListTribeTribeChangesParams,
) {
domainParams := domain.NewListTribeChangesParams()
if err := domainParams.SetSort([]domain.TribeChangeSort{domain.TribeChangeSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if err := domainParams.SetTribeIDs([]int{tribeID}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(*params.Sort); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
} else {
if err := domainParams.PrependSortString([]string{domain.TribeChangeSortCreatedAtASC.String()}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
if params.Before != nil {
if err := domainParams.SetBefore(domain.NullTime{
V: *params.Before,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Since != nil {
if err := domainParams.SetSince(domain.NullTime{
V: *params.Since,
Valid: true,
}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
if params.Cursor != nil {
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListTribeChangesErrorPath).render(w, r, err)
return
}
}
res, err := h.tribeChangeSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
h.errorRenderer.render(w, r, err)
return
}
renderJSON(w, r, http.StatusOK, apimodel.NewListTribeChangesResponse(res))
}
func formatListTribeChangesErrorPath(segments []domain.ErrorPathSegment) []string {
if segments[0].Model != "ListTribeChangesParams" {
return nil
}
switch segments[0].Field {
case "cursor":
return []string{"$query", "cursor"}
case "limit":
return []string{"$query", "limit"}
case "sort":
path := []string{"$query", "sort"}
if segments[0].Index >= 0 {
path = append(path, strconv.Itoa(segments[0].Index))
}
return path
default:
return nil
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
package apimodel
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
func NewTribeChange(withRelations domain.TribeChangeWithRelations) TribeChange {
tc := withRelations.TribeChange()
return TribeChange{
CreatedAt: tc.CreatedAt(),
Id: tc.ID(),
NewTribe: NewNullTribeMeta(withRelations.NewTribe()),
Player: NewPlayerMeta(withRelations.Player().WithRelations(withRelations.OldTribe())),
}
}
func NewListTribeChangesResponse(res domain.ListTribeChangesWithRelationsResult) ListTribeChangesResponse {
tcs := res.TribeChanges()
resp := ListTribeChangesResponse{
Data: make([]TribeChange, 0, len(tcs)),
Cursor: Cursor{
Next: res.Next().Encode(),
Self: res.Self().Encode(),
},
}
for _, tc := range tcs {
resp.Data = append(resp.Data, NewTribeChange(tc))
}
return resp
}