From cde03857a28ecf8f57b7a9c3a53cec3273b03037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Wed, 14 Feb 2024 07:43:55 +0100 Subject: [PATCH] feat: api - add a new endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/tribes --- api/openapi3.yml | 130 +++++++++++++++++++++++ cmd/twhelp/cmd_serve.go | 3 + internal/port/handler_http_api.go | 3 + internal/port/handler_http_api_server.go | 2 - internal/port/handler_http_api_test.go | 2 + internal/port/handler_http_api_tribe.go | 79 ++++++++++++++ internal/port/internal/apimodel/tribe.go | 57 ++++++++++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 internal/port/handler_http_api_tribe.go create mode 100644 internal/port/internal/apimodel/tribe.go diff --git a/api/openapi3.yml b/api/openapi3.yml index 4e80810..0dacee1 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -12,6 +12,7 @@ info: tags: - name: versions - name: servers + - name: tribes servers: - url: "{scheme}://{hostname}/api" variables: @@ -127,6 +128,26 @@ paths: $ref: "#/components/responses/GetUnitInfoResponse" default: $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/tribes: + get: + operationId: listTribes + tags: + - versions + - servers + - tribes + description: List tribes + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + - $ref: "#/components/parameters/TribeDeletedQueryParam" + responses: + 200: + $ref: "#/components/responses/ListTribesResponse" + default: + $ref: "#/components/responses/ErrorResponse" + components: schemas: Error: @@ -165,6 +186,7 @@ components: example: pl host: type: string + format: hostname example: www.tribalwars.net name: type: string @@ -193,6 +215,7 @@ components: type: boolean url: type: string + format: uri example: https://pl151.plemiona.pl numPlayers: type: integer @@ -790,6 +813,88 @@ components: $ref: "#/components/schemas/Building" wood: $ref: "#/components/schemas/Building" + Tribe: + type: object + required: + - id + - name + - tag + - profileUrl + - points + - allPoints + - numMembers + - numVillages + - rank + - dominance + - rankAtt + - scoreAtt + - rankDef + - scoreDef + - rankTotal + - scoreTotal + - bestRank + - bestRankAt + - mostVillages + - mostVillagesAt + - mostPoints + - mostPointsAt + - createdAt + properties: + id: + type: integer + name: + type: string + tag: + type: string + profileUrl: + type: string + format: uri + points: + type: integer + allPoints: + type: integer + numMembers: + type: integer + numVillages: + type: integer + rank: + type: integer + dominance: + type: number + format: double + rankAtt: + type: integer + scoreAtt: + type: integer + rankDef: + type: integer + scoreDef: + type: integer + rankTotal: + type: integer + scoreTotal: + type: integer + bestRank: + type: integer + bestRankAt: + type: string + format: date-time + mostVillages: + type: integer + mostVillagesAt: + type: string + format: date-time + mostPoints: + type: integer + mostPointsAt: + type: string + format: date-time + createdAt: + type: string + format: date-time + deletedAt: + type: string + format: date-time Cursor: type: object x-go-type-skip-optional-pointer: true @@ -819,6 +924,7 @@ components: name: limit schema: type: integer + minimum: 0 required: false ServerOpenQueryParam: name: open @@ -828,6 +934,14 @@ components: schema: type: boolean required: false + TribeDeletedQueryParam: + name: deleted + in: query + description: true=only deleted tribes, false=only existing tribes, + by default both existing and deleted tribes are returned + schema: + type: boolean + required: false VersionCodePathParam: in: path name: versionCode @@ -926,6 +1040,22 @@ components: properties: data: $ref: "#/components/schemas/BuildingInfo" + ListTribesResponse: + description: "" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PaginationResponse" + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/Tribe" + ErrorResponse: description: Default error response. content: diff --git a/cmd/twhelp/cmd_serve.go b/cmd/twhelp/cmd_serve.go index c16bc36..2c480a9 100644 --- a/cmd/twhelp/cmd_serve.go +++ b/cmd/twhelp/cmd_serve.go @@ -106,10 +106,12 @@ var cmdServe = &cli.Command{ // adapters versionRepo := adapter.NewVersionBunRepository(bunDB) serverRepo := adapter.NewServerBunRepository(bunDB) + tribeRepo := adapter.NewTribeBunRepository(bunDB) // services versionSvc := app.NewVersionService(versionRepo) serverSvc := app.NewServerService(serverRepo, nil, nil) + tribeSvc := app.NewTribeService(tribeRepo, nil, nil) // health h := health.New() @@ -135,6 +137,7 @@ var cmdServe = &cli.Command{ r.Mount(apiBasePath, port.NewAPIHTTPHandler( versionSvc, serverSvc, + tribeSvc, port.WithOpenAPIConfig(oapiCfg), )) diff --git a/internal/port/handler_http_api.go b/internal/port/handler_http_api.go index 9b89c47..8f35de3 100644 --- a/internal/port/handler_http_api.go +++ b/internal/port/handler_http_api.go @@ -15,6 +15,7 @@ import ( type apiHTTPHandler struct { versionSvc *app.VersionService serverSvc *app.ServerService + tribeSvc *app.TribeService openAPISchema func() (*openapi3.T, error) } @@ -27,6 +28,7 @@ func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption { func NewAPIHTTPHandler( versionSvc *app.VersionService, serverSvc *app.ServerService, + tribeSvc *app.TribeService, opts ...APIHTTPHandlerOption, ) http.Handler { cfg := newAPIHTTPHandlerConfig(opts...) @@ -34,6 +36,7 @@ func NewAPIHTTPHandler( h := &apiHTTPHandler{ versionSvc: versionSvc, serverSvc: serverSvc, + tribeSvc: tribeSvc, openAPISchema: sync.OnceValues(func() (*openapi3.T, error) { return getOpenAPISchema(cfg.openAPI) }), diff --git a/internal/port/handler_http_api_server.go b/internal/port/handler_http_api_server.go index 738b374..6f304b1 100644 --- a/internal/port/handler_http_api_server.go +++ b/internal/port/handler_http_api_server.go @@ -152,8 +152,6 @@ func formatListServersParamsErrorPath(segments []errorPathSegment) []string { return []string{"$query", "open"} case "sort": return []string{"$query", "sort"} - case "versionCodes": - return []string{"$path", "versionCode"} default: return nil } diff --git a/internal/port/handler_http_api_test.go b/internal/port/handler_http_api_test.go index cfaf355..06a660e 100644 --- a/internal/port/handler_http_api_test.go +++ b/internal/port/handler_http_api_test.go @@ -29,10 +29,12 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h versionRepo := adapter.NewVersionBunRepository(db) serverRepo := adapter.NewServerBunRepository(db) + tribeRepo := adapter.NewTribeBunRepository(db) return port.NewAPIHTTPHandler( app.NewVersionService(versionRepo), app.NewServerService(serverRepo, nil, nil), + app.NewTribeService(tribeRepo, nil, nil), cfg.options..., ) } diff --git a/internal/port/handler_http_api_tribe.go b/internal/port/handler_http_api_tribe.go new file mode 100644 index 0000000..262df3e --- /dev/null +++ b/internal/port/handler_http_api_tribe.go @@ -0,0 +1,79 @@ +package port + +import ( + "net/http" + + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" + "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel" +) + +func (h *apiHTTPHandler) ListTribes( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + params apimodel.ListTribesParams, +) { + domainParams := domain.NewListTribesParams() + + if err := domainParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); 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 + } + + if params.Deleted != nil { + if err := domainParams.SetDeleted(domain.NullBool{ + Value: *params.Deleted, + Valid: true, + }); err != nil { + apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListTribesParamsErrorPath}.render(w, r) + return + } + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListTribesParamsErrorPath}.render(w, r) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + apiErrorRenderer{errors: []error{err}, formatErrorPath: formatListTribesParamsErrorPath}.render(w, r) + return + } + } + + res, err := h.tribeSvc.List(r.Context(), domainParams) + if err != nil { + apiErrorRenderer{errors: []error{err}}.render(w, r) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListTribesResponse(res)) +} + +func formatListTribesParamsErrorPath(segments []errorPathSegment) []string { + if segments[0].model != "ListTribesParams" { + return nil + } + + switch segments[0].field { + case "cursor": + return []string{"$query", "cursor"} + case "limit": + return []string{"$query", "limit"} + case "deleted": + return []string{"$query", "deleted"} + case "sort": + return []string{"$query", "sort"} + default: + return nil + } +} diff --git a/internal/port/internal/apimodel/tribe.go b/internal/port/internal/apimodel/tribe.go new file mode 100644 index 0000000..7503a23 --- /dev/null +++ b/internal/port/internal/apimodel/tribe.go @@ -0,0 +1,57 @@ +package apimodel + +import ( + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" +) + +func NewTribe(t domain.Tribe) Tribe { + converted := Tribe{ + AllPoints: t.AllPoints(), + BestRank: t.BestRank(), + BestRankAt: t.BestRankAt(), + CreatedAt: t.CreatedAt(), + Dominance: t.Dominance(), + Id: t.ID(), + MostPoints: t.MostPoints(), + MostPointsAt: t.MostPointsAt(), + MostVillages: t.MostVillages(), + MostVillagesAt: t.MostVillagesAt(), + Name: t.Name(), + NumMembers: t.NumMembers(), + NumVillages: t.NumVillages(), + Points: t.Points(), + ProfileUrl: t.ProfileURL().String(), + Rank: t.Rank(), + RankAtt: t.OD().RankAtt(), + RankDef: t.OD().RankDef(), + RankTotal: t.OD().RankTotal(), + ScoreAtt: t.OD().ScoreAtt(), + ScoreDef: t.OD().ScoreDef(), + ScoreTotal: t.OD().ScoreTotal(), + Tag: t.Tag(), + } + + if deletedAt := t.DeletedAt(); !deletedAt.IsZero() { + converted.DeletedAt = &deletedAt + } + + return converted +} + +func NewListTribesResponse(res domain.ListTribesResult) ListTribesResponse { + tribes := res.Tribes() + + resp := ListTribesResponse{ + Data: make([]Tribe, 0, len(tribes)), + Cursor: Cursor{ + Next: res.Next().Encode(), + Self: res.Self().Encode(), + }, + } + + for _, t := range tribes { + resp.Data = append(resp.Data, NewTribe(t)) + } + + return resp +}