From bf3a7b11e5077057e4e8502d06dc30640ba7d997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Tue, 20 Feb 2024 07:34:44 +0100 Subject: [PATCH] feat: api - GET /api/v2/versions/{versionCode}/servers/{serverKey}/tribes - add more sort options --- api/openapi3.yml | 23 +++ internal/domain/tribe.go | 60 ++++++++ internal/domain/tribe_test.go | 179 ++++++++++++++++++++++++ internal/domain/validation.go | 24 ++++ internal/port/handler_http_api_tribe.go | 15 +- 5 files changed, 300 insertions(+), 1 deletion(-) diff --git a/api/openapi3.yml b/api/openapi3.yml index 3f377ef..eb04ef8 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -142,6 +142,7 @@ paths: - $ref: "#/components/parameters/CursorQueryParam" - $ref: "#/components/parameters/LimitQueryParam" - $ref: "#/components/parameters/TribeDeletedQueryParam" + - $ref: "#/components/parameters/TribeSortQueryParam" responses: 200: $ref: "#/components/responses/ListTribesResponse" @@ -949,6 +950,28 @@ components: schema: type: boolean required: false + TribeSortQueryParam: + name: sort + in: query + description: Order matters! + schema: + type: array + items: + type: string + enum: + - odScoreAtt:ASC + - odScoreAtt:DESC + - odScoreDef:ASC + - odScoreDef:DESC + - odScoreTotal:ASC + - odScoreTotal:DESC + - points:ASC + - points:DESC + - dominance:ASC + - dominance:DESC + - deletedAt:ASC + - deletedAt:DESC + maxItems: 2 VersionCodePathParam: in: path name: versionCode diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index 15e538e..d8ef8e8 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -6,6 +6,7 @@ import ( "math" "net/url" "slices" + "strings" "time" ) @@ -380,6 +381,40 @@ const ( TribeSortDeletedAtDESC ) +//nolint:gocyclo +func newTribeSortFromString(s string) (TribeSort, error) { + switch strings.ToLower(s) { + case "odscoreatt:asc": + return TribeSortODScoreAttASC, nil + case "odscoreatt:desc": + return TribeSortODScoreAttDESC, nil + case "odscoredef:asc": + return TribeSortODScoreDefASC, nil + case "odscoredef:desc": + return TribeSortODScoreDefDESC, nil + case "odscoretotal:asc": + return TribeSortODScoreTotalASC, nil + case "odscoretotal:desc": + return TribeSortODScoreTotalDESC, nil + case "points:asc": + return TribeSortPointsASC, nil + case "points:desc": + return TribeSortPointsDESC, nil + case "dominance:asc": + return TribeSortDominanceASC, nil + case "dominance:desc": + return TribeSortDominanceDESC, nil + case "deletedat:asc": + return TribeSortDeletedAtASC, nil + case "deletedat:desc": + return TribeSortDeletedAtDESC, nil + default: + return 0, UnsupportedSortStringError{ + Sort: s, + } + } +} + type TribeCursor struct { id int serverKey string @@ -666,6 +701,31 @@ func (params *ListTribesParams) SetSort(sort []TribeSort) error { 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, + } + } + + for i := len(sort) - 1; i >= 0; i-- { + converted, err := newTribeSortFromString(sort[i]) + if err != nil { + return SliceElementValidationError{ + Model: listTribesParamsModelName, + Field: "sort", + Index: i, + Err: err, + } + } + params.sort = append([]TribeSort{converted}, params.sort...) + } + + return nil +} + func (params *ListTribesParams) Cursor() TribeCursor { return params.cursor } diff --git a/internal/domain/tribe_test.go b/internal/domain/tribe_test.go index 6e597f2..80643e4 100644 --- a/internal/domain/tribe_test.go +++ b/internal/domain/tribe_test.go @@ -510,6 +510,185 @@ func TestListTribesParams_SetSort(t *testing.T) { } } +func TestListTribesParams_PrependSortString(t *testing.T) { + t.Parallel() + + defaultNewParams := func(t *testing.T) domain.ListTribesParams { + t.Helper() + return domain.ListTribesParams{} + } + + type args struct { + sort []string + } + + tests := []struct { + name string + newParams func(t *testing.T) domain.ListTribesParams + args args + expectedSort []domain.TribeSort + expectedErr error + }{ + { + name: "OK: [odScoreAtt:ASC, odScoreDef:ASC, odScoreTotal:ASC]", + args: args{ + sort: []string{ + "odScoreAtt:ASC", + "odScoreDef:ASC", + "odScoreTotal:ASC", + }, + }, + expectedSort: []domain.TribeSort{ + domain.TribeSortODScoreAttASC, + domain.TribeSortODScoreDefASC, + domain.TribeSortODScoreTotalASC, + }, + }, + { + name: "OK: [odScoreAtt:DESC, odScoreDef:DESC, odScoreTotal:DESC]", + args: args{ + sort: []string{ + "odScoreAtt:DESC", + "odScoreDef:DESC", + "odScoreTotal:DESC", + }, + }, + expectedSort: []domain.TribeSort{ + domain.TribeSortODScoreAttDESC, + domain.TribeSortODScoreDefDESC, + domain.TribeSortODScoreTotalDESC, + }, + }, + { + name: "OK: [points:ASC, dominance:ASC, deletedAt:ASC]", + args: args{ + sort: []string{ + "points:ASC", + "dominance:ASC", + "deletedAt:ASC", + }, + }, + expectedSort: []domain.TribeSort{ + domain.TribeSortPointsASC, + domain.TribeSortDominanceASC, + domain.TribeSortDeletedAtASC, + }, + }, + { + name: "OK: [points:DESC, dominance:DESC, deletedAt:DESC]", + args: args{ + sort: []string{ + "points:DESC", + "dominance:DESC", + "deletedAt:DESC", + }, + }, + expectedSort: []domain.TribeSort{ + domain.TribeSortPointsDESC, + domain.TribeSortDominanceDESC, + domain.TribeSortDeletedAtDESC, + }, + }, + { + name: "ERR: len(sort) < 1", + args: args{ + sort: nil, + }, + expectedErr: domain.ValidationError{ + Model: "ListTribesParams", + Field: "sort", + Err: domain.LenOutOfRangeError{ + Min: 1, + Max: 3, + Current: 0, + }, + }, + }, + { + name: "ERR: len(sort) > 3", + args: args{ + sort: []string{ + "odScoreAtt:ASC", + "odScoreDef:ASC", + "odScoreTotal:ASC", + "points:ASC", + }, + }, + expectedErr: domain.ValidationError{ + Model: "ListTribesParams", + Field: "sort", + Err: domain.LenOutOfRangeError{ + Min: 1, + Max: 3, + Current: 4, + }, + }, + }, + { + name: "ERR: custom params + len(sort) > 2", + newParams: func(t *testing.T) domain.ListTribesParams { + t.Helper() + params := domain.NewListTribesParams() + require.NoError(t, params.SetSort([]domain.TribeSort{domain.TribeSortIDASC})) + return params + }, + args: args{ + sort: []string{ + "odScoreAtt:ASC", + "odScoreDef:ASC", + "odScoreTotal:ASC", + }, + }, + expectedErr: domain.ValidationError{ + Model: "ListTribesParams", + Field: "sort", + Err: domain.LenOutOfRangeError{ + Min: 1, + Max: 2, + Current: 3, + }, + }, + }, + { + name: "ERR: unsupported sort string", + newParams: defaultNewParams, + args: args{ + sort: []string{ + "odScoreAtt:ASC", + "odScoreDef:ASC", + "odScoreTotal:", + }, + }, + expectedErr: domain.SliceElementValidationError{ + Model: "ListTribesParams", + Field: "sort", + Index: 2, + Err: domain.UnsupportedSortStringError{ + Sort: "odScoreTotal:", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + newParams := defaultNewParams + if tt.newParams != nil { + newParams = tt.newParams + } + params := newParams(t) + + require.ErrorIs(t, params.PrependSortString(tt.args.sort), tt.expectedErr) + if tt.expectedErr != nil { + return + } + assert.Equal(t, tt.expectedSort, params.Sort()) + }) + } +} + func TestListTribesParams_SetEncodedCursor(t *testing.T) { t.Parallel() diff --git a/internal/domain/validation.go b/internal/domain/validation.go index 399c326..caffbb9 100644 --- a/internal/domain/validation.go +++ b/internal/domain/validation.go @@ -229,6 +229,30 @@ func (e InvalidURLError) Params() map[string]any { } } +type UnsupportedSortStringError struct { + Sort string +} + +var _ ErrorWithParams = UnsupportedSortStringError{} + +func (e UnsupportedSortStringError) Error() string { + return fmt.Sprintf("sort %s is unsupported", e.Sort) +} + +func (e UnsupportedSortStringError) Type() ErrorType { + return ErrorTypeIncorrectInput +} + +func (e UnsupportedSortStringError) Code() string { + return "unsupported-sort-string" +} + +func (e UnsupportedSortStringError) Params() map[string]any { + return map[string]any{ + "Sort": e.Sort, + } +} + var ErrRequired error = simpleError{ msg: "can't be blank", typ: ErrorTypeIncorrectInput, diff --git a/internal/port/handler_http_api_tribe.go b/internal/port/handler_http_api_tribe.go index 262df3e..f8f1e6b 100644 --- a/internal/port/handler_http_api_tribe.go +++ b/internal/port/handler_http_api_tribe.go @@ -2,11 +2,13 @@ package port import ( "net/http" + "strconv" "gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel" ) +//nolint:gocyclo func (h *apiHTTPHandler) ListTribes( w http.ResponseWriter, r *http.Request, @@ -21,6 +23,13 @@ func (h *apiHTTPHandler) ListTribes( return } + if params.Sort != nil { + if err := domainParams.PrependSortString(*params.Sort); err != nil { + apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListTribesParamsErrorPath}.render(w, r) + return + } + } + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListTribesParamsErrorPath}.render(w, r) return @@ -72,7 +81,11 @@ func formatListTribesParamsErrorPath(segments []errorPathSegment) []string { case "deleted": return []string{"$query", "deleted"} case "sort": - return []string{"$query", "sort"} + path := []string{"$query", "sort"} + if segments[0].index >= 0 { + path = append(path, strconv.Itoa(segments[0].index)) + } + return path default: return nil }