core/internal/domain/tribe.go

999 lines
20 KiB
Go

package domain
import (
"cmp"
"fmt"
"math"
"net/url"
"slices"
"time"
)
const (
tribeNameMinLength = 1
tribeNameMaxLength = 255
tribeTagMinLength = 1
tribeTagMaxLength = 10
)
type Tribe struct {
id int
serverKey string
name string
tag string
numMembers int
numVillages int
points int
allPoints int
rank int
od OpponentsDefeated
profileURL *url.URL
dominance float64
bestRank int
bestRankAt time.Time
mostPoints int
mostPointsAt time.Time
mostVillages int
mostVillagesAt time.Time
createdAt time.Time
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,
}
}
if err := validateServerKey(serverKey); err != nil {
return Tribe{}, ValidationError{
Model: tribeModelName,
Field: "serverKey",
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
}
func (t Tribe) ServerKey() string {
return t.serverKey
}
func (t Tribe) Name() string {
return t.name
}
func (t Tribe) Tag() string {
return t.tag
}
func (t Tribe) NumMembers() int {
return t.numMembers
}
func (t Tribe) NumVillages() int {
return t.numVillages
}
func (t Tribe) Points() int {
return t.points
}
func (t Tribe) AllPoints() int {
return t.allPoints
}
func (t Tribe) Rank() int {
return t.rank
}
func (t Tribe) OD() OpponentsDefeated {
return t.od
}
func (t Tribe) ProfileURL() *url.URL {
if t.profileURL == nil {
return &url.URL{}
}
return t.profileURL
}
func (t Tribe) Dominance() float64 {
return t.dominance
}
func (t Tribe) BestRank() int {
return t.bestRank
}
func (t Tribe) BestRankAt() time.Time {
return t.bestRankAt
}
func (t Tribe) MostPoints() int {
return t.mostPoints
}
func (t Tribe) MostPointsAt() time.Time {
return t.mostPointsAt
}
func (t Tribe) MostVillages() int {
return t.mostVillages
}
func (t Tribe) MostVillagesAt() time.Time {
return t.mostVillagesAt
}
func (t Tribe) CreatedAt() time.Time {
return t.createdAt
}
func (t Tribe) DeletedAt() time.Time {
return t.deletedAt
}
func (t Tribe) ToCursor() (TribeCursor, error) {
return NewTribeCursor(
t.id,
t.serverKey,
t.od.scoreAtt,
t.od.scoreDef,
t.od.scoreTotal,
t.points,
t.dominance,
t.deletedAt,
)
}
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,
}
}
func (t Tribe) Meta() TribeMeta {
return TribeMeta{
id: t.id,
name: t.name,
tag: t.tag,
profileURL: t.profileURL,
}
}
func (t Tribe) IsDeleted() bool {
return !t.deletedAt.IsZero()
}
func (t Tribe) IsZero() bool {
return t == Tribe{}
}
// active tribes must be sorted in ascending order by ID
func (t Tribe) canBeDeleted(serverKey string, active BaseTribes) bool {
if t.IsDeleted() {
return false
}
if t.serverKey != serverKey {
return false
}
_, found := slices.BinarySearchFunc(active, t, func(a BaseTribe, b Tribe) int {
return cmp.Compare(a.ID(), b.id)
})
return !found
}
type Tribes []Tribe
// Delete finds all tribes with the given serverKey that are not in the given slice with active tribes
// and returns their ids. Both slices must be sorted in ascending order by ID
// + if vs contains tribes from different servers. they must be sorted in ascending order by server key.
func (ts Tribes) Delete(serverKey string, active BaseTribes) []int {
//nolint:prealloc
var toDelete []int
for _, t := range ts {
if !t.canBeDeleted(serverKey, active) {
continue
}
toDelete = append(toDelete, t.ID())
}
return toDelete
}
type TribeMeta struct {
id int
name string
tag string
profileURL *url.URL
}
const tribeMetaModelName = "TribeMeta"
// UnmarshalTribeMetaFromDatabase unmarshals TribeMeta from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalTribeMetaFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalTribeMetaFromDatabase(id int, name string, tag string, rawProfileURL string) (TribeMeta, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return TribeMeta{}, ValidationError{
Model: tribeMetaModelName,
Field: "id",
Err: err,
}
}
profileURL, err := parseURL(rawProfileURL)
if err != nil {
return TribeMeta{}, ValidationError{
Model: tribeMetaModelName,
Field: "profileURL",
Err: err,
}
}
return TribeMeta{id: id, name: name, tag: tag, profileURL: profileURL}, nil
}
func (t TribeMeta) ID() int {
return t.id
}
func (t TribeMeta) Name() string {
return t.name
}
func (t TribeMeta) Tag() string {
return t.tag
}
func (t TribeMeta) ProfileURL() *url.URL {
if t.profileURL == nil {
return &url.URL{}
}
return t.profileURL
}
func (t TribeMeta) IsZero() bool {
return t == TribeMeta{}
}
type NullTribeMeta NullValue[TribeMeta]
func (t NullTribeMeta) IsZero() bool {
return !t.Valid
}
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"
// NewCreateTribeParams constructs a slice of CreateTribeParams based on the given parameters.
// Both slices must be sorted in ascending order by ID
// + if storedTribes contains tribes from different servers. they must be sorted in ascending order by server key.
//
//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
idx, found := slices.BinarySearchFunc(storedTribes, t, func(a Tribe, b BaseTribe) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), serverKey),
cmp.Compare(a.ID(), b.ID()),
)
})
if found {
old = storedTribes[idx]
}
now := time.Now()
p := CreateTribeParams{
base: t,
serverKey: serverKey,
bestRank: t.Rank(),
bestRankAt: now,
mostPoints: t.AllPoints(),
mostPointsAt: now,
mostVillages: t.NumVillages(),
mostVillagesAt: now,
}
if old.ID() > 0 {
if old.MostPoints() >= p.MostPoints() {
p.mostPoints = old.MostPoints()
p.mostPointsAt = old.MostPointsAt()
}
if old.MostVillages() >= p.MostVillages() {
p.mostVillages = old.MostVillages()
p.mostVillagesAt = old.MostVillagesAt()
}
if old.BestRank() <= p.BestRank() {
p.bestRank = old.BestRank()
p.bestRankAt = old.BestRankAt()
}
}
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
TribeSortODScoreAttASC
TribeSortODScoreAttDESC
TribeSortODScoreDefASC
TribeSortODScoreDefDESC
TribeSortODScoreTotalASC
TribeSortODScoreTotalDESC
TribeSortPointsASC
TribeSortPointsDESC
TribeSortDominanceASC
TribeSortDominanceDESC
TribeSortDeletedAtASC
TribeSortDeletedAtDESC
)
// IsInConflict returns true if two sorts can't be used together (e.g. TribeSortIDASC and TribeSortIDDESC).
func (s TribeSort) IsInConflict(s2 TribeSort) bool {
return isSortInConflict(s, s2)
}
//nolint:gocyclo
func (s TribeSort) String() string {
switch s {
case TribeSortIDASC:
return "id:ASC"
case TribeSortIDDESC:
return "id:DESC"
case TribeSortServerKeyASC:
return "serverKey:ASC"
case TribeSortServerKeyDESC:
return "serverKey:DESC"
case TribeSortODScoreAttASC:
return "odScoreAtt:ASC"
case TribeSortODScoreAttDESC:
return "odScoreAtt:DESC"
case TribeSortODScoreDefASC:
return "odScoreDef:ASC"
case TribeSortODScoreDefDESC:
return "odScoreDef:DESC"
case TribeSortODScoreTotalASC:
return "odScoreTotal:ASC"
case TribeSortODScoreTotalDESC:
return "odScoreTotal:DESC"
case TribeSortPointsASC:
return "points:ASC"
case TribeSortPointsDESC:
return "points:DESC"
case TribeSortDominanceASC:
return "dominance:ASC"
case TribeSortDominanceDESC:
return "dominance:DESC"
case TribeSortDeletedAtASC:
return "deletedAt:ASC"
case TribeSortDeletedAtDESC:
return "deletedAt:DESC"
default:
return "unknown tribe sort"
}
}
type TribeCursor struct {
id int
serverKey string
odScoreAtt int
odScoreDef int
odScoreTotal int
points int
dominance float64
deletedAt time.Time
}
const tribeCursorModelName = "TribeCursor"
func NewTribeCursor(
id int,
serverKey string,
odScoreAtt int,
odScoreDef int,
odScoreTotal int,
points int,
dominance float64,
deletedAt time.Time,
) (TribeCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "serverKey",
Err: err,
}
}
if err := validateIntInRange(odScoreAtt, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreAtt",
Err: err,
}
}
if err := validateIntInRange(odScoreDef, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreDef",
Err: err,
}
}
if err := validateIntInRange(odScoreTotal, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "odScoreTotal",
Err: err,
}
}
if err := validateIntInRange(points, 0, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "points",
Err: err,
}
}
return TribeCursor{
id: id,
serverKey: serverKey,
odScoreAtt: odScoreAtt,
odScoreDef: odScoreDef,
odScoreTotal: odScoreTotal,
points: points,
dominance: dominance,
deletedAt: deletedAt,
}, nil
}
//nolint:gocyclo
func decodeTribeCursor(encoded string) (TribeCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return TribeCursor{}, err
}
id, err := m.int("id")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
odScoreAtt, err := m.int("odScoreAtt")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
odScoreDef, err := m.int("odScoreDef")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
odScoreTotal, err := m.int("odScoreTotal")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
points, err := m.int("points")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
dominance, err := m.float64("dominance")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
deletedAt, err := m.time("deletedAt")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
tc, err := NewTribeCursor(
id,
serverKey,
odScoreAtt,
odScoreDef,
odScoreTotal,
points,
dominance,
deletedAt,
)
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
return tc, nil
}
func (tc TribeCursor) ID() int {
return tc.id
}
func (tc TribeCursor) ServerKey() string {
return tc.serverKey
}
func (tc TribeCursor) ODScoreAtt() int {
return tc.odScoreAtt
}
func (tc TribeCursor) ODScoreDef() int {
return tc.odScoreDef
}
func (tc TribeCursor) ODScoreTotal() int {
return tc.odScoreTotal
}
func (tc TribeCursor) Points() int {
return tc.points
}
func (tc TribeCursor) Dominance() float64 {
return tc.dominance
}
func (tc TribeCursor) DeletedAt() time.Time {
return tc.deletedAt
}
func (tc TribeCursor) IsZero() bool {
return tc == TribeCursor{}
}
func (tc TribeCursor) Encode() string {
if tc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", tc.id},
{"serverKey", tc.serverKey},
{"odScoreAtt", tc.odScoreAtt},
{"odScoreDef", tc.odScoreDef},
{"odScoreTotal", tc.odScoreTotal},
{"points", tc.points},
{"dominance", tc.dominance},
{"deletedAt", tc.deletedAt},
})
}
type ListTribesParams struct {
ids []int
serverKeys []string
tags []string
deleted NullBool
sort []TribeSort
cursor TribeCursor
limit int
}
const (
TribeListMaxLimit = 500
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, 1, math.MaxInt); err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "ids",
Index: i,
Err: err,
}
}
}
params.ids = ids
return nil
}
func (params *ListTribesParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListTribesParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
func (params *ListTribesParams) Tags() []string {
return params.tags
}
const (
tribeTagsMinLength = 0
tribeTagsMaxLength = 100
)
func (params *ListTribesParams) SetTags(tags []string) error {
if err := validateSliceLen(tags, tribeTagsMinLength, tribeTagsMaxLength); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "tags",
Err: err,
}
}
params.tags = tags
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 = 0
tribeSortMaxLength = 4
)
func (params *ListTribesParams) SetSort(sort []TribeSort) error {
if err := validateSort(sort, tribeSortMinLength, tribeSortMaxLength); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListTribesParams) PrependSortString(sort []string, allowed []TribeSort, maxLength int) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(min(tribeSortMaxLength-len(params.sort), maxLength), 0)); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "sort",
Err: err,
}
}
toPrepend := make([]TribeSort, 0, len(sort))
for i, s := range sort {
converted, err := newSortFromString(s, allowed...)
if err != nil {
return SliceElementValidationError{
Model: listTribesParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
toPrepend = append(toPrepend, converted)
}
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListTribesParams) Cursor() TribeCursor {
return params.cursor
}
func (params *ListTribesParams) SetCursor(cursor TribeCursor) error {
params.cursor = cursor
return nil
}
func (params *ListTribesParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeTribeCursor(encoded)
if err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
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
}
type ListTribesResult struct {
tribes Tribes
self TribeCursor
next TribeCursor
}
const listTribesResultModelName = "ListTribesResult"
func NewListTribesResult(tribes Tribes, next Tribe) (ListTribesResult, error) {
var err error
res := ListTribesResult{
tribes: tribes,
}
if len(tribes) > 0 {
res.self, err = tribes[0].ToCursor()
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,
Field: "self",
Err: err,
}
}
}
if !next.IsZero() {
res.next, err = next.ToCursor()
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,
Field: "next",
Err: err,
}
}
}
return res, nil
}
func (res ListTribesResult) Tribes() Tribes {
return res.tribes
}
func (res ListTribesResult) Self() TribeCursor {
return res.self
}
func (res ListTribesResult) Next() TribeCursor {
return res.next
}
type TribeNotFoundError struct {
ID int
ServerKey string
}
var _ ErrorWithParams = TribeNotFoundError{}
func (e TribeNotFoundError) Error() string {
return fmt.Sprintf("tribe with id %d and server key %s not found", e.ID, e.ServerKey)
}
func (e TribeNotFoundError) Type() ErrorType {
return ErrorTypeNotFound
}
const errorCodeTribeNotFound = "tribe-not-found"
func (e TribeNotFoundError) Code() string {
return errorCodeTribeNotFound
}
func (e TribeNotFoundError) Params() map[string]any {
return map[string]any{
"ID": e.ID,
"ServerKey": e.ServerKey,
}
}