feat: tribe - sort - add more options (#63)

Reviewed-on: twhelp/corev3#63
This commit is contained in:
Dawid Wysokiński 2024-02-19 07:17:38 +00:00
parent 52c3ccb01b
commit e5d8ba5390
9 changed files with 877 additions and 91 deletions

View File

@ -264,20 +264,7 @@ func (a listServersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQue
}
case sortLen > 1:
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
switch sort[0] {
case domain.ServerSortKeyASC:
q = q.Where("server.key > ?", cursorKey)
case domain.ServerSortKeyDESC:
q = q.Where("server.key < ?", cursorKey)
case domain.ServerSortOpenASC:
q = q.Where("server.open > ?", cursorOpen)
case domain.ServerSortOpenDESC:
q = q.Where("server.open < ?", cursorOpen)
default:
return q.Err(errUnsupportedSortValue)
}
for i := 1; i < sortLen; i++ {
for i := 0; i < sortLen; i++ {
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
current := sort[i]
@ -296,15 +283,23 @@ func (a listServersParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQue
}
}
greaterSymbol := bun.Safe(">")
lessSymbol := bun.Safe("<")
if i == sortLen-1 {
greaterSymbol = ">="
lessSymbol = "<="
}
switch current {
case domain.ServerSortKeyASC:
q = q.Where("server.key >= ?", cursorKey)
q = q.Where("server.key ? ?", greaterSymbol, cursorKey)
case domain.ServerSortKeyDESC:
q = q.Where("server.key <= ?", cursorKey)
q = q.Where("server.key ? ?", lessSymbol, cursorKey)
case domain.ServerSortOpenASC:
q = q.Where("server.open >= ?", cursorOpen)
q = q.Where("server.open ? ?", greaterSymbol, cursorOpen)
case domain.ServerSortOpenDESC:
q = q.Where("server.open <= ?", cursorOpen)
q = q.Where("server.open ? ?", lessSymbol, cursorOpen)
default:
return q.Err(errUnsupportedSortValue)
}

View File

@ -183,6 +183,30 @@ func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Order("tribe.server_key ASC")
case domain.TribeSortServerKeyDESC:
q = q.Order("tribe.server_key DESC")
case domain.TribeSortODScoreAttASC:
q = q.Order("tribe.score_att ASC")
case domain.TribeSortODScoreAttDESC:
q = q.Order("tribe.score_att DESC")
case domain.TribeSortODScoreDefASC:
q = q.Order("tribe.score_def ASC")
case domain.TribeSortODScoreDefDESC:
q = q.Order("tribe.score_def DESC")
case domain.TribeSortODScoreTotalASC:
q = q.Order("tribe.score_total ASC")
case domain.TribeSortODScoreTotalDESC:
q = q.Order("tribe.score_total DESC")
case domain.TribeSortPointsASC:
q = q.Order("tribe.points ASC")
case domain.TribeSortPointsDESC:
q = q.Order("tribe.points DESC")
case domain.TribeSortDominanceASC:
q = q.Order("tribe.dominance ASC")
case domain.TribeSortDominanceDESC:
q = q.Order("tribe.dominance DESC")
case domain.TribeSortDeletedAtASC:
q = q.OrderExpr("COALESCE(tribe.deleted_at, ?) ASC", time.Time{})
case domain.TribeSortDeletedAtDESC:
q = q.OrderExpr("COALESCE(tribe.deleted_at, ?) DESC", time.Time{})
default:
return q.Err(errUnsupportedSortValue)
}
@ -198,8 +222,15 @@ func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuer
}
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
cursorID := a.params.Cursor().ID()
cursorServerKey := a.params.Cursor().ServerKey()
cursor := a.params.Cursor()
cursorID := cursor.ID()
cursorServerKey := cursor.ServerKey()
cursorODScoreAtt := cursor.ODScoreAtt()
cursorODScoreDef := cursor.ODScoreDef()
cursorODScoreTotal := cursor.ODScoreTotal()
cursorPoints := cursor.Points()
cursorDominance := cursor.Dominance()
cursorDeletedAt := cursor.DeletedAt()
sort := a.params.Sort()
sortLen := len(sort)
@ -213,27 +244,27 @@ func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuer
q = q.Where("tribe.id >= ?", cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id <= ?", cursorID)
case domain.TribeSortServerKeyASC, domain.TribeSortServerKeyDESC:
case domain.TribeSortServerKeyASC,
domain.TribeSortServerKeyDESC,
domain.TribeSortODScoreAttASC,
domain.TribeSortODScoreAttDESC,
domain.TribeSortODScoreDefASC,
domain.TribeSortODScoreDefDESC,
domain.TribeSortODScoreTotalASC,
domain.TribeSortODScoreTotalDESC,
domain.TribeSortPointsASC,
domain.TribeSortPointsDESC,
domain.TribeSortDominanceASC,
domain.TribeSortDominanceDESC,
domain.TribeSortDeletedAtASC,
domain.TribeSortDeletedAtDESC:
return q.Err(errSortNoUniqueField)
default:
return q.Err(errUnsupportedSortValue)
}
case sortLen > 1:
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
switch sort[0] {
case domain.TribeSortIDASC:
q = q.Where("tribe.id > ?", cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id < ?", cursorID)
case domain.TribeSortServerKeyASC:
q = q.Where("tribe.server_key > ?", cursorServerKey)
case domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key < ?", cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
for i := 1; i < sortLen; i++ {
for i := 0; i < sortLen; i++ {
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
current := sort[i]
@ -247,20 +278,70 @@ func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuer
case domain.TribeSortServerKeyASC,
domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key = ?", cursorServerKey)
case domain.TribeSortODScoreAttASC,
domain.TribeSortODScoreAttDESC:
q = q.Where("tribe.score_att = ?", cursorODScoreAtt)
case domain.TribeSortODScoreDefASC,
domain.TribeSortODScoreDefDESC:
q = q.Where("tribe.score_def = ?", cursorODScoreDef)
case domain.TribeSortODScoreTotalASC,
domain.TribeSortODScoreTotalDESC:
q = q.Where("tribe.score_total = ?", cursorODScoreTotal)
case domain.TribeSortPointsASC,
domain.TribeSortPointsDESC:
q = q.Where("tribe.points = ?", cursorPoints)
case domain.TribeSortDominanceASC,
domain.TribeSortDominanceDESC:
q = q.Where("tribe.dominance = ?", cursorDominance)
case domain.TribeSortDeletedAtASC,
domain.TribeSortDeletedAtDESC:
q = q.Where("COALESCE(tribe.deleted_at, ?) = ?", time.Time{}, cursorDeletedAt)
default:
return q.Err(errUnsupportedSortValue)
}
}
greaterSymbol := bun.Safe(">")
lessSymbol := bun.Safe("<")
if i == sortLen-1 {
greaterSymbol = ">="
lessSymbol = "<="
}
switch current {
case domain.TribeSortIDASC:
q = q.Where("tribe.id >= ?", cursorID)
q = q.Where("tribe.id ? ?", greaterSymbol, cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id <= ?", cursorID)
q = q.Where("tribe.id ? ?", lessSymbol, cursorID)
case domain.TribeSortServerKeyASC:
q = q.Where("tribe.server_key >= ?", cursorServerKey)
q = q.Where("tribe.server_key ? ?", greaterSymbol, cursorServerKey)
case domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key <= ?", cursorServerKey)
q = q.Where("tribe.server_key ? ?", lessSymbol, cursorServerKey)
case domain.TribeSortODScoreAttASC:
q = q.Where("tribe.score_att ? ?", greaterSymbol, cursorODScoreAtt)
case domain.TribeSortODScoreAttDESC:
q = q.Where("tribe.score_att ? ?", lessSymbol, cursorODScoreAtt)
case domain.TribeSortODScoreDefASC:
q = q.Where("tribe.score_def ? ?", greaterSymbol, cursorODScoreDef)
case domain.TribeSortODScoreDefDESC:
q = q.Where("tribe.score_def ? ?", lessSymbol, cursorODScoreDef)
case domain.TribeSortODScoreTotalASC:
q = q.Where("tribe.score_total ? ?", greaterSymbol, cursorODScoreTotal)
case domain.TribeSortODScoreTotalDESC:
q = q.Where("tribe.score_total ? ?", lessSymbol, cursorODScoreTotal)
case domain.TribeSortPointsASC:
q = q.Where("tribe.points ? ?", greaterSymbol, cursorPoints)
case domain.TribeSortPointsDESC:
q = q.Where("tribe.points ? ?", lessSymbol, cursorPoints)
case domain.TribeSortDominanceASC:
q = q.Where("tribe.dominance ? ?", greaterSymbol, cursorDominance)
case domain.TribeSortDominanceDESC:
q = q.Where("tribe.dominance ? ?", lessSymbol, cursorDominance)
case domain.TribeSortDeletedAtASC:
q = q.Where("COALESCE(tribe.deleted_at, ?) ? ?", time.Time{}, greaterSymbol, cursorDeletedAt)
case domain.TribeSortDeletedAtDESC:
q = q.Where("COALESCE(tribe.deleted_at, ?) ? ?", time.Time{}, lessSymbol, cursorDeletedAt)
default:
return q.Err(errUnsupportedSortValue)
}

View File

@ -224,6 +224,209 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, err)
},
},
{
name: "OK: sort=[scoreAtt DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortODScoreAttDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreAtt(), b.OD().ScoreAtt())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[scoreDef DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortODScoreDefDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreDef(), b.OD().ScoreDef())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[scoreTotal DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortODScoreTotalDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.OD().ScoreTotal(), b.OD().ScoreTotal())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[points DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortPointsDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.Points(), b.Points())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[dominance ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortDominanceASC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.Dominance(), b.Dominance()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[deletedAt ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortDeletedAtASC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
a.DeletedAt().Compare(b.DeletedAt()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: sort=[deletedAt DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortDeletedAtDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
a.DeletedAt().Compare(b.DeletedAt())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListTribesParams {
@ -430,6 +633,98 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, err)
},
},
{
name: "OK: cursor sort=[points DESC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortPointsDESC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Tribes()), 2)
require.NoError(t, params.SetCursor(domaintest.NewTribeCursor(t, func(cfg *domaintest.TribeCursorConfig) {
cfg.ID = res.Tribes()[1].ID()
cfg.ServerKey = res.Tribes()[1].ServerKey()
cfg.Points = res.Tribes()[1].Points()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.Points(), b.Points())*-1,
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() {
assert.LessOrEqual(t, tr.Points(), params.Cursor().Points())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: cursor sort=[deletedAt ASC, serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortDeletedAtASC,
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Tribes()), 2)
require.NoError(t, params.SetCursor(domaintest.NewTribeCursor(t, func(cfg *domaintest.TribeCursorConfig) {
cfg.ID = res.Tribes()[1].ID()
cfg.ServerKey = res.Tribes()[1].ServerKey()
cfg.DeletedAt = res.Tribes()[1].DeletedAt()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
a.DeletedAt().Compare(b.DeletedAt()),
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
assert.GreaterOrEqual(t, tribes[0].ServerKey(), params.Cursor().ServerKey())
for _, tr := range res.Tribes() {
assert.GreaterOrEqual(t, tr.DeletedAt(), params.Cursor().DeletedAt())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListTribesParams {

View File

@ -1,6 +1,7 @@
package domaintest
import (
"math"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
@ -13,23 +14,44 @@ func RandTribeTag() string {
}
type TribeCursorConfig struct {
ID int
ServerKey string
ID int
ServerKey string
ODScoreAtt int
ODScoreDef int
ODScoreTotal int
Points int
Dominance float64
DeletedAt time.Time
}
func NewTribeCursor(tb TestingTB, opts ...func(cfg *TribeCursorConfig)) domain.TribeCursor {
tb.Helper()
cfg := &TribeCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
ID: RandID(),
ServerKey: RandServerKey(),
ODScoreAtt: gofakeit.IntRange(0, math.MaxInt),
ODScoreDef: gofakeit.IntRange(0, math.MaxInt),
ODScoreTotal: gofakeit.IntRange(0, math.MaxInt),
Points: gofakeit.IntRange(0, math.MaxInt),
Dominance: gofakeit.Float64Range(0.1, 99.9),
DeletedAt: time.Time{},
}
for _, opt := range opts {
opt(cfg)
}
tc, err := domain.NewTribeCursor(cfg.ID, cfg.ServerKey)
tc, err := domain.NewTribeCursor(
cfg.ID,
cfg.ServerKey,
cfg.ODScoreAtt,
cfg.ODScoreDef,
cfg.ODScoreTotal,
cfg.Points,
cfg.Dominance,
cfg.DeletedAt,
)
require.NoError(tb, err)
return tc

View File

@ -5,7 +5,6 @@ import (
"math"
"net/url"
"slices"
"strconv"
"time"
)
@ -494,12 +493,17 @@ func decodeServerCursor(encoded string) (ServerCursor, error) {
return ServerCursor{}, err
}
open, err := strconv.ParseBool(m["open"])
key, err := m.string("key")
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
vc, err := NewServerCursor(m["key"], open)
open, err := m.bool("open")
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
vc, err := NewServerCursor(key, open)
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
@ -524,9 +528,9 @@ func (sc ServerCursor) Encode() string {
return ""
}
return encodeCursor([][2]string{
return encodeCursor([]keyValuePair{
{"key", sc.key},
{"open", strconv.FormatBool(sc.open)},
{"open", sc.open},
})
}

View File

@ -6,7 +6,6 @@ import (
"math"
"net/url"
"slices"
"strconv"
"time"
)
@ -367,16 +366,43 @@ const (
TribeSortIDDESC
TribeSortServerKeyASC
TribeSortServerKeyDESC
TribeSortODScoreAttASC
TribeSortODScoreAttDESC
TribeSortODScoreDefASC
TribeSortODScoreDefDESC
TribeSortODScoreTotalASC
TribeSortODScoreTotalDESC
TribeSortPointsASC
TribeSortPointsDESC
TribeSortDominanceASC
TribeSortDominanceDESC
TribeSortDeletedAtASC
TribeSortDeletedAtDESC
)
type TribeCursor struct {
id int
serverKey string
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) (TribeCursor, error) {
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,
@ -393,24 +419,107 @@ func NewTribeCursor(id int, serverKey string) (TribeCursor, error) {
}
}
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,
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 := strconv.Atoi(m["id"])
id, err := m.int("id")
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
tc, err := NewTribeCursor(id, m["serverKey"])
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
}
@ -426,6 +535,30 @@ 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{}
}
@ -435,9 +568,15 @@ func (tc TribeCursor) Encode() string {
return ""
}
return encodeCursor([][2]string{
{"id", strconv.Itoa(tc.id)},
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},
})
}
@ -510,7 +649,7 @@ func (params *ListTribesParams) Sort() []TribeSort {
const (
tribeSortMinLength = 1
tribeSortMaxLength = 2
tribeSortMaxLength = 3
)
func (params *ListTribesParams) SetSort(sort []TribeSort) error {
@ -584,7 +723,17 @@ func NewListTribesResult(tribes Tribes, next Tribe) (ListTribesResult, error) {
}
if len(tribes) > 0 {
res.self, err = NewTribeCursor(tribes[0].ID(), tribes[0].ServerKey())
od := tribes[0].OD()
res.self, err = NewTribeCursor(
tribes[0].ID(),
tribes[0].ServerKey(),
od.ScoreAtt(),
od.ScoreDef(),
od.ScoreTotal(),
tribes[0].Points(),
tribes[0].Dominance(),
tribes[0].DeletedAt(),
)
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,
@ -595,7 +744,17 @@ func NewListTribesResult(tribes Tribes, next Tribe) (ListTribesResult, error) {
}
if !next.IsZero() {
res.next, err = NewTribeCursor(next.ID(), next.ServerKey())
od := next.OD()
res.next, err = NewTribeCursor(
next.ID(),
next.ServerKey(),
od.ScoreAtt(),
od.ScoreDef(),
od.ScoreTotal(),
next.Points(),
next.Dominance(),
next.DeletedAt(),
)
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,

View File

@ -186,8 +186,14 @@ func TestNewTribeCursor(t *testing.T) {
validTribeCursor := domaintest.NewTribeCursor(t)
type args struct {
id int
serverKey string
id int
serverKey string
odScoreAtt int
odScoreDef int
odScoreTotal int
points int
dominance float64
deletedAt time.Time
}
type test struct {
@ -200,16 +206,28 @@ func TestNewTribeCursor(t *testing.T) {
{
name: "OK",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validTribeCursor.ServerKey(),
id: 0,
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
@ -220,14 +238,104 @@ func TestNewTribeCursor(t *testing.T) {
},
},
},
{
name: "ERR: odScoreAtt < 0",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: -1,
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreAtt",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: odScoreDef < 0",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: -1,
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreDef",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: odScoreTotal < 0",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: -1,
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "odScoreTotal",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
{
name: "ERR: points < 0",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: -1,
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "points",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validTribeCursor.ID(),
serverKey: serverKeyTest.key,
id: validTribeCursor.ID(),
serverKey: serverKeyTest.key,
odScoreAtt: validTribeCursor.ODScoreAtt(),
odScoreDef: validTribeCursor.ODScoreDef(),
odScoreTotal: validTribeCursor.ODScoreTotal(),
points: validTribeCursor.Points(),
dominance: validTribeCursor.Dominance(),
deletedAt: validTribeCursor.DeletedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
@ -243,7 +351,16 @@ func TestNewTribeCursor(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tc, err := domain.NewTribeCursor(tt.args.id, tt.args.serverKey)
tc, err := domain.NewTribeCursor(
tt.args.id,
tt.args.serverKey,
tt.args.odScoreAtt,
tt.args.odScoreDef,
tt.args.odScoreTotal,
tt.args.points,
tt.args.dominance,
tt.args.deletedAt,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
@ -333,6 +450,7 @@ func TestListTribesParams_SetSort(t *testing.T) {
name: "OK",
args: args{
sort: []domain.TribeSort{
domain.TribeSortPointsASC,
domain.TribeSortIDASC,
domain.TribeSortServerKeyASC,
},
@ -348,18 +466,19 @@ func TestListTribesParams_SetSort(t *testing.T) {
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Max: 3,
Current: 0,
},
},
},
{
name: "ERR: len(sort) > 2",
name: "ERR: len(sort) > 3",
args: args{
sort: []domain.TribeSort{
domain.TribeSortDominanceASC,
domain.TribeSortPointsASC,
domain.TribeSortIDASC,
domain.TribeSortServerKeyASC,
domain.TribeSortServerKeyDESC,
},
},
expectedErr: domain.ValidationError{
@ -367,8 +486,8 @@ func TestListTribesParams_SetSort(t *testing.T) {
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 2,
Current: 3,
Max: 3,
Current: 4,
},
},
},

View File

@ -3,8 +3,11 @@ package domain
import (
"bytes"
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
func parseURL(rawURL string) (*url.URL, error) {
@ -23,38 +26,76 @@ const (
cursorKeyValueSeparator = "="
)
func encodeCursor(s [][2]string) string {
if len(s) == 0 {
type keyValuePair struct {
key string
value any
}
func (kvp keyValuePair) valueString() string {
switch v := kvp.value.(type) {
case string:
return v
case int:
return strconv.Itoa(v)
case bool:
return strconv.FormatBool(v)
case float64:
return strconv.FormatFloat(v, 'g', -1, 64)
case time.Time:
return v.Format(time.RFC3339)
case fmt.Stringer:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func encodeCursor(kvps []keyValuePair) string {
if len(kvps) == 0 {
return ""
}
n := len(cursorSeparator) * (len(s) - 1)
for _, el := range s {
n += len(el[0]) + len(el[1]) + len(cursorKeyValueSeparator)
values := make([]string, len(kvps))
n := len(cursorSeparator) * (len(kvps) - 1)
for i, kvp := range kvps {
value := kvp.valueString()
n += len(kvp.key) + len(value) + len(cursorKeyValueSeparator)
values[i] = value
}
var b bytes.Buffer
b.Grow(n)
for _, el := range s {
for i, kvp := range kvps {
if b.Len() > 0 {
_, _ = b.WriteString(cursorSeparator)
}
_, _ = b.WriteString(el[0])
_, _ = b.WriteString(kvp.key)
_, _ = b.WriteString(cursorKeyValueSeparator)
_, _ = b.WriteString(el[1])
_, _ = b.WriteString(values[i])
}
return base64.StdEncoding.EncodeToString(b.Bytes())
}
type cursorKeyNotFoundError struct {
key string
}
func (e cursorKeyNotFoundError) Error() string {
return "key " + e.key + " not found"
}
type decodedCursor map[string]string
const (
encodedCursorMinLength = 1
encodedCursorMaxLength = 1000
)
func decodeCursor(s string) (map[string]string, error) {
func decodeCursor(s string) (decodedCursor, error) {
if err := validateStringLen(s, encodedCursorMinLength, encodedCursorMaxLength); err != nil {
return nil, err
}
@ -65,12 +106,12 @@ func decodeCursor(s string) (map[string]string, error) {
}
decoded := string(decodedBytes)
kvs := strings.Split(decoded, cursorSeparator)
kvps := strings.Split(decoded, cursorSeparator)
m := make(map[string]string, len(kvs))
m := make(decodedCursor, len(kvps))
for _, kv := range kvs {
k, v, found := strings.Cut(kv, cursorKeyValueSeparator)
for _, kvp := range kvps {
k, v, found := strings.Cut(kvp, cursorKeyValueSeparator)
if !found {
return nil, ErrInvalidCursor
}
@ -79,3 +120,68 @@ func decodeCursor(s string) (map[string]string, error) {
return m, nil
}
func (c decodedCursor) string(k string) (string, error) {
val, ok := c[k]
if !ok {
return "", cursorKeyNotFoundError{k}
}
return val, nil
}
func (c decodedCursor) int(k string) (int, error) {
valStr, ok := c[k]
if !ok {
return 0, cursorKeyNotFoundError{k}
}
val, err := strconv.Atoi(valStr)
if err != nil {
return 0, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
}
return val, nil
}
func (c decodedCursor) float64(k string) (float64, error) {
valStr, ok := c[k]
if !ok {
return 0, cursorKeyNotFoundError{k}
}
val, err := strconv.ParseFloat(valStr, 64)
if err != nil {
return 0, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
}
return val, nil
}
func (c decodedCursor) bool(k string) (bool, error) {
valStr, ok := c[k]
if !ok {
return false, cursorKeyNotFoundError{k}
}
val, err := strconv.ParseBool(valStr)
if err != nil {
return false, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
}
return val, nil
}
func (c decodedCursor) time(k string) (time.Time, error) {
valStr, ok := c[k]
if !ok {
return time.Time{}, cursorKeyNotFoundError{k}
}
val, err := time.Parse(time.RFC3339, valStr)
if err != nil {
return time.Time{}, fmt.Errorf("couldn't parse value for key %s: %w", k, err)
}
return val, nil
}

View File

@ -131,7 +131,12 @@ func decodeVersionCursor(encoded string) (VersionCursor, error) {
return VersionCursor{}, err
}
vc, err := NewVersionCursor(m["code"])
code, err := m.string("code")
if err != nil {
return VersionCursor{}, ErrInvalidCursor
}
vc, err := NewVersionCursor(code)
if err != nil {
return VersionCursor{}, ErrInvalidCursor
}
@ -152,7 +157,7 @@ func (vc VersionCursor) Encode() string {
return ""
}
return encodeCursor([][2]string{
return encodeCursor([]keyValuePair{
{"code", vc.code},
})
}