feat: list villages - new param - coords
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
Dawid Wysokiński 2023-05-17 07:38:10 +02:00
parent 094f7e4d72
commit a6dbb02aed
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
29 changed files with 355 additions and 30 deletions

View File

@ -0,0 +1,34 @@
package migrations
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/core/internal/bundb/internal/model"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
if _, err := db.NewCreateIndex().
Model(&model.Village{}).
Index("villages_server_key_x_y_idx").
IfNotExists().
Column("server_key", "x", "y").
Concurrently().
Exec(ctx); err != nil {
return fmt.Errorf("couldn't create the 'villages_server_key_x_y_idx' index: %w", err)
}
return nil
}, func(ctx context.Context, db *bun.DB) error {
if _, err := db.NewDropIndex().
Model(&model.Village{}).
Index("villages_server_key_x_y_idx").
IfExists().
Concurrently().
Exec(ctx); err != nil {
return fmt.Errorf("couldn't drop the 'villages_server_key_x_y_idx' index: %w", err)
}
return nil
})
}

View File

@ -126,6 +126,14 @@ func (l listVillagesParamsApplier) applyFilters(q *bun.SelectQuery) *bun.SelectQ
q = q.Where("village.server_key IN (?)", bun.In(l.params.ServerKeys))
}
if len(l.params.Coords) > 0 {
coords := make([][]int64, 0, len(l.params.Coords))
for _, c := range l.params.Coords {
coords = append(coords, []int64{c.X, c.Y})
}
q = q.Where("(village.x, village.y) in (?)", bun.In(coords))
}
return q
}

View File

@ -247,6 +247,27 @@ func TestVillage_List_ListCountWithRelations(t *testing.T) {
},
expectedCount: 3,
},
{
name: "Coords=[{X: 533, Y: 548}, {X: 633, Y: 548}],ServerKeys=[it70]",
params: domain.ListVillagesParams{
Coords: []domain.Coords{
{X: 533, Y: 548},
{X: 633, Y: 548},
},
ServerKeys: []string{"it70"},
},
expectedVillages: []expectedVillage{
{
id: 10022,
serverKey: "it70",
},
{
id: 10023,
serverKey: "it70",
},
},
expectedCount: 2,
},
}
for _, tt := range tests {

38
internal/domain/coords.go Normal file
View File

@ -0,0 +1,38 @@
package domain
import (
"errors"
"strconv"
"strings"
)
const coordsSubstrings = 2
var ErrIncorrectCoordsFormat = errors.New("incorrect coords format, should be x|y")
type Coords struct {
X int64
Y int64
}
func NewCoords(s string) (Coords, error) {
coords := strings.SplitN(s, "|", coordsSubstrings)
if len(coords) != coordsSubstrings {
return Coords{}, ErrIncorrectCoordsFormat
}
x, err := strconv.ParseInt(coords[0], 10, 64)
if err != nil || x <= 0 {
return Coords{}, ErrIncorrectCoordsFormat
}
y, err := strconv.ParseInt(coords[1], 10, 64)
if err != nil || y <= 0 {
return Coords{}, ErrIncorrectCoordsFormat
}
return Coords{
X: x,
Y: y,
}, nil
}

View File

@ -0,0 +1,58 @@
package domain_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
)
func TestNewCoords(t *testing.T) {
t.Parallel()
tests := []struct {
coords string
expected domain.Coords
expectedErr error
}{
{
coords: "571|123",
expected: domain.Coords{
X: 571,
Y: 123,
},
},
{
coords: "-571|123",
expectedErr: domain.ErrIncorrectCoordsFormat,
},
{
coords: "571|-123",
expectedErr: domain.ErrIncorrectCoordsFormat,
},
{
coords: " 571|123",
expectedErr: domain.ErrIncorrectCoordsFormat,
},
{
coords: "571123",
expectedErr: domain.ErrIncorrectCoordsFormat,
},
{
coords: "571|123|",
expectedErr: domain.ErrIncorrectCoordsFormat,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.coords, func(t *testing.T) {
t.Parallel()
coords, err := domain.NewCoords(tt.coords)
assert.ErrorIs(t, err, tt.expectedErr)
assert.Equal(t, tt.expected, coords)
})
}
}

View File

@ -113,18 +113,18 @@ func (e MinError) Code() ErrorCode {
return ErrorCodeValidationError
}
type MaxLengthError struct {
type MaxLenError struct {
Max int
}
func (e MaxLengthError) Error() string {
func (e MaxLenError) Error() string {
return fmt.Sprintf("the length must be no more than %d", e.Max)
}
func (e MaxLengthError) UserError() string {
func (e MaxLenError) UserError() string {
return e.Error()
}
func (e MaxLengthError) Code() ErrorCode {
func (e MaxLenError) Code() ErrorCode {
return ErrorCodeValidationError
}

View File

@ -77,7 +77,7 @@ func TestMaxError(t *testing.T) {
func TestMaxLengthError(t *testing.T) {
t.Parallel()
err := domain.MaxLengthError{
err := domain.MaxLenError{
Max: 25,
}
var _ domain.Error = err

View File

@ -64,6 +64,7 @@ type CreateVillageParams struct {
type ListVillagesParams struct {
IDs []int64
ServerKeys []string
Coords []Coords
Pagination Pagination
}

View File

@ -138,3 +138,25 @@ func (q queryParams[_]) nullTime(key string) (domain.NullTime, error) {
Time: t,
}, nil
}
func (q queryParams[_]) coords(key string) ([]domain.Coords, error) {
vals := q.values[key]
if len(vals) == 0 {
return nil, nil
}
coords := make([]domain.Coords, 0, len(vals))
for i, v := range vals {
c, err := domain.NewCoords(v)
if err != nil {
return nil, domain.SliceValidationError{
Field: key,
Index: i,
Err: err,
}
}
coords = append(coords, c)
}
return coords, nil
}

View File

@ -84,7 +84,7 @@ func New(
if cfg.swagger.Enabled {
docs.SwaggerInfo.Host = cfg.swagger.Host
docs.SwaggerInfo.Schemes = cfg.swagger.Schemes
r.Get("/swagger/*", httpSwagger.Handler())
r.Get("/swagger/*", httpSwagger.Handler(httpSwagger.DocExpansion("none")))
}
r.Route("/versions", func(r chi.Router) {

View File

@ -36,6 +36,7 @@ type village struct {
// @Param serverKey path string true "Server key"
// @Param offset query int false "specifies where to start a page" minimum(0) default(0)
// @Param limit query int false "page size" minimum(1) maximum(500) default(500)
// @Param coords query []string false "village coordinates, max 50"
// @Router /versions/{versionCode}/servers/{serverKey}/villages [get]
func (v *village) list(w http.ResponseWriter, r *http.Request) {
var err error
@ -54,6 +55,12 @@ func (v *village) list(w http.ResponseWriter, r *http.Request) {
return
}
params.Coords, err = query.coords("coords")
if err != nil {
renderErr(w, err)
return
}
villages, count, err := v.svc.ListCountWithRelations(ctx, params)
if err != nil {
renderErr(w, err)

View File

@ -240,6 +240,74 @@ func TestVillage_list(t *testing.T) {
},
expectedTotalCount: "12355",
},
{
name: "OK: coords=[22|23]",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, villageSvc *mock.FakeVillageService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
villageSvc.ListCountWithRelationsCalls(func(
_ context.Context,
params domain.ListVillagesParams,
) ([]domain.VillageWithRelations, int64, error) {
expectedParams := domain.ListVillagesParams{
ServerKeys: []string{server.Key},
Coords: []domain.Coords{{X: 22, Y: 23}},
}
if diff := cmp.Diff(params, expectedParams); diff != "" {
fmt.Println(diff)
return nil, 0, fmt.Errorf("validation failed: %s", diff)
}
return []domain.VillageWithRelations{
{
Village: domain.Village{
ID: 12345,
Name: "name 2",
Points: 1,
X: 22,
Y: 23,
Continent: "K22",
Bonus: 0,
PlayerID: 0,
ProfileURL: "profile-12345",
ServerKey: server.Key,
CreatedAt: now,
},
Player: domain.NullPlayerMetaWithRelations{},
},
}, 1, nil
})
},
versionCode: version.Code,
serverKey: server.Key,
queryParams: url.Values{
"coords": []string{"22|23"},
},
expectedStatus: http.StatusOK,
target: &model.ListVillagesResp{},
expectedResponse: &model.ListVillagesResp{
Data: []model.Village{
{
ID: 12345,
Name: "name 2",
FullName: "name 2 (22|23) K22",
Points: 1,
X: 22,
Y: 23,
Continent: "K22",
Bonus: 0,
Player: model.NullPlayerMeta{
Valid: false,
Player: model.PlayerMeta{},
},
ProfileURL: "profile-12345",
CreatedAt: now,
},
},
},
expectedTotalCount: "1",
},
{
name: "ERR: limit is not a valid int32",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, villageSvc *mock.FakeVillageService) {
@ -282,6 +350,48 @@ func TestVillage_list(t *testing.T) {
},
expectedTotalCount: "",
},
{
name: "ERR: coords - incorrect format 1",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, villageSvc *mock.FakeVillageService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
serverKey: server.Key,
queryParams: url.Values{
"coords": []string{"22|-22"},
},
expectedStatus: http.StatusBadRequest,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "coords[0]: " + domain.ErrIncorrectCoordsFormat.Error(),
},
},
expectedTotalCount: "",
},
{
name: "ERR: coords - incorrect format 2",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, villageSvc *mock.FakeVillageService) {
versionSvc.GetByCodeReturns(version, nil)
serverSvc.GetNormalByVersionCodeAndKeyReturns(server, nil)
},
versionCode: version.Code,
serverKey: server.Key,
queryParams: url.Values{
"coords": []string{"22|22", "235"},
},
expectedStatus: http.StatusBadRequest,
target: &model.ErrorResp{},
expectedResponse: &model.ErrorResp{
Error: model.APIError{
Code: domain.ErrorCodeValidationError.String(),
Message: "coords[1]: " + domain.ErrIncorrectCoordsFormat.Error(),
},
},
expectedTotalCount: "",
},
{
name: "ERR: version not found",
setup: func(versionSvc *mock.FakeVersionService, serverSvc *mock.FakeServerService, villageSvc *mock.FakeVillageService) {

View File

@ -106,7 +106,7 @@ func (e *Ennoblement) ListCountWithRelations(
if len(params.Sort) > ennoblementSortMaxLen {
return nil, 0, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: ennoblementSortMaxLen,
},
}
@ -117,7 +117,7 @@ func (e *Ennoblement) ListCountWithRelations(
}
if err := validatePagination(params.Pagination, ennoblementMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
return nil, 0, err
}
ennoblements, count, err := e.repo.ListCountWithRelations(ctx, params)

View File

@ -252,7 +252,7 @@ func TestEnnoblement_ListCountWithRelations(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 1,
},
},

View File

@ -353,14 +353,14 @@ func (l listPlayersParamsBuilder) build() (domain.ListPlayersParams, error) {
if len(l.Sort) > playerSortMaxLen {
return domain.ListPlayersParams{}, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: playerSortMaxLen,
},
}
}
if err := validatePagination(l.Pagination, playerMaxLimit); err != nil {
return domain.ListPlayersParams{}, fmt.Errorf("validatePagination: %w", err)
return domain.ListPlayersParams{}, err
}
return l.ListPlayersParams, nil

View File

@ -110,14 +110,14 @@ func (p *PlayerSnapshot) ListCountWithRelations(
if len(params.Sort) > playerSnapshotSortMaxLen {
return nil, 0, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: playerSnapshotSortMaxLen,
},
}
}
if err := validatePagination(params.Pagination, playerSnapshotMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
return nil, 0, err
}
snapshots, count, err := p.repo.ListCountWithRelations(ctx, params)

View File

@ -219,7 +219,7 @@ func TestPlayerSnapshot_ListCountWithRelations(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 2,
},
},

View File

@ -790,7 +790,7 @@ func TestPlayer_List_ListCountWithRelations(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 3,
},
},

View File

@ -381,7 +381,7 @@ func (l listServersParamsBuilder) build() (domain.ListServersParams, error) {
}
if err := validatePagination(l.Pagination, serverMaxLimit); err != nil {
return domain.ListServersParams{}, fmt.Errorf("validatePagination: %w", err)
return domain.ListServersParams{}, err
}
return l.ListServersParams, nil

View File

@ -302,7 +302,7 @@ func (l listTribesParamsBuilder) build() (domain.ListTribesParams, error) {
if len(l.Tags) > tribeTagMaxLen {
return domain.ListTribesParams{}, domain.ValidationError{
Field: "tag",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: tribeTagMaxLen,
},
}
@ -311,14 +311,14 @@ func (l listTribesParamsBuilder) build() (domain.ListTribesParams, error) {
if len(l.Sort) > tribeSortMaxLen {
return domain.ListTribesParams{}, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: tribeSortMaxLen,
},
}
}
if err := validatePagination(l.Pagination, tribeMaxLimit); err != nil {
return domain.ListTribesParams{}, fmt.Errorf("validatePagination: %w", err)
return domain.ListTribesParams{}, err
}
return l.ListTribesParams, nil

View File

@ -68,14 +68,14 @@ func (t *TribeChange) ListCountWithRelations(
if len(params.Sort) > tribeChangeSortMaxLen {
return nil, 0, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: tribeChangeSortMaxLen,
},
}
}
if err := validatePagination(params.Pagination, tribeChangeMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
return nil, 0, err
}
tcs, count, err := t.repo.ListCountWithRelations(ctx, params)

View File

@ -181,7 +181,7 @@ func TestTribeChange_ListCountWithRelations(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 2,
},
},

View File

@ -109,14 +109,14 @@ func (t *TribeSnapshot) ListCount(ctx context.Context, params domain.ListTribeSn
if len(params.Sort) > tribeSnapshotSortMaxLen {
return nil, 0, domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: tribeSnapshotSortMaxLen,
},
}
}
if err := validatePagination(params.Pagination, tribeSnapshotMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
return nil, 0, err
}
snapshots, count, err := t.repo.ListCount(ctx, params)

View File

@ -228,7 +228,7 @@ func TestTribeSnapshot_ListCount(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 2,
},
},

View File

@ -486,7 +486,7 @@ func TestTribe_List_ListCount(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "sort",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 3,
},
},
@ -498,7 +498,7 @@ func TestTribe_List_ListCount(t *testing.T) {
},
expectedErr: domain.ValidationError{
Field: "tag",
Err: domain.MaxLengthError{
Err: domain.MaxLenError{
Max: 20,
},
},

View File

@ -9,8 +9,9 @@ import (
)
const (
villageChunkSize = 1000
villageMaxLimit = 500
villageChunkSize = 1000
villageMaxLimit = 500
villageCoordsMaxLen = 50
)
//counterfeiter:generate -o internal/mock/village_repository.gen.go . VillageRepository
@ -89,7 +90,16 @@ func (v *Village) ListCountWithRelations(ctx context.Context, params domain.List
}
if err := validatePagination(params.Pagination, villageMaxLimit); err != nil {
return nil, 0, fmt.Errorf("validatePagination: %w", err)
return nil, 0, err
}
if len(params.Coords) > villageCoordsMaxLen {
return nil, 0, domain.ValidationError{
Field: "coords",
Err: domain.MaxLenError{
Max: villageCoordsMaxLen,
},
}
}
villages, count, err := v.repo.ListCountWithRelations(ctx, params)

View File

@ -185,6 +185,18 @@ func TestVillage_ListCountWithRelations(t *testing.T) {
},
},
},
{
name: "ERR: len(params.Coords) > 50",
params: domain.ListVillagesParams{
Coords: make([]domain.Coords, 51),
},
expectedErr: domain.ValidationError{
Field: "coords",
Err: domain.MaxLenError{
Max: 50,
},
},
},
}
for _, tt := range tests {

View File

@ -193,6 +193,8 @@ spec:
value: "1"
- name: DB_MAX_IDLE_CONNECTIONS
value: "1"
- name: DB_READ_TIMEOUT
value: "60s"
- name: DB_DSN
valueFrom:
secretKeyRef:

View File

@ -141,6 +141,8 @@ spec:
value: "1"
- name: DB_MAX_IDLE_CONNECTIONS
value: "1"
- name: DB_READ_TIMEOUT
value: "60s"
- name: DB_DSN
valueFrom:
secretKeyRef: