core/internal/domain/village.go

604 lines
12 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 fmt.Sprintf("%s (%d|%d) %s", v.name, v.x, v.y, v.continent)
}
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) 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) 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 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
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) 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
}