package domain import ( "cmp" "fmt" "math" "net/url" "slices" "time" ) const ( villageNameMinLength = 1 villageNameMaxLength = 150 villageContinentMinLength = 1 villageContinentMaxLength = 5 ) type Village struct { id int serverKey string name string points int x int y int continent string bonus int playerID int profileURL *url.URL createdAt time.Time } const villageModelName = "Village" // UnmarshalVillageFromDatabase unmarshals Village from the database. // // It should be used only for unmarshalling from the database! // You can't use UnmarshalVillageFromDatabase as constructor - It may put domain into the invalid state! func UnmarshalVillageFromDatabase( id int, serverKey string, name string, points int, x int, y int, continent string, bonus int, playerID int, rawProfileURL string, createdAt time.Time, ) (Village, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return Village{}, ValidationError{ Model: villageModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return Village{}, ValidationError{ Model: villageModelName, Field: "serverKey", Err: err, } } profileURL, err := parseURL(rawProfileURL) if err != nil { return Village{}, ValidationError{ Model: villageModelName, Field: "profileURL", Err: err, } } return Village{ id: id, serverKey: serverKey, name: name, points: points, x: x, y: y, continent: continent, bonus: bonus, playerID: playerID, profileURL: profileURL, createdAt: createdAt, }, nil } func (v Village) ID() int { return v.id } func (v Village) ServerKey() string { return v.serverKey } func (v Village) Name() string { return v.name } func (v Village) FullName() string { return formatVillageFullName(v) } func (v Village) Points() int { return v.points } func (v Village) X() int { return v.x } func (v Village) Y() int { return v.y } func (v Village) Coords() Coords { return Coords{ x: v.x, y: v.y, } } func (v Village) Continent() string { return v.continent } func (v Village) Bonus() int { return v.bonus } func (v Village) PlayerID() int { return v.playerID } func (v Village) ProfileURL() *url.URL { return v.profileURL } func (v Village) CreatedAt() time.Time { return v.createdAt } func (v Village) WithRelations(player NullPlayerMetaWithRelations) VillageWithRelations { return VillageWithRelations{ village: v, player: player, } } func (v Village) ToCursor() (VillageCursor, error) { return NewVillageCursor(v.id, v.serverKey) } func (v Village) Meta() VillageMeta { return VillageMeta{ id: v.id, name: v.name, x: v.x, y: v.y, continent: v.continent, profileURL: v.profileURL, } } func (v Village) Base() BaseVillage { return BaseVillage{ id: v.id, name: v.name, points: v.points, x: v.x, y: v.y, continent: v.continent, bonus: v.bonus, playerID: v.playerID, profileURL: v.profileURL, } } func (v Village) IsZero() bool { return v == Village{} } type Villages []Village // Delete finds all villages with the given serverKey that are not in the given slice with active villages // and returns their ids. Both slices must be sorted in ascending order by ID // + if vs contains villages from different servers. they must be sorted in ascending order by server key. func (vs Villages) Delete(serverKey string, active BaseVillages) []int { // villages are deleted now and then, there is no point in prereallocating these slices //nolint:prealloc var toDelete []int for _, v := range vs { if v.ServerKey() != serverKey { continue } _, found := slices.BinarySearchFunc(active, v, func(a BaseVillage, b Village) int { return cmp.Compare(a.ID(), b.ID()) }) if found { continue } toDelete = append(toDelete, v.ID()) } return toDelete } type VillageWithRelations struct { village Village player NullPlayerMetaWithRelations } func (v VillageWithRelations) Village() Village { return v.village } func (v VillageWithRelations) Player() NullPlayerMetaWithRelations { return v.player } func (v VillageWithRelations) IsZero() bool { return v.village.IsZero() } type VillagesWithRelations []VillageWithRelations type VillageMeta struct { id int name string x int y int continent string profileURL *url.URL } const villageMetaModelName = "VillageMeta" // UnmarshalVillageMetaFromDatabase unmarshals VillageMeta from the database. // // It should be used only for unmarshalling from the database! // You can't use UnmarshalVillageMetaFromDatabase as constructor - It may put domain into the invalid state! func UnmarshalVillageMetaFromDatabase( id int, name string, x int, y int, continent string, rawProfileURL string, ) (VillageMeta, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return VillageMeta{}, ValidationError{ Model: villageMetaModelName, Field: "id", Err: err, } } profileURL, err := parseURL(rawProfileURL) if err != nil { return VillageMeta{}, ValidationError{ Model: villageMetaModelName, Field: "profileURL", Err: err, } } return VillageMeta{ id: id, name: name, x: x, y: y, continent: continent, profileURL: profileURL, }, nil } func (v VillageMeta) ID() int { return v.id } func (v VillageMeta) Name() string { return v.name } func (v VillageMeta) FullName() string { return formatVillageFullName(v) } func (v VillageMeta) X() int { return v.x } func (v VillageMeta) Y() int { return v.y } func (v VillageMeta) Continent() string { return v.continent } func (v VillageMeta) ProfileURL() *url.URL { return v.profileURL } func (v VillageMeta) IsZero() bool { return v == VillageMeta{} } type CreateVillageParams struct { base BaseVillage serverKey string } const createVillageParamsModelName = "CreateVillageParams" func NewCreateVillageParams(serverKey string, villages BaseVillages) ([]CreateVillageParams, error) { if err := validateServerKey(serverKey); err != nil { return nil, ValidationError{ Model: createVillageParamsModelName, Field: "serverKey", Err: err, } } params := make([]CreateVillageParams, 0, len(villages)) for i, v := range villages { if v.IsZero() { return nil, fmt.Errorf("villages[%d] is an empty struct", i) } params = append(params, CreateVillageParams{ base: v, serverKey: serverKey, }) } return params, nil } func (params CreateVillageParams) Base() BaseVillage { return params.base } func (params CreateVillageParams) ServerKey() string { return params.serverKey } type VillageSort uint8 const ( VillageSortIDASC VillageSort = iota + 1 VillageSortIDDESC VillageSortServerKeyASC VillageSortServerKeyDESC ) // IsInConflict returns true if two sorts can't be used together (e.g. VillageSortIDASC and VillageSortIDDESC). func (s VillageSort) IsInConflict(s2 VillageSort) bool { ss := []VillageSort{s, s2} slices.Sort(ss) // ASC is always an odd number, DESC is always an even number return (ss[0]%2 == 1 && ss[0] == ss[1]-1) || ss[0] == ss[1] } //nolint:gocyclo func (s VillageSort) String() string { switch s { case VillageSortIDASC: return "id:ASC" case VillageSortIDDESC: return "id:DESC" case VillageSortServerKeyASC: return "serverKey:ASC" case VillageSortServerKeyDESC: return "serverKey:DESC" default: return "unknown village sort" } } type VillageCursor struct { id int serverKey string } const villageCursorModelName = "VillageCursor" func NewVillageCursor(id int, serverKey string) (VillageCursor, error) { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return VillageCursor{}, ValidationError{ Model: villageCursorModelName, Field: "id", Err: err, } } if err := validateServerKey(serverKey); err != nil { return VillageCursor{}, ValidationError{ Model: villageCursorModelName, Field: "serverKey", Err: err, } } return VillageCursor{ id: id, serverKey: serverKey, }, nil } //nolint:gocyclo func decodeVillageCursor(encoded string) (VillageCursor, error) { m, err := decodeCursor(encoded) if err != nil { return VillageCursor{}, err } id, err := m.int("id") if err != nil { return VillageCursor{}, ErrInvalidCursor } serverKey, err := m.string("serverKey") if err != nil { return VillageCursor{}, ErrInvalidCursor } vc, err := NewVillageCursor( id, serverKey, ) if err != nil { return VillageCursor{}, ErrInvalidCursor } return vc, nil } func (vc VillageCursor) ID() int { return vc.id } func (vc VillageCursor) ServerKey() string { return vc.serverKey } func (vc VillageCursor) IsZero() bool { return vc == VillageCursor{} } func (vc VillageCursor) Encode() string { if vc.IsZero() { return "" } return encodeCursor([]keyValuePair{ {"id", vc.id}, {"serverKey", vc.serverKey}, }) } type ListVillagesParams struct { ids []int serverKeys []string coords []Coords playerIDs []int tribeIDs []int sort []VillageSort cursor VillageCursor limit int } const ( VillageListMaxLimit = 500 listVillagesParamsModelName = "ListVillagesParams" ) func NewListVillagesParams() ListVillagesParams { return ListVillagesParams{ sort: []VillageSort{ VillageSortServerKeyASC, VillageSortIDASC, }, limit: VillageListMaxLimit, } } func (params *ListVillagesParams) IDs() []int { return params.ids } func (params *ListVillagesParams) SetIDs(ids []int) error { for i, id := range ids { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return SliceElementValidationError{ Model: listVillagesParamsModelName, Field: "ids", Index: i, Err: err, } } } params.ids = ids return nil } func (params *ListVillagesParams) ServerKeys() []string { return params.serverKeys } func (params *ListVillagesParams) SetServerKeys(serverKeys []string) error { for i, sk := range serverKeys { if err := validateServerKey(sk); err != nil { return SliceElementValidationError{ Model: listVillagesParamsModelName, Field: "serverKeys", Index: i, Err: err, } } } params.serverKeys = serverKeys return nil } func (params *ListVillagesParams) Coords() []Coords { return params.coords } const ( villageCoordsMinLength = 0 villageCoordsMaxLength = 200 ) func (params *ListVillagesParams) SetCoords(coords []Coords) error { if err := validateSliceLen(coords, villageCoordsMinLength, villageCoordsMaxLength); err != nil { return ValidationError{ Model: listVillagesParamsModelName, Field: "coords", Err: err, } } params.coords = coords return nil } func (params *ListVillagesParams) SetCoordsString(rawCoords []string) error { if err := validateSliceLen(rawCoords, villageCoordsMinLength, villageCoordsMaxLength); err != nil { return ValidationError{ Model: listVillagesParamsModelName, Field: "coords", Err: err, } } coords := make([]Coords, 0, len(rawCoords)) for i, s := range rawCoords { c, err := newCoords(s) if err != nil { return SliceElementValidationError{ Model: listVillagesParamsModelName, Field: "coords", Index: i, Err: err, } } coords = append(coords, c) } params.coords = coords return nil } func (params *ListVillagesParams) PlayerIDs() []int { return params.playerIDs } func (params *ListVillagesParams) SetPlayerIDs(playerIDs []int) error { for i, id := range playerIDs { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return SliceElementValidationError{ Model: listVillagesParamsModelName, Field: "playerIDs", Index: i, Err: err, } } } params.playerIDs = playerIDs return nil } func (params *ListVillagesParams) TribeIDs() []int { return params.tribeIDs } func (params *ListVillagesParams) SetTribeIDs(tribeIDs []int) error { for i, id := range tribeIDs { if err := validateIntInRange(id, 1, math.MaxInt); err != nil { return SliceElementValidationError{ Model: listVillagesParamsModelName, Field: "tribeIDs", Index: i, Err: err, } } } params.tribeIDs = tribeIDs return nil } func (params *ListVillagesParams) Sort() []VillageSort { return params.sort } const ( villageSortMinLength = 1 villageSortMaxLength = 2 ) func (params *ListVillagesParams) SetSort(sort []VillageSort) error { if err := validateSort(sort, villageSortMinLength, villageSortMaxLength); err != nil { return ValidationError{ Model: listVillagesParamsModelName, Field: "sort", Err: err, } } params.sort = sort return nil } func (params *ListVillagesParams) Cursor() VillageCursor { return params.cursor } func (params *ListVillagesParams) SetCursor(cursor VillageCursor) error { params.cursor = cursor return nil } func (params *ListVillagesParams) SetEncodedCursor(encoded string) error { decoded, err := decodeVillageCursor(encoded) if err != nil { return ValidationError{ Model: listVillagesParamsModelName, Field: "cursor", Err: err, } } params.cursor = decoded return nil } func (params *ListVillagesParams) Limit() int { return params.limit } func (params *ListVillagesParams) SetLimit(limit int) error { if err := validateIntInRange(limit, 1, VillageListMaxLimit); err != nil { return ValidationError{ Model: listVillagesParamsModelName, Field: "limit", Err: err, } } params.limit = limit return nil } type ListVillagesResult struct { villages Villages self VillageCursor next VillageCursor } const listVillagesResultModelName = "ListVillagesResult" func NewListVillagesResult(villages Villages, next Village) (ListVillagesResult, error) { var err error res := ListVillagesResult{ villages: villages, } if len(villages) > 0 { res.self, err = villages[0].ToCursor() if err != nil { return ListVillagesResult{}, ValidationError{ Model: listVillagesResultModelName, Field: "self", Err: err, } } } if !next.IsZero() { res.next, err = next.ToCursor() if err != nil { return ListVillagesResult{}, ValidationError{ Model: listVillagesResultModelName, Field: "next", Err: err, } } } return res, nil } func (res ListVillagesResult) Villages() Villages { return res.villages } func (res ListVillagesResult) Self() VillageCursor { return res.self } func (res ListVillagesResult) Next() VillageCursor { return res.next } type ListVillagesWithRelationsResult struct { villages VillagesWithRelations self VillageCursor next VillageCursor } const listVillagesWithRelationsResultModelName = "ListVillagesWithRelationsResult" func NewListVillagesWithRelationsResult( villages VillagesWithRelations, next VillageWithRelations, ) (ListVillagesWithRelationsResult, error) { var err error res := ListVillagesWithRelationsResult{ villages: villages, } if len(villages) > 0 { res.self, err = villages[0].Village().ToCursor() if err != nil { return ListVillagesWithRelationsResult{}, ValidationError{ Model: listVillagesWithRelationsResultModelName, Field: "self", Err: err, } } } if !next.IsZero() { res.next, err = next.Village().ToCursor() if err != nil { return ListVillagesWithRelationsResult{}, ValidationError{ Model: listVillagesWithRelationsResultModelName, Field: "next", Err: err, } } } return res, nil } func (res ListVillagesWithRelationsResult) Villages() VillagesWithRelations { return res.villages } func (res ListVillagesWithRelationsResult) Self() VillageCursor { return res.self } func (res ListVillagesWithRelationsResult) Next() VillageCursor { return res.next } type VillageNotFoundError struct { ID int ServerKey string } var _ ErrorWithParams = VillageNotFoundError{} func (e VillageNotFoundError) Error() string { return fmt.Sprintf("village with id %d and server key %s not found", e.ID, e.ServerKey) } func (e VillageNotFoundError) Type() ErrorType { return ErrorTypeNotFound } func (e VillageNotFoundError) Code() string { return "village-not-found" } func (e VillageNotFoundError) Params() map[string]any { return map[string]any{ "ID": e.ID, "ServerKey": e.ServerKey, } } func formatVillageFullName(v interface { Name() string X() int Y() int Continent() string }) string { return fmt.Sprintf("%s (%d|%d) %s", v.Name(), v.X(), v.Y(), v.Continent()) }