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 = 1 tribeSortMaxLength = 3 ) 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) error { if err := validateSliceLen(sort, tribeSortMinLength, max(tribeSortMaxLength-len(params.sort), 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, TribeSortODScoreAttASC, TribeSortODScoreAttDESC, TribeSortODScoreDefASC, TribeSortODScoreDefDESC, TribeSortODScoreTotalASC, TribeSortODScoreTotalDESC, TribeSortPointsASC, TribeSortPointsDESC, TribeSortDominanceASC, TribeSortDominanceDESC, TribeSortDeletedAtASC, TribeSortDeletedAtDESC, ) 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, } }