core/internal/domain/village.go

868 lines
17 KiB
Go

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 {
if v.profileURL == nil {
return &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{}
}
// active villages must be sorted in ascending order by ID
func (v Village) canBeDeleted(serverKey string, active BaseVillages) bool {
if v.serverKey != serverKey {
return false
}
_, found := slices.BinarySearchFunc(active, v, func(a BaseVillage, b Village) int {
return cmp.Compare(a.ID(), b.id)
})
return !found
}
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 this slice
//nolint:prealloc
var toDelete []int
for _, v := range vs {
if !v.canBeDeleted(serverKey, active) {
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 {
if v.profileURL == nil {
return &url.URL{}
}
return v.profileURL
}
func (v VillageMeta) IsZero() bool {
return v == VillageMeta{}
}
func (v VillageMeta) WithRelations(player NullPlayerMetaWithRelations) VillageMetaWithRelations {
return VillageMetaWithRelations{
village: v,
player: player,
}
}
type VillageMetaWithRelations struct {
village VillageMeta
player NullPlayerMetaWithRelations
}
func (v VillageMetaWithRelations) Village() VillageMeta {
return v.village
}
func (v VillageMetaWithRelations) Player() NullPlayerMetaWithRelations {
return v.player
}
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 {
return isSortInConflict(s, s2)
}
//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 = 0
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
}
const errorCodeVillageNotFound = "village-not-found"
func (e VillageNotFoundError) Code() string {
return errorCodeVillageNotFound
}
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())
}