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 { 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) 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 // 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.IsDeleted() || t.ServerKey() != serverKey { continue } _, found := slices.BinarySearchFunc(active, t, func(a BaseTribe, b Tribe) int { return cmp.Compare(a.ID(), b.ID()) }) if found { continue } toDelete = append(toDelete, t.ID()) } return toDelete } 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 { if res := cmp.Compare(a.ServerKey(), serverKey); res != 0 { return res } return 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 ) 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 }