feat: server - sync config and unit/building info (#10)

Reviewed-on: twhelp/corev3#10
This commit is contained in:
Dawid Wysokiński 2023-12-25 11:04:12 +00:00
parent 1b078c8212
commit 206eed966e
16 changed files with 476 additions and 17 deletions

View File

@ -60,6 +60,7 @@ var cmdConsumer = &cli.Command{
logger,
marshaler,
c.String(rmqFlagTopicSyncServersCmd.Name),
c.String(rmqFlagTopicServerSyncedEvent.Name),
)
consumer.Register(router)

View File

@ -39,3 +39,115 @@ func (t *TWHTTP) convertServersToDomain(servers []tw.Server) (domain.BaseServers
return res, nil
}
func (t *TWHTTP) GetServerConfig(ctx context.Context, baseURL string) (domain.ServerConfig, error) {
cfg, err := t.client.GetServerConfig(ctx, baseURL)
if err != nil {
return domain.ServerConfig{}, err
}
res, err := domain.NewServerConfig(
cfg.Speed,
cfg.UnitSpeed,
cfg.Moral,
domain.ServerConfigBuild(cfg.Build),
domain.ServerConfigMisc(cfg.Misc),
domain.ServerConfigCommands(cfg.Commands),
domain.ServerConfigNewbie(cfg.Newbie),
domain.ServerConfigGame{
BuildtimeFormula: cfg.Game.BuildtimeFormula,
Knight: cfg.Game.Knight,
KnightNewItems: cfg.Game.KnightNewItems.Int(),
Archer: cfg.Game.Archer,
Tech: cfg.Game.Tech,
FarmLimit: cfg.Game.FarmLimit,
Church: cfg.Game.Church,
Watchtower: cfg.Game.Watchtower,
Stronghold: cfg.Game.Stronghold,
FakeLimit: cfg.Game.FakeLimit,
BarbarianRise: cfg.Game.BarbarianRise,
BarbarianShrink: cfg.Game.BarbarianShrink,
BarbarianMaxPoints: cfg.Game.BarbarianMaxPoints,
Scavenging: cfg.Game.Scavenging,
Hauls: cfg.Game.Hauls,
HaulsBase: cfg.Game.HaulsBase,
HaulsMax: cfg.Game.HaulsMax,
BaseProduction: cfg.Game.BaseProduction,
Event: cfg.Game.Event,
SuppressEvents: cfg.Game.SuppressEvents,
},
domain.ServerConfigBuildings(cfg.Buildings),
domain.ServerConfigSnob(cfg.Snob),
domain.ServerConfigAlly(cfg.Ally),
domain.ServerConfigCoord(cfg.Coord),
domain.ServerConfigSitter(cfg.Sitter),
domain.ServerConfigSleep(cfg.Sleep),
domain.ServerConfigNight(cfg.Night),
domain.ServerConfigWin(cfg.Win),
)
if err != nil {
return domain.ServerConfig{}, fmt.Errorf("couldn't construct domain.ServerConfig: %w", err)
}
return res, nil
}
func (t *TWHTTP) GetUnitInfo(ctx context.Context, baseURL string) (domain.UnitInfo, error) {
info, err := t.client.GetUnitInfo(ctx, baseURL)
if err != nil {
return domain.UnitInfo{}, err
}
res, err := domain.NewUnitInfo(
domain.Unit(info.Spear),
domain.Unit(info.Sword),
domain.Unit(info.Axe),
domain.Unit(info.Archer),
domain.Unit(info.Spy),
domain.Unit(info.Light),
domain.Unit(info.Marcher),
domain.Unit(info.Heavy),
domain.Unit(info.Ram),
domain.Unit(info.Catapult),
domain.Unit(info.Knight),
domain.Unit(info.Snob),
domain.Unit(info.Militia),
)
if err != nil {
return domain.UnitInfo{}, fmt.Errorf("couldn't construct domain.UnitInfo: %w", err)
}
return res, nil
}
func (t *TWHTTP) GetBuildingInfo(ctx context.Context, baseURL string) (domain.BuildingInfo, error) {
info, err := t.client.GetBuildingInfo(ctx, baseURL)
if err != nil {
return domain.BuildingInfo{}, err
}
res, err := domain.NewBuildingInfo(
domain.Building(info.Main),
domain.Building(info.Barracks),
domain.Building(info.Stable),
domain.Building(info.Garage),
domain.Building(info.Watchtower),
domain.Building(info.Snob),
domain.Building(info.Smith),
domain.Building(info.Place),
domain.Building(info.Statue),
domain.Building(info.Market),
domain.Building(info.Wood),
domain.Building(info.Stone),
domain.Building(info.Iron),
domain.Building(info.Farm),
domain.Building(info.Storage),
domain.Building(info.Hide),
domain.Building(info.Wall),
)
if err != nil {
return domain.BuildingInfo{}, fmt.Errorf("couldn't construct domain.BuildingInfo: %w", err)
}
return res, nil
}

View File

@ -77,6 +77,49 @@ func (repo *ServerBunRepository) baseListQuery(params domain.ListServersParams)
return repo.db.NewSelect().Apply(listServersParamsApplier{params: params}.apply)
}
func (repo *ServerBunRepository) Update(ctx context.Context, key string, params domain.UpdateServerParams) error {
if params.IsZero() {
return errors.New("nothing to update")
}
res, err := repo.db.NewUpdate().
Model(&bunmodel.Server{}).
Where("key = ?", key).
Apply(updateServerParamsApplier{params: params}.apply).
Returning("").
Exec(ctx)
if err != nil {
return fmt.Errorf("couldn't update server with key %s: %w", key, err)
}
if affected, _ := res.RowsAffected(); affected == 0 {
return domain.ServerNotFoundError{
Key: key,
}
}
return nil
}
type updateServerParamsApplier struct {
params domain.UpdateServerParams
}
func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
if config := a.params.Config(); config.Valid {
q = q.Set("config = ?", bunmodel.NewServerConfig(config.Value))
}
if unitInfo := a.params.UnitInfo(); unitInfo.Valid {
q = q.Set("unit_info = ?", bunmodel.NewUnitInfo(unitInfo.Value))
}
if buildingInfo := a.params.BuildingInfo(); buildingInfo.Valid {
q = q.Set("building_info = ?", bunmodel.NewBuildingInfo(buildingInfo.Value))
}
return q
}
type listServersParamsApplier struct {
params domain.ListServersParams
}

View File

@ -18,14 +18,14 @@ import (
func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
ctx := context.Background()
t.Run("CreateOrUpdate", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := context.Background()
repos := newRepos(t)
versions, err := repos.version.List(ctx, domain.NewListVersionsParams())
@ -103,7 +103,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
repos := newRepos(t)
servers, listServersErr := repos.server.List(context.Background(), domain.NewListServersParams())
servers, listServersErr := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
randServer := servers[0]
@ -370,7 +370,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
params := tt.params(t)
res, err := repos.server.List(ctx, params)
@ -379,6 +378,74 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})
}
})
t.Run("Update", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
t.Run("OK", func(t *testing.T) {
t.Parallel()
listServersBeforeUpdateParams := domain.NewListServersParams()
require.NoError(t, listServersBeforeUpdateParams.SetLimit(1))
serversBeforeUpdate, err := repos.server.List(ctx, listServersBeforeUpdateParams)
require.NoError(t, err)
require.NotEmpty(t, serversBeforeUpdate)
var updateParams domain.UpdateServerParams
require.NoError(t, updateParams.SetConfig(domain.NullServerConfig{
Value: domaintest.NewServerConfig(t),
Valid: true,
}))
require.NoError(t, updateParams.SetUnitInfo(domain.NullUnitInfo{
Value: domaintest.NewUnitInfo(t),
Valid: true,
}))
require.NoError(t, updateParams.SetBuildingInfo(domain.NullBuildingInfo{
Value: domaintest.NewBuildingInfo(t),
Valid: true,
}))
require.NoError(t, repos.server.Update(ctx, serversBeforeUpdate[0].Key(), updateParams))
listServersAfterUpdateParams := domain.NewListServersParams()
require.NoError(t, listServersAfterUpdateParams.SetLimit(1))
require.NoError(t, listServersAfterUpdateParams.SetKeys([]string{serversBeforeUpdate[0].Key()}))
serversAfterUpdate, err := repos.server.List(ctx, listServersAfterUpdateParams)
require.NoError(t, err)
require.NotEmpty(t, serversAfterUpdate)
assert.Equal(t, updateParams.Config().Value, serversAfterUpdate[0].Config())
assert.Equal(t, updateParams.UnitInfo().Value, serversAfterUpdate[0].UnitInfo())
assert.Equal(t, updateParams.BuildingInfo().Value, serversAfterUpdate[0].BuildingInfo())
})
t.Run("ERR: not found", func(t *testing.T) {
t.Parallel()
var updateParams domain.UpdateServerParams
require.NoError(t, updateParams.SetConfig(domain.NullServerConfig{
Value: domaintest.NewServerConfig(t),
Valid: true,
}))
key := domaintest.RandServerKey()
require.ErrorIs(t, repos.server.Update(ctx, key, updateParams), domain.ServerNotFoundError{
Key: key,
})
})
t.Run("ERR: nothing to update", func(t *testing.T) {
t.Parallel()
key := domaintest.RandServerKey()
require.Error(t, repos.server.Update(ctx, key, domain.UpdateServerParams{}))
})
})
}
func randURL(tb testing.TB) *url.URL {

View File

@ -22,6 +22,7 @@ type versionRepository interface {
type serverRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error
}
type repositories struct {

View File

@ -10,6 +10,7 @@ import (
type ServerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error
}
type ServerService struct {
@ -135,3 +136,47 @@ func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServers
}
}
}
func (svc *ServerService) SyncConfigAndInfo(ctx context.Context, payload domain.ServerSyncedEventPayload) error {
url := payload.URL().String()
cfg, err := svc.twSvc.GetServerConfig(ctx, url)
if err != nil {
return fmt.Errorf("couldn't get server config for server %s: %w", payload.Key(), err)
}
buildingInfo, err := svc.twSvc.GetBuildingInfo(ctx, url)
if err != nil {
return fmt.Errorf("couldn't get building info for server %s: %w", payload.Key(), err)
}
unitInfo, err := svc.twSvc.GetUnitInfo(ctx, url)
if err != nil {
return fmt.Errorf("couldn't get unit info for server %s: %w", payload.Key(), err)
}
var updateParams domain.UpdateServerParams
if err = updateParams.SetConfig(domain.NullServerConfig{
Value: cfg,
Valid: true,
}); err != nil {
return err
}
if err = updateParams.SetBuildingInfo(domain.NullBuildingInfo{
Value: buildingInfo,
Valid: true,
}); err != nil {
return err
}
if err = updateParams.SetUnitInfo(domain.NullUnitInfo{
Value: unitInfo,
Valid: true,
}); err != nil {
return err
}
return svc.repo.Update(ctx, payload.Key(), updateParams)
}

View File

@ -8,4 +8,7 @@ import (
type TWService interface {
GetOpenServers(ctx context.Context, baseURL string) (domain.BaseServers, error)
GetServerConfig(ctx context.Context, baseURL string) (domain.ServerConfig, error)
GetUnitInfo(ctx context.Context, baseURL string) (domain.UnitInfo, error)
GetBuildingInfo(ctx context.Context, baseURL string) (domain.BuildingInfo, error)
}

View File

@ -35,6 +35,8 @@ type BuildingInfo struct {
wall Building
}
type NullBuildingInfo = NullValue[BuildingInfo]
func NewBuildingInfo(
main Building,
barracks Building,

View File

@ -0,0 +1,31 @@
package domaintest
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func NewBuildingInfo(tb TestingTB) domain.BuildingInfo {
buildingInfo, err := domain.NewBuildingInfo(
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
domain.Building{MaxLevel: gofakeit.IntRange(1, 30)},
)
require.NoError(tb, err)
return buildingInfo
}

View File

@ -54,9 +54,9 @@ func NewServer(tb TestingTB, opts ...func(cfg *ServerConfig)) domain.Server {
0,
0,
0,
domain.ServerConfig{},
domain.BuildingInfo{},
domain.UnitInfo{},
NewServerConfig(tb),
NewBuildingInfo(tb),
NewUnitInfo(tb),
time.Now(),
time.Time{},
time.Time{},

View File

@ -0,0 +1,30 @@
package domaintest
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func NewServerConfig(tb TestingTB) domain.ServerConfig {
cfg, err := domain.NewServerConfig(
gofakeit.Float64Range(1, 1000),
gofakeit.Float64Range(1, 1000),
gofakeit.IntRange(1, 20),
domain.ServerConfigBuild{},
domain.ServerConfigMisc{},
domain.ServerConfigCommands{},
domain.ServerConfigNewbie{},
domain.ServerConfigGame{},
domain.ServerConfigBuildings{},
domain.ServerConfigSnob{},
domain.ServerConfigAlly{Limit: gofakeit.IntRange(1, 20)},
domain.ServerConfigCoord{},
domain.ServerConfigSitter{},
domain.ServerConfigSleep{},
domain.ServerConfigNight{},
domain.ServerConfigWin{Check: gofakeit.IntRange(1, 20)},
)
require.NoError(tb, err)
return cfg
}

View File

@ -0,0 +1,27 @@
package domaintest
import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func NewUnitInfo(tb TestingTB) domain.UnitInfo {
unitInfo, err := domain.NewUnitInfo(
domain.Unit{Speed: gofakeit.Float64Range(1, 25)},
domain.Unit{Pop: gofakeit.IntRange(1, 3000)},
domain.Unit{BuildTime: gofakeit.Float64Range(1, 25)},
domain.Unit{Attack: gofakeit.IntRange(1, 3000)},
domain.Unit{Defense: gofakeit.IntRange(1, 3000)},
domain.Unit{Carry: gofakeit.IntRange(1, 3000)},
domain.Unit{DefenseCavalry: gofakeit.IntRange(1, 3000)},
domain.Unit{DefenseArcher: gofakeit.IntRange(1, 3000)},
domain.Unit{Speed: gofakeit.Float64Range(1, 25)},
domain.Unit{Attack: gofakeit.IntRange(1, 3000)},
domain.Unit{DefenseArcher: gofakeit.IntRange(1, 3000)},
domain.Unit{Carry: gofakeit.IntRange(1, 3000)},
domain.Unit{Pop: gofakeit.IntRange(1, 3000)},
)
require.NoError(tb, err)
return unitInfo
}

View File

@ -243,6 +243,45 @@ func (params CreateServerParams) VersionCode() string {
return params.versionCode
}
type UpdateServerParams struct {
config NullServerConfig
buildingInfo NullBuildingInfo
unitInfo NullUnitInfo
}
func (params *UpdateServerParams) Config() NullServerConfig {
return params.config
}
func (params *UpdateServerParams) SetConfig(config NullServerConfig) error {
params.config = config
return nil
}
func (params *UpdateServerParams) BuildingInfo() NullBuildingInfo {
return params.buildingInfo
}
func (params *UpdateServerParams) SetBuildingInfo(buildingInfo NullBuildingInfo) error {
params.buildingInfo = buildingInfo
return nil
}
func (params *UpdateServerParams) UnitInfo() NullUnitInfo {
return params.unitInfo
}
func (params *UpdateServerParams) SetUnitInfo(unitInfo NullUnitInfo) error {
params.unitInfo = unitInfo
return nil
}
func (params *UpdateServerParams) IsZero() bool {
return !params.config.Valid &&
!params.buildingInfo.Valid &&
!params.unitInfo.Valid
}
type ServerSort uint8
const (
@ -392,3 +431,27 @@ func (params *ListServersParams) SetOffset(offset int) error {
return nil
}
type ServerNotFoundError struct {
Key string
}
var _ ErrorWithParams = ServerNotFoundError{}
func (e ServerNotFoundError) Error() string {
return fmt.Sprintf("server with key %s not found", e.Key)
}
func (e ServerNotFoundError) Code() ErrorCode {
return ErrorCodeEntityNotFound
}
func (e ServerNotFoundError) Slug() string {
return "server-not-found"
}
func (e ServerNotFoundError) Params() map[string]any {
return map[string]any{
"Key": e.Key,
}
}

View File

@ -153,6 +153,8 @@ type ServerConfig struct {
win ServerConfigWin
}
type NullServerConfig = NullValue[ServerConfig]
func NewServerConfig(
speed float64,
unitSpeed float64,

View File

@ -27,6 +27,8 @@ type UnitInfo struct {
militia Unit
}
type NullUnitInfo = NullValue[UnitInfo]
func NewUnitInfo(
spear Unit,
sword Unit,

View File

@ -9,11 +9,12 @@ import (
)
type ServerWatermillConsumer struct {
svc *app.ServerService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
cmdSyncTopic string
svc *app.ServerService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
cmdSyncTopic string
eventServerSyncedTopic string
}
func NewServerWatermillConsumer(
@ -22,18 +23,26 @@ func NewServerWatermillConsumer(
logger watermill.LoggerAdapter,
marshaler watermillmsg.Marshaler,
cmdSyncTopic string,
eventServerSyncedTopic string,
) *ServerWatermillConsumer {
return &ServerWatermillConsumer{
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic,
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic,
eventServerSyncedTopic: eventServerSyncedTopic,
}
}
func (c *ServerWatermillConsumer) Register(router *message.Router) {
router.AddNoPublisherHandler("ServerConsumer.sync", c.cmdSyncTopic, c.subscriber, c.sync)
router.AddNoPublisherHandler(
"ServerConsumer.syncConfigAndInfo",
c.eventServerSyncedTopic,
c.subscriber,
c.syncConfigAndInfo,
)
}
func (c *ServerWatermillConsumer) sync(msg *message.Message) error {
@ -56,3 +65,24 @@ func (c *ServerWatermillConsumer) sync(msg *message.Message) error {
return c.svc.Sync(msg.Context(), payload)
}
func (c *ServerWatermillConsumer) syncConfigAndInfo(msg *message.Message) error {
var rawPayload watermillmsg.ServerSyncedEventPayload
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
payload, err := domain.NewServerSyncedEventPayload(rawPayload.Key, rawPayload.URL, rawPayload.VersionCode)
if err != nil {
c.logger.Error("couldn't construct domain.ServerSyncedEventPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
return c.svc.SyncConfigAndInfo(msg.Context(), payload)
}