feat: tribe repository (#12)

Reviewed-on: twhelp/corev3#12
This commit is contained in:
Dawid Wysokiński 2023-12-28 10:56:59 +00:00
parent b5b699ca49
commit 9668c23cc8
33 changed files with 2368 additions and 281 deletions

View File

@ -92,7 +92,7 @@ var cmdConsumer = &cli.Command{
}
consumer := port.NewTribeWatermillConsumer(
app.NewTribeService(twSvc),
app.NewTribeService(adapter.NewTribeBunRepository(db), twSvc),
subscriber,
logger,
marshaler,

9
go.mod
View File

@ -11,7 +11,6 @@ require (
github.com/go-chi/chi/v5 v5.0.10
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.3.1
github.com/gosimple/slug v1.13.1
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.8.4
github.com/uptrace/bun v1.1.16
@ -20,6 +19,7 @@ require (
github.com/uptrace/bun/dialect/sqlitedialect v1.1.16
github.com/uptrace/bun/driver/pgdriver v1.1.16
github.com/uptrace/bun/driver/sqliteshim v1.1.16
github.com/uptrace/bun/extra/bundebug v1.1.16
github.com/urfave/cli/v2 v2.26.0
)
@ -36,15 +36,16 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
@ -68,9 +69,9 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
golang.org/x/tools v0.16.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.3.0 // indirect

24
go.sum
View File

@ -42,6 +42,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elliotchance/phpserialize v1.3.3 h1:hV4QVmGdCiYgoBbw+ADt6fNgyZ2mYX0OgpnON1adTCM=
github.com/elliotchance/phpserialize v1.3.3/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@ -65,10 +67,6 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -92,6 +90,9 @@ github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW
github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
@ -159,6 +160,8 @@ github.com/uptrace/bun/driver/pgdriver v1.1.16 h1:b/NiSXk6Ldw7KLfMLbOqIkm4odHd7Q
github.com/uptrace/bun/driver/pgdriver v1.1.16/go.mod h1:Rmfbc+7lx1z/umjMyAxkOHK81LgnGj71XC5YpA6k1vU=
github.com/uptrace/bun/driver/sqliteshim v1.1.16 h1:dhXrdXQegGSM+Jk07s9p1Y575DMhCKl7wBkss8kgM+8=
github.com/uptrace/bun/driver/sqliteshim v1.1.16/go.mod h1:7fJaPqaiSvnWytQcMwVv1ZcjUJh7tGDonO/zq3O6RRI=
github.com/uptrace/bun/extra/bundebug v1.1.16 h1:SgicRQGtnjhrIhlYOxdkOm1Em4s6HykmT3JblHnoTBM=
github.com/uptrace/bun/extra/bundebug v1.1.16/go.mod h1:SkiOkfUirBiO1Htc4s5bQKEq+JSeU1TkBVpMsPz2ePM=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
@ -187,8 +190,8 @@ golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -198,8 +201,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -212,6 +215,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -223,8 +227,8 @@ golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -19,6 +19,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
bunDB.RegisterModel(
(*bunmodel.Version)(nil),
(*bunmodel.Server)(nil),
(*bunmodel.Tribe)(nil),
)
return &Fixture{
f: dbfixture.New(bunDB),

View File

@ -141,7 +141,7 @@ func (p *Postgres) NewBunDB(tb TestingTB) *bun.DB {
schema := generatePostgresSchema()
sqldb := sql.OpenDB(
sqlDB := sql.OpenDB(
pgdriver.NewConnector(
pgdriver.WithDSN(p.connectionString.String()),
pgdriver.WithConnParams(map[string]any{
@ -150,11 +150,13 @@ func (p *Postgres) NewBunDB(tb TestingTB) *bun.DB {
),
)
bunDB := bun.NewDB(sqldb, pgdialect.New())
bunDB := bun.NewDB(sqlDB, pgdialect.New())
tb.Cleanup(func() {
_ = bunDB.Close()
})
bunDB.AddQueryHook(newBunDebugHook())
bo := backoff.NewExponentialBackOff()
bo.MaxInterval = postgresPingBackOffMaxInterval
bo.MaxElapsedTime = postgresPingBackOffMaxElapsedTime

View File

@ -12,15 +12,17 @@ import (
// NewBunDBSQLite initializes a new instance of *bun.DB, which is ready for use (all required migrations are applied).
// Data is stored in memory (https://www.sqlite.org/inmemorydb.html).
func NewBunDBSQLite(tb TestingTB) *bun.DB {
sqldb, err := sql.Open(sqliteshim.ShimName, "file::memory:")
sqlDB, err := sql.Open(sqliteshim.ShimName, "file::memory:")
require.NoError(tb, err)
sqldb.SetMaxOpenConns(1)
sqldb.SetMaxIdleConns(1)
sqldb.SetConnMaxLifetime(0)
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(0)
db := bun.NewDB(sqldb, sqlitedialect.New())
bunDB := bun.NewDB(sqlDB, sqlitedialect.New())
runMigrations(tb, db)
bunDB.AddQueryHook(newBunDebugHook())
return db
runMigrations(tb, bunDB)
return bunDB
}

View File

@ -13,6 +13,7 @@ import (
"github.com/ory/dockertest/v3"
"github.com/stretchr/testify/require"
"github.com/uptrace/bun"
"github.com/uptrace/bun/extra/bundebug"
)
const migrationTimeout = 10 * time.Second
@ -55,3 +56,7 @@ func retry(bo backoff.BackOff, op backoff.Operation) error {
return nil
}
func newBunDebugHook() *bundebug.QueryHook {
return bundebug.NewQueryHook(bundebug.FromEnv("BUNDEBUG"))
}

View File

@ -0,0 +1,14 @@
package adapter
import "github.com/uptrace/bun"
func appendODSetClauses(q *bun.InsertQuery) *bun.InsertQuery {
return q.Set("rank_att = EXCLUDED.rank_att").
Set("score_att = EXCLUDED.score_att").
Set("rank_def = EXCLUDED.rank_def").
Set("score_def = EXCLUDED.score_def").
Set("rank_sup = EXCLUDED.rank_sup").
Set("score_sup = EXCLUDED.score_sup").
Set("rank_total = EXCLUDED.rank_total").
Set("score_total = EXCLUDED.score_total")
}

View File

@ -0,0 +1,48 @@
package bunmodel
import (
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type OpponentsDefeated struct {
RankAtt int `bun:"rank_att"`
ScoreAtt int `bun:"score_att"`
RankDef int `bun:"rank_def"`
ScoreDef int `bun:"score_def"`
RankSup int `bun:"rank_sup"`
ScoreSup int `bun:"score_sup"`
RankTotal int `bun:"rank_total"`
ScoreTotal int `bun:"score_total"`
}
func NewOpponentsDefeated(od domain.OpponentsDefeated) OpponentsDefeated {
return OpponentsDefeated{
RankAtt: od.RankAtt(),
ScoreAtt: od.ScoreAtt(),
RankDef: od.RankDef(),
ScoreDef: od.ScoreDef(),
RankSup: od.RankSup(),
ScoreSup: od.ScoreSup(),
RankTotal: od.RankTotal(),
ScoreTotal: od.ScoreTotal(),
}
}
func (od OpponentsDefeated) ToDomain() (domain.OpponentsDefeated, error) {
converted, err := domain.NewOpponentsDefeated(
od.RankAtt,
od.ScoreAtt,
od.RankDef,
od.ScoreDef,
od.RankSup,
od.ScoreSup,
od.RankTotal,
od.ScoreTotal,
)
if err != nil {
return domain.OpponentsDefeated{}, fmt.Errorf("couldn't construct domain.OpponentsDefeated: %w", err)
}
return converted, nil
}

View File

@ -0,0 +1,87 @@
package bunmodel
import (
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
)
type Tribe struct {
bun.BaseModel `bun:"table:tribes,alias:tribe"`
ID int `bun:"id,nullzero,pk"`
ServerKey string `bun:"server_key,nullzero,pk"`
Name string `bun:"name,nullzero"`
Tag string `bun:"tag,nullzero"`
NumMembers int `bun:"num_members"`
NumVillages int `bun:"num_villages"`
Points int `bun:"points"`
AllPoints int `bun:"all_points"`
Rank int `bun:"rank"`
Dominance float64 `bun:"dominance"`
ProfileURL string `bun:"profile_url,nullzero"`
BestRank int `bun:"best_rank,nullzero,default:999999"`
BestRankAt time.Time `bun:"best_rank_at,nullzero,default:CURRENT_TIMESTAMP"`
MostPoints int `bun:"most_points"`
MostPointsAt time.Time `bun:"most_points_at,nullzero,default:CURRENT_TIMESTAMP"`
MostVillages int `bun:"most_villages"`
MostVillagesAt time.Time `bun:"most_villages_at,nullzero,default:CURRENT_TIMESTAMP"`
CreatedAt time.Time `bun:"created_at,nullzero,default:CURRENT_TIMESTAMP"`
DeletedAt time.Time `bun:"deleted_at,nullzero"`
OpponentsDefeated
}
func (t Tribe) ToDomain() (domain.Tribe, error) {
od, err := t.OpponentsDefeated.ToDomain()
if err != nil {
return domain.Tribe{}, fmt.Errorf("couldn't construct domain.Tribe (id=%d,serverKey=%s): %w", t.ID, t.ServerKey, err)
}
converted, err := domain.UnmarshalTribeFromDatabase(
t.ID,
t.ServerKey,
t.Name,
t.Tag,
t.NumMembers,
t.NumVillages,
t.Points,
t.AllPoints,
t.Rank,
od,
t.ProfileURL,
t.Dominance,
t.BestRank,
t.BestRankAt,
t.MostPoints,
t.MostPointsAt,
t.MostVillages,
t.MostVillagesAt,
t.CreatedAt,
t.DeletedAt,
)
if err != nil {
return domain.Tribe{}, fmt.Errorf("couldn't construct domain.Tribe (id=%d,serverKey=%s): %w", t.ID, t.ServerKey, err)
}
return converted, nil
}
type Tribes []Tribe
func (ts Tribes) ToDomain() (domain.Tribes, error) {
res := make(domain.Tribes, 0, len(ts))
for _, t := range ts {
converted, err := t.ToDomain()
if err != nil {
return nil, err
}
res = append(res, converted)
}
return res, nil
}

View File

@ -146,8 +146,6 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("server.special = ?", special.Value)
}
q = q.Limit(a.params.Limit()).Offset(a.params.Offset())
for _, s := range a.params.Sort() {
switch s {
case domain.ServerSortKeyASC:
@ -163,5 +161,5 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
}
}
return q
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
}

View File

@ -0,0 +1,167 @@
package adapter
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/internal/bunmodel"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
type TribeBunRepository struct {
db bun.IDB
}
func NewTribeBunRepository(db bun.IDB) *TribeBunRepository {
return &TribeBunRepository{db: db}
}
func (repo *TribeBunRepository) CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error {
if len(params) == 0 {
return nil
}
tribes := make(bunmodel.Tribes, 0, len(params))
for _, p := range params {
base := p.Base()
tribes = append(tribes, bunmodel.Tribe{
ID: base.ID(),
ServerKey: p.ServerKey(),
Name: base.Name(),
Tag: base.Tag(),
NumMembers: base.NumMembers(),
NumVillages: base.NumVillages(),
Points: base.Points(),
AllPoints: base.AllPoints(),
Rank: base.Rank(),
ProfileURL: base.ProfileURL().String(),
BestRank: p.BestRank(),
BestRankAt: p.BestRankAt(),
MostPoints: p.MostPoints(),
MostPointsAt: p.MostPointsAt(),
MostVillages: p.MostVillages(),
MostVillagesAt: p.MostVillagesAt(),
CreatedAt: time.Now(),
OpponentsDefeated: bunmodel.NewOpponentsDefeated(base.OD()),
})
}
q := repo.db.NewInsert().
Model(&tribes)
//nolint:exhaustive
switch q.Dialect().Name() {
case dialect.PG:
q = q.On("CONFLICT ON CONSTRAINT tribes_pkey DO UPDATE")
case dialect.SQLite:
q = q.On("CONFLICT(id, server_key) DO UPDATE")
default:
q = q.Err(errors.New("unsupported dialect"))
}
if _, err := q.
Set("name = EXCLUDED.name").
Set("tag = EXCLUDED.tag").
Set("num_members = EXCLUDED.num_members").
Set("num_villages = EXCLUDED.num_villages").
Set("points = EXCLUDED.points").
Set("all_points = EXCLUDED.all_points").
Set("rank = EXCLUDED.rank").
Set("profile_url = EXCLUDED.profile_url").
Set("best_rank = EXCLUDED.best_rank").
Set("best_rank_at = EXCLUDED.best_rank_at").
Set("most_villages = EXCLUDED.most_villages").
Set("most_villages_at = EXCLUDED.most_villages_at").
Set("most_points = EXCLUDED.most_points").
Set("most_points_at = EXCLUDED.most_points_at").
Set("deleted_at = EXCLUDED.deleted_at").
Apply(appendODSetClauses).
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("something went wrong while inserting tribes into the db: %w", err)
}
return nil
}
func (repo *TribeBunRepository) List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error) {
var servers bunmodel.Tribes
if err := repo.db.NewSelect().
Model(&servers).
Apply(listTribesParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select tribes from the db: %w", err)
}
return servers.ToDomain()
}
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
if len(ids) == 0 {
return nil
}
if _, err := repo.db.NewUpdate().
Model((*bunmodel.Tribe)(nil)).
Where("deleted_at IS NULL").
Where("id IN (?)", bun.In(ids)).
Where("server_key = ?", serverKey).
Set("deleted_at = ?", time.Now()).
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("couldn't delete tribes: %w", err)
}
return nil
}
type listTribesParamsApplier struct {
params domain.ListTribesParams
}
//nolint:gocyclo
func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if ids := a.params.IDs(); len(ids) > 0 {
q = q.Where("tribe.id IN (?)", bun.In(ids))
}
if idGT := a.params.IDGT(); idGT.Valid {
q = q.Where("tribe.id > ?", idGT.Value)
}
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("tribe.server_key IN (?)", bun.In(serverKeys))
}
if deleted := a.params.Deleted(); deleted.Valid {
if deleted.Value {
q = q.Where("tribe.deleted_at IS NOT NULL")
} else {
q = q.Where("tribe.deleted_at IS NULL")
}
}
for _, s := range a.params.Sort() {
switch s {
case domain.TribeSortIDASC:
q = q.Order("tribe.id ASC")
case domain.TribeSortIDDESC:
q = q.Order("tribe.id DESC")
case domain.TribeSortServerKeyASC:
q = q.Order("tribe.server_key ASC")
case domain.TribeSortServerKeyDESC:
q = q.Order("tribe.server_key DESC")
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
}

View File

@ -0,0 +1,29 @@
package adapter_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter/adaptertest"
)
func TestTribeBunRepository_Postgres(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
testTribeRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, postgres.NewBunDB(t))
})
}
func TestTribeBunRepository_SQLite(t *testing.T) {
t.Parallel()
testTribeRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, adaptertest.NewBunDBSQLite(t))
})
}

View File

@ -4,13 +4,11 @@ import (
"cmp"
"context"
"fmt"
"net/url"
"slices"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -23,11 +21,39 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("CreateOrUpdate", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
assertCreatedUpdated := func(t *testing.T, params []domain.CreateServerParams) {
t.Helper()
require.NotEmpty(t, params)
keys := make([]string, 0, len(params))
for _, p := range params {
keys = append(keys, p.Base().Key())
}
listParams := domain.NewListServersParams()
require.NoError(t, listParams.SetKeys(keys))
servers, err := repos.server.List(ctx, listParams)
require.NoError(t, err)
require.Len(t, servers, len(params))
for i, p := range params {
idx := slices.IndexFunc(servers, func(server domain.Server) bool {
return server.Key() == p.Base().Key()
})
require.GreaterOrEqualf(t, idx, 0, "params[%d]", i)
server := servers[idx]
assert.Equalf(t, p.Base(), server.Base(), "params[%d]", i)
assert.Equalf(t, p.VersionCode(), server.VersionCode(), "params[%d]", i)
}
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
versions, err := repos.version.List(ctx, domain.NewListVersionsParams())
require.NoError(t, err)
require.NotEmpty(t, versions)
@ -42,31 +68,11 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, err)
require.NoError(t, repos.server.CreateOrUpdate(ctx, createParams...))
keys := make([]string, 0, len(serversToCreate))
for _, s := range serversToCreate {
keys = append(keys, s.Key())
}
listCreatedServersParams := domain.NewListServersParams()
require.NoError(t, listCreatedServersParams.SetKeys(keys))
createdServers, err := repos.server.List(ctx, listCreatedServersParams)
require.NoError(t, err)
require.Len(t, createdServers, len(serversToCreate))
for _, base := range serversToCreate {
assert.True(t, slices.ContainsFunc(createdServers, func(server domain.Server) bool {
return server.Key() == base.Key() &&
server.Open() == base.Open() &&
server.URL().String() == base.URL().String() &&
server.VersionCode() == version.Code()
}))
}
assertCreatedUpdated(t, createParams)
serversToUpdate := domain.BaseServers{
domaintest.NewBaseServer(t, func(cfg *domaintest.BaseServerConfig) {
cfg.Key = serversToCreate[0].Key()
cfg.URL = randURL(t)
cfg.Open = !serversToCreate[0].Open()
}),
}
@ -75,26 +81,13 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, err)
require.NoError(t, repos.server.CreateOrUpdate(ctx, updateParams...))
assertCreatedUpdated(t, updateParams)
})
keys = make([]string, 0, len(serversToUpdate))
for _, s := range serversToUpdate {
keys = append(keys, s.Key())
}
t.Run("OK: len(params) == 0", func(t *testing.T) {
t.Parallel()
listUpdatedServersParams := domain.NewListServersParams()
require.NoError(t, listUpdatedServersParams.SetKeys(keys))
updatedServers, err := repos.server.List(ctx, listUpdatedServersParams)
require.NoError(t, err)
require.Len(t, updatedServers, len(serversToUpdate))
for _, base := range serversToUpdate {
assert.True(t, slices.ContainsFunc(updatedServers, func(server domain.Server) bool {
return server.Key() == base.Key() &&
server.Open() == base.Open() &&
server.URL().String() == base.URL().String() &&
server.VersionCode() == version.Code()
}))
}
require.NoError(t, repos.server.CreateOrUpdate(ctx))
})
})
@ -447,10 +440,3 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
})
})
}
func randURL(tb testing.TB) *url.URL {
tb.Helper()
u, err := url.Parse("https://" + gofakeit.DomainName())
require.NoError(tb, err)
return u
}

View File

@ -25,9 +25,16 @@ type serverRepository interface {
Update(ctx context.Context, key string, params domain.UpdateServerParams) error
}
type tribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}
type repositories struct {
version versionRepository
server serverRepository
tribe tribeRepository
}
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
@ -38,5 +45,6 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
return repositories{
version: adapter.NewVersionBunRepository(bunDB),
server: adapter.NewServerBunRepository(bunDB),
tribe: adapter.NewTribeBunRepository(bunDB),
}
}

View File

@ -0,0 +1,390 @@
package adapter_test
import (
"cmp"
"context"
"fmt"
"math"
"slices"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
ctx := context.Background()
t.Run("CreateOrUpdate", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
assertCreatedUpdated := func(t *testing.T, params []domain.CreateTribeParams) {
t.Helper()
require.NotEmpty(t, params)
ids := make([]int, 0, len(params))
for _, p := range params {
ids = append(ids, p.Base().ID())
}
listParams := domain.NewListTribesParams()
require.NoError(t, listParams.SetIDs(ids))
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
tribes, err := repos.tribe.List(ctx, listParams)
require.NoError(t, err)
assert.Len(t, tribes, len(params))
for i, p := range params {
idx := slices.IndexFunc(tribes, func(tribe domain.Tribe) bool {
return tribe.ID() == p.Base().ID() && tribe.ServerKey() == p.ServerKey()
})
require.GreaterOrEqualf(t, idx, 0, "params[%d]", i)
tribe := tribes[idx]
assert.Equalf(t, p.Base(), tribe.Base(), "params[%d]", i)
assert.Equalf(t, p.ServerKey(), tribe.ServerKey(), "params[%d]", i)
assert.Equalf(t, p.BestRank(), tribe.BestRank(), "params[%d]", i)
assert.WithinDurationf(t, p.BestRankAt(), tribe.BestRankAt(), time.Minute, "params[%d]", i)
assert.Equalf(t, p.MostVillages(), tribe.MostVillages(), "params[%d]", i)
assert.WithinDurationf(t, p.MostVillagesAt(), tribe.MostVillagesAt(), time.Minute, "params[%d]", i)
assert.Equalf(t, p.MostPoints(), tribe.MostPoints(), "params[%d]", i)
assert.WithinDurationf(t, p.MostPointsAt(), tribe.MostPointsAt(), time.Minute, "params[%d]", i)
}
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err)
require.NotEmpty(t, servers)
server := servers[0]
tribesToCreate := domain.BaseTribes{
domaintest.NewBaseTribe(t),
domaintest.NewBaseTribe(t),
}
createParams, err := domain.NewCreateTribeParams(server.Key(), tribesToCreate, nil)
require.NoError(t, err)
require.NoError(t, repos.tribe.CreateOrUpdate(ctx, createParams...))
assertCreatedUpdated(t, createParams)
tribesToUpdate := domain.BaseTribes{
domaintest.NewBaseTribe(t, func(cfg *domaintest.BaseTribeConfig) {
cfg.ID = tribesToCreate[0].ID()
}),
}
updateParams, err := domain.NewCreateTribeParams(server.Key(), tribesToUpdate, nil)
require.NoError(t, err)
require.NoError(t, repos.tribe.CreateOrUpdate(ctx, updateParams...))
assertCreatedUpdated(t, updateParams)
})
t.Run("OK: len(params) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.tribe.CreateOrUpdate(ctx))
})
})
t.Run("List & ListCount", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tribes, listTribesErr := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, listTribesErr)
require.NotEmpty(t, tribes)
randTribe := tribes[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListTribesParams
assertTribes func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListTribesParams, total int)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
return domain.NewListTribesParams()
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
if x := cmp.Compare(a.ServerKey(), b.ServerKey()); x != 0 {
return x
}
return cmp.Compare(a.ID(), b.ID())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{domain.TribeSortServerKeyDESC, domain.TribeSortIDDESC}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
if x := cmp.Compare(a.ServerKey(), b.ServerKey()) * -1; x != 0 {
return x
}
return cmp.Compare(a.ID(), b.ID()) * -1
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randTribe.ID(), randTribe.ServerKey()),
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetIDs([]int{randTribe.ID()}))
require.NoError(t, params.SetServerKeys([]string{randTribe.ServerKey()}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
ids := params.IDs()
serverKeys := params.ServerKeys()
for _, tr := range tribes {
assert.True(t, slices.Contains(ids, tr.ID()))
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: idGT=%d", randTribe.ID()),
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetIDGT(domain.NullInt{
Value: randTribe.ID(),
Valid: true,
}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, tribes)
for _, tr := range tribes {
assert.Greater(t, tr.ID(), params.IDGT().Value, tr.ID())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: deleted=true",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetDeleted(domain.NullBool{
Value: true,
Valid: true,
}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, tribes)
for _, s := range tribes {
assert.True(t, s.IsDeleted())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: deleted=false",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetDeleted(domain.NullBool{
Value: false,
Valid: true,
}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, tribes)
for _, s := range tribes {
assert.False(t, s.IsDeleted())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.Len(t, tribes, params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := tt.params(t)
res, err := repos.tribe.List(ctx, params)
tt.assertError(t, err)
tt.assertTribes(t, params, res)
})
}
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
t.Run("OK", func(t *testing.T) {
t.Parallel()
serverKeys := make([]string, 0, len(servers))
for _, s := range servers {
serverKeys = append(serverKeys, s.Key())
}
listTribesParams := domain.NewListTribesParams()
require.NoError(t, listTribesParams.SetDeleted(domain.NullBool{Value: false, Valid: true}))
require.NoError(t, listTribesParams.SetServerKeys(serverKeys))
tribes, err := repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
var serverKey string
var ids []int
for _, tr := range tribes {
if serverKey == "" {
serverKey = tr.ServerKey()
}
if tr.ServerKey() == serverKey {
ids = append(ids, tr.ID())
}
}
idsToDelete := ids[:int(math.Ceil(float64(len(ids))/2))]
require.NoError(t, repos.tribe.Delete(ctx, serverKey, idsToDelete...))
listTribesParams = domain.NewListTribesParams()
require.NoError(t, listTribesParams.SetDeleted(domain.NullBool{Valid: false}))
require.NoError(t, listTribesParams.SetServerKeys(serverKeys))
tribes, err = repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
for _, tr := range tribes {
if tr.ServerKey() == serverKey && slices.Contains(ids, tr.ID()) {
if slices.Contains(idsToDelete, tr.ID()) {
assert.WithinDuration(t, time.Now(), tr.DeletedAt(), time.Minute)
} else {
assert.Zero(t, tr.DeletedAt())
}
continue
}
// ensure that no other tribe is removed
assert.WithinRange(t, tr.DeletedAt(), time.Time{}, time.Now().Add(-time.Minute))
}
})
t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.tribe.Delete(ctx, servers[0].Key()))
})
})
}

View File

@ -14,6 +14,8 @@ import (
func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
ctx := context.Background()
t.Run("List & ListCount", func(t *testing.T) {
t.Parallel()
@ -80,7 +82,6 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
params := tt.params(t)
res, err := repos.version.List(ctx, params)

View File

@ -76,3 +76,427 @@
village_data_updated_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z
version_code: pl
- model: Tribe
rows:
- rank_att: 1
score_att: 730488339
rank_def: 1
score_def: 303300128
rank_sup: 0
score_sup: 0
rank_total: 1
score_total: 1033788467
_id: de188-obott
id: 772
server_key: de188
name: Obdachlose Otter
tag: ObOtt
num_members: 20
num_villages: 8419
points: 86151161
all_points: 86151161
rank: 1
dominance: 58.367997781475324
created_at: 2021-03-07T11:00:51.000Z
profile_url: https://de188.die-staemme.de/game.php?screen=info_ally&id=772
- rank_att: 4
score_att: 84537872
rank_def: 2
score_def: 233565453
rank_sup: 0
score_sup: 0
rank_total: 2
score_total: 318103325
_id: de188-clap
id: 950
server_key: de188
name: Klatschn muss et
tag: Clap!
num_members: 4
num_villages: 333
points: 3274015
all_points: 3274015
rank: 5
dominance: 2.3086522462562398
created_at: 2021-03-16T01:00:54.000Z
profile_url: https://de188.die-staemme.de/game.php?screen=info_ally&id=950
- rank_att: 2
score_att: 91976316
rank_def: 21
score_def: 15324882
rank_sup: 0
score_sup: 0
rank_total: 6
score_total: 107301198
_id: de188-rb
id: 1307
server_key: de188
name: Rasselbande
tag: -RB-
num_members: 6
num_villages: 1509
points: 14729501
all_points: 14729501
rank: 2
dominance: 10.461730449251249
created_at: 2021-04-17T09:01:01.000Z
profile_url: https://de188.die-staemme.de/game.php?screen=info_ally&id=1307
- rank_att: 3
score_att: 87034216
rank_def: 3
score_def: 228160403
rank_sup: 0
score_sup: 0
rank_total: 3
score_total: 315194619
_id: de188-lost
id: 97
server_key: de188
name: Fühl ich nicht.
tag: LOST
num_members: 2
num_villages: 274
points: 2541853
all_points: 2541853
rank: 7
dominance: 1.8996117581808099
created_at: 2021-02-26T15:00:43.000Z
profile_url: https://de188.die-staemme.de/game.php?screen=info_ally&id=97
- rank_att: 5
score_att: 81487046
rank_def: 5
score_def: 67353196
rank_sup: 0
score_sup: 0
rank_total: 4
score_total: 148840242
_id: de188-shnauz
id: 64
server_key: de188
name: aufshnauz
tag: shnauz
num_members: 2
num_villages: 351
points: 3431912
all_points: 3431912
rank: 4
dominance: 2.4334442595673877
created_at: 2021-02-24T22:00:55.000Z
profile_url: https://de188.die-staemme.de/game.php?screen=info_ally&id=64
- rank_att: 1
score_att: 1449031872
rank_def: 1
score_def: 993299748
rank_sup: 0
score_sup: 0
rank_total: 1
score_total: 2442331620
_id: en113-kekw
id: 122
server_key: en113
name: Identity Crisis
tag: KEKW
num_members: 20
num_villages: 22568
points: 266237628
all_points: 266237628
rank: 1
dominance: 54.27478896611433
created_at: 2020-06-22T13:45:46.000Z
profile_url: https://en113.tribalwars.net/game.php?screen=info_ally&id=122
- rank_att: 2
score_att: 508057735
rank_def: 2
score_def: 742858106
rank_sup: 0
score_sup: 0
rank_total: 2
score_total: 1250915841
_id: en113-pear
id: 112
server_key: en113
name: The Pear Bears
tag: Pear
num_members: 6
num_villages: 804
points: 7407640
all_points: 7407640
rank: 7
dominance: 1.9335754310863134
created_at: 2020-06-22T13:45:46.000Z
profile_url: https://en113.tribalwars.net/game.php?screen=info_ally&id=112
- rank_att: 3
score_att: 321486919
rank_def: 2
score_def: 531735322
rank_sup: 0
score_sup: 0
rank_total: 3
score_total: 853222241
_id: en113-virus
id: 1337
server_key: en113
name: RIM VIRUS
tag: VIRUS
num_members: 4
num_villages: 34
points: 268858
all_points: 268858
rank: 30
dominance: 0
created_at: 2020-06-22T13:45:46.000Z
deleted_at: 2020-12-03T18:01:07Z
profile_url: https://en113.tribalwars.net/game.php?screen=info_ally&id=1337
- rank_att: 3
score_att: 231146512
rank_def: 5
score_def: 107383183
rank_sup: 0
score_sup: 0
rank_total: 3
score_total: 338529695
_id: en113-rule
id: 2544
server_key: en113
name: One Rule to Rule Them All
tag: RULE
num_members: 11
num_villages: 4529
points: 47701113
all_points: 47701113
rank: 2
dominance: 10.891993939539693
created_at: 2020-06-22T13:45:46.000Z
profile_url: https://en113.tribalwars.net/game.php?screen=info_ally&id=2544
- rank_att: 5
score_att: 63878040
rank_def: 3
score_def: 132316341
rank_sup: 0
score_sup: 0
rank_total: 5
score_total: 196194381
_id: en113-pq
id: 1115
server_key: en113
name: Promising Quest in your area
tag: ~PQ~
num_members: 1
num_villages: 23
points: 72446
all_points: 72446
rank: 40
dominance: 0
created_at: 2020-06-22T13:45:46.000Z
deleted_at: 2020-12-07T04:00:56Z
profile_url: https://en113.tribalwars.net/game.php?screen=info_ally&id=1115
- rank_att: 1
score_att: 132897
rank_def: 3
score_def: 60776
rank_sup: 0
score_sup: 0
rank_total: 1
score_total: 193673
_id: it70-tww
id: 5
server_key: it70
name: The White Wolves
tag: TWW
num_members: 25
num_villages: 65
points: 182130
all_points: 182130
rank: 1
dominance: 3.026070763500931
created_at: 2022-02-21T20:00:07.000Z
profile_url: https://it70.tribals.it/game.php?screen=info_ally&id=5
- rank_att: 2
score_att: 73109
rank_def: 6
score_def: 29374
rank_sup: 0
score_sup: 0
rank_total: 4
score_total: 102483
_id: it70-tbw
id: 30
server_key: it70
name: The Black Wolves
tag: TBW
num_members: 25
num_villages: 57
points: 155593
all_points: 155593
rank: 2
dominance: 2.653631284916201
created_at: 2022-02-24T12:00:05.000Z
profile_url: https://it70.tribals.it/game.php?screen=info_ally&id=30
- rank_att: 4
score_att: 48518
rank_def: 16
score_def: 7674
rank_sup: 0
score_sup: 0
rank_total: 7
score_total: 56192
_id: it70-tprm
id: 1
server_key: it70
name: The Pokémon Red Machine
tag: TPRM
num_members: 23
num_villages: 72
points: 147717
all_points: 147717
rank: 3
dominance: 3.35195530726257
created_at: 2022-02-21T19:00:08.000Z
profile_url: https://it70.tribals.it/game.php?screen=info_ally&id=1
- rank_att: 5
score_att: 47159
rank_def: 1
score_def: 88401
rank_sup: 0
score_sup: 0
rank_total: 2
score_total: 135560
_id: it70-tbrm
id: 3
server_key: it70
name: The Big Red Machine
tag: TBRM
num_members: 23
num_villages: 56
points: 142377
all_points: 142377
rank: 4
dominance: 2.60707635009311
created_at: 2022-02-21T20:00:07.000Z
profile_url: https://it70.tribals.it/game.php?screen=info_ally&id=3
- rank_att: 6
score_att: 46008
rank_def: 2
score_def: 74025
rank_sup: 0
score_sup: 0
rank_total: 3
score_total: 120033
_id: it70-tsrm
id: 31
server_key: it70
name: The Small Red Machine
tag: TSRM
num_members: 25
num_villages: 46
points: 105512
all_points: 105512
rank: 5
dominance: 2.1415270018621975
created_at: 2022-02-24T12:00:05.000Z
profile_url: https://it70.tribals.it/game.php?screen=info_ally&id=31
- rank_att: 1
score_att: 669292167
rank_def: 5
score_def: 199124128
rank_sup: 0
score_sup: 0
rank_total: 2
score_total: 868416295
_id: pl169-csa
id: 28
server_key: pl169
name: Konfederacja
tag: CSA.
num_members: 66
num_villages: 14480
points: 114277979
all_points: 143350058
rank: 1
dominance: 29.76912481240106
created_at: 2021-09-02T23:01:13.000Z
profile_url: https://pl169.plemiona.pl/game.php?screen=info_ally&id=28
- rank_att: 3
score_att: 358150686
rank_def: 3
score_def: 331975367
rank_sup: 0
score_sup: 0
rank_total: 4
score_total: 690126053
_id: pl169-csa-jr
id: 2
server_key: pl169
name: KONFEDERACJA JUNIOR
tag: CSA.JR
num_members: 53
num_villages: 10752
points: 97684589
all_points: 105576587
rank: 2
dominance: 22.10480870047902
created_at: 2021-09-02T18:01:08.000Z
profile_url: https://pl169.plemiona.pl/game.php?screen=info_ally&id=2
- rank_att: 2
score_att: 513902873
rank_def: 6
score_def: 177660231
rank_sup: 0
score_sup: 0
rank_total: 3
score_total: 691563104
_id: pl169-kuzyni
id: 27
server_key: pl169
name: Spoceni Kuzyni
tag: KUZYNI
num_members: 52
num_villages: 10152
points: 91951132
all_points: 97812820
rank: 3
dominance: 20.87128142924693
created_at: 2021-09-02T22:00:37.000Z
profile_url: https://pl169.plemiona.pl/game.php?screen=info_ally&id=27
- rank_att: 4
score_att: 228636414
rank_def: 1
score_def: 865423891
rank_sup: 0
score_sup: 0
rank_total: 1
score_total: 1094060305
_id: pl169-lpk
id: 1
server_key: pl169
name: Łapanka
tag: ŁPK
num_members: 61
num_villages: 3471
points: 28417793
all_points: 30608034
rank: 4
dominance: 7.13595526407763
created_at: 2021-09-02T18:01:08.000Z
profile_url: https://pl169.plemiona.pl/game.php?screen=info_ally&id=1
- rank_att: 5
score_att: 163168564
rank_def: 13
score_def: 60229489
rank_sup: 0
score_sup: 0
rank_total: 7
score_total: 223398053
_id: pl169-tt
id: 122
server_key: pl169
name: TIP TOP
tag: TT.
num_members: 23
num_villages: 908
points: 8188040
all_points: 8188040
rank: 6
dominance: 1.8667379371312267
created_at: 2021-09-08T02:00:58.000Z
profile_url: https://pl169.plemiona.pl/game.php?screen=info_ally&id=122

View File

@ -7,12 +7,19 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type TribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}
type TribeService struct {
repo TribeRepository
twSvc TWService
}
func NewTribeService(twSvc TWService) *TribeService {
return &TribeService{twSvc: twSvc}
func NewTribeService(repo TribeRepository, twSvc TWService) *TribeService {
return &TribeService{repo: repo, twSvc: twSvc}
}
func (svc *TribeService) Sync(ctx context.Context, payload domain.ServerSyncedEventPayload) error {

View File

@ -26,7 +26,7 @@ func NewBaseServer(key string, u *url.URL, open bool) (BaseServer, error) {
return BaseServer{}, ValidationError{
Model: baseServerModelName,
Field: "url",
Err: ErrNotNil,
Err: ErrNil,
}
}
@ -37,20 +37,20 @@ func NewBaseServer(key string, u *url.URL, open bool) (BaseServer, error) {
}, nil
}
func (b BaseServer) Key() string {
return b.key
func (s BaseServer) Key() string {
return s.key
}
func (b BaseServer) URL() *url.URL {
return b.url
func (s BaseServer) URL() *url.URL {
return s.url
}
func (b BaseServer) Open() bool {
return b.open
func (s BaseServer) Open() bool {
return s.open
}
func (b BaseServer) IsZero() bool {
return b == BaseServer{}
func (s BaseServer) IsZero() bool {
return s == BaseServer{}
}
type BaseServers []BaseServer

View File

@ -47,7 +47,7 @@ func TestNewBaseServer(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseServer",
Field: "url",
Err: domain.ErrNotNil,
Err: domain.ErrNil,
},
},
}
@ -108,12 +108,12 @@ func TestBaseServers_FilterOutSpecial(t *testing.T) {
}
res := servers.FilterOutSpecial(special)
assert.Len(t, res, len(servers)-len(special))
for _, s := range servers {
if slices.ContainsFunc(special, func(server domain.Server) bool {
return server.Key() == s.Key()
}) {
return
assert.NotContains(t, res, s)
continue
}
assert.Contains(t, res, s)

View File

@ -1,6 +1,9 @@
package domain
import "net/url"
import (
"math"
"net/url"
)
type BaseTribe struct {
id int
@ -30,96 +33,70 @@ func NewBaseTribe(
od OpponentsDefeated,
profileURL *url.URL,
) (BaseTribe, error) {
if id < 1 {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "id",
Err: MinGreaterEqualError{
Min: 1,
Current: id,
},
Err: err,
}
}
if l := len(name); l < tribeNameMinLength || l > tribeNameMaxLength {
if err := validateStringLen(name, tribeNameMinLength, tribeNameMaxLength); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "name",
Err: LenOutOfRangeError{
Min: tribeNameMinLength,
Max: tribeNameMaxLength,
Current: l,
},
Err: err,
}
}
// we convert tag to []rune to get the correct length
// e.g. for tags such as Орловы, which takes 12 bytes rather than 6
// explanation: https://golangbyexample.com/number-characters-string-golang/
if l := len([]rune(tag)); l < tribeTagMinLength || l > tribeTagMaxLength {
if err := validateSliceLen([]rune(tag), tribeTagMinLength, tribeTagMaxLength); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "tag",
Err: LenOutOfRangeError{
Min: tribeTagMinLength,
Max: tribeTagMaxLength,
Current: l,
},
Err: err,
}
}
if numMembers < 0 {
if err := validateIntInRange(numMembers, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numMembers",
Err: MinGreaterEqualError{
Min: 0,
Current: numMembers,
},
Err: err,
}
}
if numVillages < 0 {
if err := validateIntInRange(numVillages, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "numVillages",
Err: MinGreaterEqualError{
Min: 0,
Current: numVillages,
},
Err: err,
}
}
if points < 0 {
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "points",
Err: MinGreaterEqualError{
Min: 0,
Current: points,
},
Err: err,
}
}
if allPoints < 0 {
if err := validateIntInRange(allPoints, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "allPoints",
Err: MinGreaterEqualError{
Min: 0,
Current: allPoints,
},
Err: err,
}
}
if rank < 0 {
if err := validateIntInRange(rank, 0, math.MaxInt); err != nil {
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "rank",
Err: MinGreaterEqualError{
Min: 0,
Current: rank,
},
Err: err,
}
}
@ -127,7 +104,7 @@ func NewBaseTribe(
return BaseTribe{}, ValidationError{
Model: baseTribeModelName,
Field: "profileURL",
Err: ErrNotNil,
Err: ErrNil,
}
}
@ -145,44 +122,48 @@ func NewBaseTribe(
}, nil
}
func (b BaseTribe) ID() int {
return b.id
func (t BaseTribe) ID() int {
return t.id
}
func (b BaseTribe) Name() string {
return b.name
func (t BaseTribe) Name() string {
return t.name
}
func (b BaseTribe) Tag() string {
return b.tag
func (t BaseTribe) Tag() string {
return t.tag
}
func (b BaseTribe) NumMembers() int {
return b.numMembers
func (t BaseTribe) NumMembers() int {
return t.numMembers
}
func (b BaseTribe) NumVillages() int {
return b.numVillages
func (t BaseTribe) NumVillages() int {
return t.numVillages
}
func (b BaseTribe) Points() int {
return b.points
func (t BaseTribe) Points() int {
return t.points
}
func (b BaseTribe) AllPoints() int {
return b.allPoints
func (t BaseTribe) AllPoints() int {
return t.allPoints
}
func (b BaseTribe) Rank() int {
return b.rank
func (t BaseTribe) Rank() int {
return t.rank
}
func (b BaseTribe) OD() OpponentsDefeated {
return b.od
func (t BaseTribe) OD() OpponentsDefeated {
return t.od
}
func (b BaseTribe) ProfileURL() *url.URL {
return b.profileURL
func (t BaseTribe) ProfileURL() *url.URL {
return t.profileURL
}
func (t BaseTribe) IsZero() bool {
return t == BaseTribe{}
}
type BaseTribes []BaseTribe

View File

@ -315,7 +315,7 @@ func TestNewBaseTribe(t *testing.T) {
expectedErr: domain.ValidationError{
Model: "BaseTribe",
Field: "profileURL",
Err: domain.ErrNotNil,
Err: domain.ErrNil,
},
},
}

View File

@ -9,18 +9,24 @@ import (
)
type BaseTribeConfig struct {
ID int
Tag string
OD domain.OpponentsDefeated
ID int
Tag string
OD domain.OpponentsDefeated
NumVillages int
AllPoints int
Rank int
}
func NewBaseTribe(tb TestingTB, opts ...func(cfg *BaseTribeConfig)) domain.BaseTribe {
tb.Helper()
cfg := &BaseTribeConfig{
ID: RandID(),
Tag: RandTribeTag(),
OD: NewOpponentsDefeated(tb),
ID: RandID(),
Tag: RandTribeTag(),
OD: NewOpponentsDefeated(tb),
NumVillages: gofakeit.IntRange(1, 10000),
AllPoints: gofakeit.IntRange(1, 10000),
Rank: gofakeit.IntRange(1, 10000),
}
for _, opt := range opts {
@ -35,10 +41,10 @@ func NewBaseTribe(tb TestingTB, opts ...func(cfg *BaseTribeConfig)) domain.BaseT
gofakeit.LetterN(50),
cfg.Tag,
gofakeit.IntRange(1, 10000),
cfg.NumVillages,
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
cfg.AllPoints,
cfg.Rank,
cfg.OD,
u,
)

View File

@ -1,7 +1,73 @@
package domaintest
import "github.com/brianvoe/gofakeit/v6"
import (
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func RandTribeTag() string {
return gofakeit.LetterN(5)
}
type TribeConfig struct {
ID int
ServerKey string
Tag string
OD domain.OpponentsDefeated
BestRank int
BestRankAt time.Time
MostPoints int
MostPointsAt time.Time
MostVillages int
MostVillagesAt time.Time
}
func NewTribe(tb TestingTB, opts ...func(cfg *TribeConfig)) domain.Tribe {
tb.Helper()
cfg := &TribeConfig{
ID: RandID(),
ServerKey: RandServerKey(),
Tag: RandTribeTag(),
OD: NewOpponentsDefeated(tb),
BestRank: gofakeit.IntRange(1, 10000),
BestRankAt: time.Now(),
MostPoints: gofakeit.IntRange(1, 10000),
MostPointsAt: time.Now(),
MostVillages: gofakeit.IntRange(1, 10000),
MostVillagesAt: time.Now(),
}
for _, opt := range opts {
opt(cfg)
}
t, err := domain.UnmarshalTribeFromDatabase(
cfg.ID,
cfg.ServerKey,
gofakeit.LetterN(50),
cfg.Tag,
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
gofakeit.IntRange(1, 10000),
cfg.OD,
gofakeit.URL(),
gofakeit.Float64Range(0.1, 99.9),
cfg.BestRank,
cfg.BestRankAt,
cfg.MostPoints,
cfg.MostPointsAt,
cfg.MostVillages,
cfg.MostVillagesAt,
time.Now(),
time.Time{},
)
require.NoError(tb, err)
return t
}

View File

@ -1,5 +1,7 @@
package domain
import "math"
type OpponentsDefeated struct {
rankAtt int
scoreAtt int
@ -23,91 +25,67 @@ func NewOpponentsDefeated(
rankTotal int,
scoreTotal int,
) (OpponentsDefeated, error) {
if rankAtt < 0 {
if err := validateIntInRange(rankAtt, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankAtt",
Err: MinGreaterEqualError{
Min: 0,
Current: rankAtt,
},
Err: err,
}
}
if scoreAtt < 0 {
if err := validateIntInRange(scoreAtt, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreAtt",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreAtt,
},
Err: err,
}
}
if rankDef < 0 {
if err := validateIntInRange(rankDef, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankDef",
Err: MinGreaterEqualError{
Min: 0,
Current: rankDef,
},
Err: err,
}
}
if scoreDef < 0 {
if err := validateIntInRange(scoreDef, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreDef",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreDef,
},
Err: err,
}
}
if rankSup < 0 {
if err := validateIntInRange(rankSup, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankSup",
Err: MinGreaterEqualError{
Min: 0,
Current: rankSup,
},
Err: err,
}
}
if scoreSup < 0 {
if err := validateIntInRange(scoreSup, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreSup",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreSup,
},
Err: err,
}
}
if rankTotal < 0 {
if err := validateIntInRange(rankTotal, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "rankTotal",
Err: MinGreaterEqualError{
Min: 0,
Current: rankTotal,
},
Err: err,
}
}
if scoreTotal < 0 {
if err := validateIntInRange(scoreTotal, 0, math.MaxInt); err != nil {
return OpponentsDefeated{}, ValidationError{
Model: opponentsDefeatedModelName,
Field: "scoreTotal",
Err: MinGreaterEqualError{
Min: 0,
Current: scoreTotal,
},
Err: err,
}
}

View File

@ -1,8 +1,8 @@
package domain
import (
"errors"
"fmt"
"math"
"net/url"
"slices"
"time"
@ -37,6 +37,8 @@ type Server struct {
ennoblementDataSyncedAt time.Time
}
const serverModelName = "Server"
// UnmarshalServerFromDatabase unmarshals Server from the database.
//
// It should be used only for unmarshalling from the database!
@ -65,16 +67,28 @@ func UnmarshalServerFromDatabase(
ennoblementDataSyncedAt time.Time,
) (Server, error) {
if key == "" {
return Server{}, errors.New("key can't be blank")
return Server{}, ValidationError{
Model: serverModelName,
Field: "key",
Err: ErrRequired,
}
}
if versionCode == "" {
return Server{}, errors.New("version code can't be blank")
return Server{}, ValidationError{
Model: serverModelName,
Field: "versionCode",
Err: ErrRequired,
}
}
u, err := parseURL(rawURL)
if err != nil {
return Server{}, err
return Server{}, ValidationError{
Model: serverModelName,
Field: "url",
Err: err,
}
}
return Server{
@ -186,6 +200,14 @@ func (s Server) EnnoblementDataSyncedAt() time.Time {
return s.ennoblementDataSyncedAt
}
func (s Server) Base() BaseServer {
return BaseServer{
key: s.key,
url: s.url,
open: s.open,
}
}
type Servers []Server
// Close finds all servers with Server.Open returning true that are not in the given slice with open servers
@ -398,25 +420,11 @@ func (params *ListServersParams) Limit() int {
}
func (params *ListServersParams) SetLimit(limit int) error {
if limit <= 0 {
if err := validateIntInRange(limit, 1, ServerListMaxLimit); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "limit",
Err: MinGreaterEqualError{
Min: 1,
Current: limit,
},
}
}
if limit > ServerListMaxLimit {
return ValidationError{
Model: listServersParamsModelName,
Field: "limit",
Err: MaxLessEqualError{
Max: ServerListMaxLimit,
Current: limit,
},
Err: err,
}
}
@ -430,14 +438,11 @@ func (params *ListServersParams) Offset() int {
}
func (params *ListServersParams) SetOffset(offset int) error {
if offset < 0 {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "offset",
Err: MinGreaterEqualError{
Min: 0,
Current: offset,
},
Err: err,
}
}

View File

@ -1,8 +1,6 @@
package domain
import (
"errors"
"fmt"
"net/url"
)
@ -11,13 +9,23 @@ type SyncServersCmdPayload struct {
url *url.URL
}
const syncServersCmdPayloadModelName = "SyncServersCmdPayload"
func NewSyncServersCmdPayload(versionCode string, u *url.URL) (SyncServersCmdPayload, error) {
if versionCode == "" {
return SyncServersCmdPayload{}, errors.New("version code can't be blank")
return SyncServersCmdPayload{}, ValidationError{
Model: syncServersCmdPayloadModelName,
Field: "versionCode",
Err: ErrRequired,
}
}
if u == nil {
return SyncServersCmdPayload{}, errors.New("url can't be nil")
return SyncServersCmdPayload{}, ValidationError{
Model: syncServersCmdPayloadModelName,
Field: "url",
Err: ErrNil,
}
}
return SyncServersCmdPayload{versionCode: versionCode, url: u}, nil
@ -37,17 +45,31 @@ type ServerSyncedEventPayload struct {
versionCode string
}
const serverSyncedEventPayloadModelName = "ServerSyncedEventPayload"
func NewServerSyncedEventPayload(key string, u *url.URL, versionCode string) (ServerSyncedEventPayload, error) {
if key == "" {
return ServerSyncedEventPayload{}, errors.New("key can't be blank")
return ServerSyncedEventPayload{}, ValidationError{
Model: serverSyncedEventPayloadModelName,
Field: "key",
Err: ErrRequired,
}
}
if versionCode == "" {
return ServerSyncedEventPayload{}, errors.New("version code can't be blank")
return ServerSyncedEventPayload{}, ValidationError{
Model: serverSyncedEventPayloadModelName,
Field: "versionCode",
Err: ErrRequired,
}
}
if u == nil {
return ServerSyncedEventPayload{}, errors.New("url can't be nil")
return ServerSyncedEventPayload{}, ValidationError{
Model: serverSyncedEventPayloadModelName,
Field: "url",
Err: ErrNil,
}
}
return ServerSyncedEventPayload{
@ -58,22 +80,15 @@ func NewServerSyncedEventPayload(key string, u *url.URL, versionCode string) (Se
}
func NewServerSyncedEventPayloads(servers BaseServers, versionCode string) ([]ServerSyncedEventPayload, error) {
if versionCode == "" {
return nil, errors.New("version code can't be blank")
}
res := make([]ServerSyncedEventPayload, 0, len(servers))
for i, s := range servers {
if s.IsZero() {
return nil, fmt.Errorf("servers[%d] is an empty struct", i)
for _, s := range servers {
payload, err := NewServerSyncedEventPayload(s.Key(), s.URL(), versionCode)
if err != nil {
return nil, err
}
res = append(res, ServerSyncedEventPayload{
key: s.Key(),
url: s.URL(),
versionCode: versionCode,
})
res = append(res, payload)
}
return res, nil

View File

@ -37,17 +37,20 @@ func TestServers_Close(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, res)
for _, s := range servers {
if !s.Open() || slices.ContainsFunc(open, func(server domain.BaseServer) bool {
return server.Key() == s.Key()
}) {
continue
}
assert.Contains(t, res, domaintest.NewBaseServer(t, func(cfg *domaintest.BaseServerConfig) {
expected := domaintest.NewBaseServer(t, func(cfg *domaintest.BaseServerConfig) {
cfg.Key = s.Key()
cfg.URL = s.URL()
cfg.Open = false
}))
})
if !s.Open() || slices.ContainsFunc(open, func(server domain.BaseServer) bool {
return server.Key() == s.Key()
}) {
assert.NotContains(t, res, expected)
continue
}
assert.Contains(t, res, expected)
}
}

View File

@ -1,7 +1,10 @@
package domain
import (
"fmt"
"math"
"net/url"
"slices"
"time"
)
@ -35,6 +38,75 @@ type Tribe struct {
deletedAt time.Time
}
const tribeModelName = "Tribe"
// UnmarshalTribeFromDatabase unmarshals Tribe from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalTribeFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalTribeFromDatabase(
id int,
serverKey string,
name string,
tag string,
numMembers int,
numVillages int,
points int,
allPoints int,
rank int,
od OpponentsDefeated,
rawProfileURL string,
dominance float64,
bestRank int,
bestRankAt time.Time,
mostPoints int,
mostPointsAt time.Time,
mostVillages int,
mostVillagesAt time.Time,
createdAt time.Time,
deletedAt time.Time,
) (Tribe, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return Tribe{}, ValidationError{
Model: tribeModelName,
Field: "id",
Err: err,
}
}
profileURL, err := parseURL(rawProfileURL)
if err != nil {
return Tribe{}, ValidationError{
Model: tribeModelName,
Field: "profileURL",
Err: err,
}
}
return Tribe{
id: id,
serverKey: serverKey,
name: name,
tag: tag,
numMembers: numMembers,
numVillages: numVillages,
points: points,
allPoints: allPoints,
rank: rank,
od: od,
profileURL: profileURL,
dominance: dominance,
bestRank: bestRank,
bestRankAt: bestRankAt,
mostPoints: mostPoints,
mostPointsAt: mostPointsAt,
mostVillages: mostVillages,
mostVillagesAt: mostVillagesAt,
createdAt: createdAt,
deletedAt: deletedAt,
}, nil
}
func (t Tribe) ID() int {
return t.id
}
@ -114,3 +186,276 @@ func (t Tribe) CreatedAt() time.Time {
func (t Tribe) DeletedAt() time.Time {
return t.deletedAt
}
func (t Tribe) IsDeleted() bool {
return !t.deletedAt.IsZero()
}
func (t Tribe) Base() BaseTribe {
return BaseTribe{
id: t.id,
name: t.name,
tag: t.tag,
numMembers: t.numMembers,
numVillages: t.numVillages,
points: t.points,
allPoints: t.allPoints,
rank: t.rank,
od: t.od,
profileURL: t.profileURL,
}
}
type Tribes []Tribe
type CreateTribeParams struct {
base BaseTribe
serverKey string
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
}
const createTribeParamsModelName = "CreateTribeParams"
//nolint:gocyclo
func NewCreateTribeParams(serverKey string, tribes BaseTribes, storedTribes Tribes) ([]CreateTribeParams, error) {
if err := validateServerKey(serverKey); err != nil {
return nil, ValidationError{
Model: createTribeParamsModelName,
Field: "serverKey",
Err: err,
}
}
params := make([]CreateTribeParams, 0, len(tribes))
for i, t := range tribes {
if t.IsZero() {
return nil, fmt.Errorf("tribes[%d] is an empty struct", i)
}
var old Tribe
if oldIdx := slices.IndexFunc(storedTribes, func(old Tribe) bool {
return old.ID() == t.ID() && old.ServerKey() == serverKey
}); oldIdx >= 0 {
old = storedTribes[oldIdx]
}
p := CreateTribeParams{
base: t,
serverKey: serverKey,
bestRank: old.bestRank,
bestRankAt: old.bestRankAt,
mostPoints: old.mostPoints,
mostPointsAt: old.mostPointsAt,
mostVillages: old.mostVillages,
mostVillagesAt: old.mostVillagesAt,
}
if t.AllPoints() > p.MostPoints() || old.ID() <= 0 {
p.mostPoints = t.AllPoints()
p.mostPointsAt = time.Now()
}
if t.NumVillages() > p.MostVillages() || old.ID() <= 0 {
p.mostVillages = t.NumVillages()
p.mostVillagesAt = time.Now()
}
if t.Rank() < p.BestRank() || old.ID() <= 0 {
p.bestRank = t.Rank()
p.bestRankAt = time.Now()
}
params = append(params, p)
}
return params, nil
}
func (params CreateTribeParams) Base() BaseTribe {
return params.base
}
func (params CreateTribeParams) ServerKey() string {
return params.serverKey
}
func (params CreateTribeParams) BestRank() int {
return params.bestRank
}
func (params CreateTribeParams) BestRankAt() time.Time {
return params.bestRankAt
}
func (params CreateTribeParams) MostPoints() int {
return params.mostPoints
}
func (params CreateTribeParams) MostPointsAt() time.Time {
return params.mostPointsAt
}
func (params CreateTribeParams) MostVillages() int {
return params.mostVillages
}
func (params CreateTribeParams) MostVillagesAt() time.Time {
return params.mostVillagesAt
}
type TribeSort uint8
const (
TribeSortIDASC TribeSort = iota + 1
TribeSortIDDESC
TribeSortServerKeyASC
TribeSortServerKeyDESC
)
const TribeListMaxLimit = 200
type ListTribesParams struct {
ids []int
idGT NullInt
serverKeys []string
deleted NullBool
sort []TribeSort
limit int
offset int
}
const listTribesParamsModelName = "ListTribesParams"
func NewListTribesParams() ListTribesParams {
return ListTribesParams{
sort: []TribeSort{
TribeSortServerKeyASC,
TribeSortIDASC,
},
limit: TribeListMaxLimit,
}
}
func (params *ListTribesParams) IDs() []int {
return params.ids
}
func (params *ListTribesParams) SetIDs(ids []int) error {
for i, id := range ids {
if err := validateIntInRange(id, 0, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "ids",
Index: i,
Err: err,
}
}
}
params.ids = ids
return nil
}
func (params *ListTribesParams) IDGT() NullInt {
return params.idGT
}
func (params *ListTribesParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListTribesParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListTribesParams) SetServerKeys(serverKeys []string) error {
params.serverKeys = serverKeys
return nil
}
func (params *ListTribesParams) Deleted() NullBool {
return params.deleted
}
func (params *ListTribesParams) SetDeleted(deleted NullBool) error {
params.deleted = deleted
return nil
}
func (params *ListTribesParams) Sort() []TribeSort {
return params.sort
}
const (
tribeSortMinLength = 1
tribeSortMaxLength = 2
)
func (params *ListTribesParams) SetSort(sort []TribeSort) error {
if err := validateSliceLen(sort, tribeSortMinLength, tribeSortMaxLength); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListTribesParams) Limit() int {
return params.limit
}
func (params *ListTribesParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, TribeListMaxLimit); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
func (params *ListTribesParams) Offset() int {
return params.offset
}
func (params *ListTribesParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "offset",
Err: err,
}
}
params.offset = offset
return nil
}

View File

@ -0,0 +1,419 @@
package domain_test
import (
"fmt"
"math"
"slices"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCreateTribeParams(t *testing.T) {
t.Parallel()
now := time.Now()
server := domaintest.NewServer(t)
tribes := domain.BaseTribes{
domaintest.NewBaseTribe(t),
domaintest.NewBaseTribe(t),
domaintest.NewBaseTribe(t),
}
storedTribes := domain.Tribes{
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
cfg.ID = tribes[0].ID()
cfg.ServerKey = server.Key()
cfg.BestRank = tribes[0].Rank() + 1
cfg.BestRankAt = now.Add(-time.Hour)
cfg.MostPoints = tribes[0].AllPoints() - 1
cfg.MostPointsAt = now.Add(-time.Hour)
cfg.MostVillages = tribes[0].NumVillages() - 1
cfg.MostVillagesAt = now.Add(-time.Hour)
}),
domaintest.NewTribe(t, func(cfg *domaintest.TribeConfig) {
cfg.ID = tribes[1].ID()
cfg.ServerKey = server.Key()
cfg.BestRank = tribes[1].Rank() - 1
cfg.BestRankAt = now.Add(-time.Hour)
cfg.MostPoints = tribes[1].AllPoints() + 1
cfg.MostPointsAt = now.Add(-time.Hour)
cfg.MostVillages = tribes[1].NumVillages() + 1
cfg.MostVillagesAt = now.Add(-time.Hour)
}),
}
expectedParams := []struct {
base domain.BaseTribe
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
}{
{
base: tribes[0],
bestRank: tribes[0].Rank(),
bestRankAt: now,
mostPoints: tribes[0].AllPoints(),
mostPointsAt: now,
mostVillages: tribes[0].NumVillages(),
mostVillagesAt: now,
},
{
base: tribes[1],
bestRank: storedTribes[1].BestRank(),
bestRankAt: storedTribes[1].BestRankAt(),
mostPoints: storedTribes[1].MostPoints(),
mostPointsAt: storedTribes[1].MostPointsAt(),
mostVillages: storedTribes[1].MostVillages(),
mostVillagesAt: storedTribes[1].MostVillagesAt(),
},
{
base: tribes[2],
bestRank: tribes[2].Rank(),
bestRankAt: now,
mostPoints: tribes[2].AllPoints(),
mostPointsAt: now,
mostVillages: tribes[2].NumVillages(),
mostVillagesAt: now,
},
}
res, err := domain.NewCreateTribeParams(server.Key(), tribes, storedTribes)
require.NoError(t, err)
assert.Len(t, res, len(expectedParams))
for i, expected := range expectedParams {
idx := slices.IndexFunc(res, func(params domain.CreateTribeParams) bool {
return params.Base().ID() == expected.base.ID()
})
require.GreaterOrEqualf(t, idx, 0, "expectedParams[%d] not found", i)
params := res[idx]
assert.Equalf(t, expected.base, params.Base(), "expectedParams[%d]", i)
assert.Equalf(t, expected.bestRank, params.BestRank(), "expectedParams[%d]", i)
assert.WithinDurationf(t, expected.bestRankAt, params.BestRankAt(), time.Minute, "expectedParams[%d]", i)
assert.Equalf(t, expected.mostPoints, params.MostPoints(), "expectedParams[%d]", i)
assert.WithinDurationf(t, expected.mostPointsAt, params.MostPointsAt(), time.Minute, "expectedParams[%d]", i)
assert.Equalf(t, expected.mostVillages, params.MostVillages(), "expectedParams[%d]", i)
assert.WithinDurationf(t, expected.mostVillagesAt, params.MostVillagesAt(), time.Minute, "expectedParams[%d]", i)
}
}
func TestListTribesParams_SetIDs(t *testing.T) {
t.Parallel()
type args struct {
ids []int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
ids: []int{
gofakeit.IntRange(0, math.MaxInt),
gofakeit.IntRange(0, math.MaxInt),
gofakeit.IntRange(0, math.MaxInt),
},
},
},
{
name: "ERR: value < 0",
args: args{
ids: []int{
gofakeit.IntRange(0, math.MaxInt),
gofakeit.IntRange(0, math.MaxInt),
gofakeit.IntRange(0, math.MaxInt),
-1,
gofakeit.IntRange(0, math.MaxInt),
},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListTribesParams",
Field: "ids",
Index: 3,
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetIDs(tt.args.ids), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.ids, params.IDs())
})
}
}
func TestListTribesParams_SetIDGT(t *testing.T) {
t.Parallel()
type args struct {
idGT domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
idGT: domain.NullInt{
Value: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "ERR: value < 0",
args: args{
idGT: domain.NullInt{
Value: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "idGT",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.idGT, params.IDGT())
})
}
}
func TestListTribesParams_SetSort(t *testing.T) {
t.Parallel()
type args struct {
sort []domain.TribeSort
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.TribeSort{
domain.TribeSortIDASC,
domain.TribeSortServerKeyASC,
},
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: 0,
},
},
},
{
name: "ERR: len(sort) > 2",
args: args{
sort: []domain.TribeSort{
domain.TribeSortIDASC,
domain.TribeSortServerKeyASC,
domain.TribeSortServerKeyDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: 3,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.sort, params.Sort())
})
}
}
func TestListTribesParams_SetLimit(t *testing.T) {
t.Parallel()
type args struct {
limit int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
limit: domain.TribeListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.TribeListMaxLimit),
args: args{
limit: domain.TribeListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.TribeListMaxLimit,
Current: domain.TribeListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestListTribesParams_SetOffset(t *testing.T) {
t.Parallel()
type args struct {
offset int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
offset: 100,
},
},
{
name: "ERR: offset < 0",
args: args{
offset: -1,
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
}

View File

@ -3,8 +3,7 @@ package domain
import (
"errors"
"fmt"
"github.com/gosimple/slug"
"strconv"
)
type ValidationError struct {
@ -41,22 +40,11 @@ func (e ValidationError) Code() ErrorCode {
}
func (e ValidationError) Slug() string {
s := "validation"
var domainErr Error
if errors.As(e.Err, &domainErr) {
s = domainErr.Slug()
return domainErr.Slug()
}
if e.Field != "" {
s = slug.Make(e.Field) + "-" + s
}
if e.Model != "" {
s = slug.Make(e.Model) + "-" + s
}
return s
return "validation-failed"
}
func (e ValidationError) Params() map[string]any {
@ -71,6 +59,60 @@ func (e ValidationError) Unwrap() error {
return e.Err
}
type SliceElementValidationError struct {
Model string
Field string
Index int
Err error
}
var _ ErrorWithParams = SliceElementValidationError{}
func (e SliceElementValidationError) Error() string {
prefix := e.Model
if e.Field != "" {
if len(prefix) > 0 {
prefix += "."
}
prefix += e.Field + "[" + strconv.Itoa(e.Index) + "]"
}
if prefix != "" {
prefix += ": "
}
return prefix + e.Err.Error()
}
func (e SliceElementValidationError) Code() ErrorCode {
var domainErr Error
if errors.As(e.Err, &domainErr) {
return domainErr.Code()
}
return ErrorCodeIncorrectInput
}
func (e SliceElementValidationError) Slug() string {
var domainErr Error
if errors.As(e.Err, &domainErr) {
return domainErr.Slug()
}
return "slice-element-validation-failed"
}
func (e SliceElementValidationError) Params() map[string]any {
var withParams ErrorWithParams
if ok := errors.As(e.Err, &withParams); !ok {
return nil
}
return withParams.Params()
}
func (e SliceElementValidationError) Unwrap() error {
return e.Err
}
type MinGreaterEqualError struct {
Min int
Current int
@ -151,10 +193,16 @@ func (e LenOutOfRangeError) Params() map[string]any {
}
}
var ErrNotNil error = simpleError{
var ErrRequired error = simpleError{
msg: "can't be blank",
code: ErrorCodeIncorrectInput,
slug: "required",
}
var ErrNil error = simpleError{
msg: "must not be nil",
code: ErrorCodeIncorrectInput,
slug: "not-nil",
slug: "nil",
}
func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
@ -169,6 +217,18 @@ func validateSliceLen[S ~[]E, E any](s S, min, max int) error {
return nil
}
func validateStringLen(s string, min, max int) error {
if l := len(s); l > max || l < min {
return LenOutOfRangeError{
Min: min,
Max: max,
Current: l,
}
}
return nil
}
func validateServerKey(key string) error {
if l := len(key); l < serverKeyMinLength || l > serverKeyMaxLength {
return LenOutOfRangeError{
@ -192,3 +252,21 @@ func validateVersionCode(code string) error {
return nil
}
func validateIntInRange(current, min, max int) error {
if current < min {
return MinGreaterEqualError{
Min: min,
Current: current,
}
}
if current > max {
return MaxLessEqualError{
Max: max,
Current: current,
}
}
return nil
}

View File

@ -1,7 +1,6 @@
package domain
import (
"errors"
"net/url"
)
@ -17,6 +16,8 @@ type Version struct {
timezone string
}
var versionModelName = "Version"
// UnmarshalVersionFromDatabase unmarshals Version from the database.
//
// It should be used only for unmarshalling from the database!
@ -28,19 +29,35 @@ func UnmarshalVersionFromDatabase(
timezone string,
) (Version, error) {
if code == "" {
return Version{}, errors.New("code can't be blank")
return Version{}, ValidationError{
Model: versionModelName,
Field: "code",
Err: ErrRequired,
}
}
if name == "" {
return Version{}, errors.New("name can't be blank")
return Version{}, ValidationError{
Model: versionModelName,
Field: "name",
Err: ErrRequired,
}
}
if host == "" {
return Version{}, errors.New("host can't be blank")
return Version{}, ValidationError{
Model: versionModelName,
Field: "host",
Err: ErrRequired,
}
}
if timezone == "" {
return Version{}, errors.New("timezone can't be blank")
return Version{}, ValidationError{
Model: versionModelName,
Field: "timezone",
Err: ErrRequired,
}
}
return Version{