From b2fa4849025effa77eeb854c457ea3e3a73b0400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Mon, 11 Mar 2024 06:41:32 +0000 Subject: [PATCH] feat: new API endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/ennoblements (#22) Reviewed-on: https://gitea.dwysokinski.me/twhelp/corev3/pulls/22 --- api/openapi3.yml | 82 ++++ cmd/twhelp/cmd_serve.go | 3 + internal/domain/player.go | 10 + internal/domain/village.go | 20 + internal/health/config.go | 4 +- internal/health/healthhttp/handler.go | 10 +- internal/port/handler_http_api.go | 27 +- internal/port/handler_http_api_ennoblement.go | 65 +++ .../port/handler_http_api_ennoblement_test.go | 419 ++++++++++++++++++ internal/port/handler_http_api_test.go | 2 + internal/port/handler_http_meta.go | 7 + .../port/internal/apimodel/ennoblement.go | 36 ++ internal/port/internal/apimodel/village.go | 19 +- 13 files changed, 687 insertions(+), 17 deletions(-) create mode 100644 internal/port/handler_http_api_ennoblement.go create mode 100644 internal/port/handler_http_api_ennoblement_test.go create mode 100644 internal/port/internal/apimodel/ennoblement.go diff --git a/api/openapi3.yml b/api/openapi3.yml index 527442d..3fff94a 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -15,6 +15,7 @@ tags: - name: tribes - name: players - name: villages + - name: ennoblements servers: - url: "{scheme}://{hostname}/api" variables: @@ -303,6 +304,24 @@ paths: $ref: "#/components/responses/ListVillagesResponse" default: $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}/servers/{serverKey}/ennoblements: + get: + operationId: listEnnoblements + tags: + - versions + - servers + - ennoblements + description: List ennoblements + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + - $ref: "#/components/parameters/ServerKeyPathParam" + - $ref: "#/components/parameters/CursorQueryParam" + - $ref: "#/components/parameters/LimitQueryParam" + responses: + 200: + $ref: "#/components/responses/ListEnnoblementsResponse" + default: + $ref: "#/components/responses/ErrorResponse" components: schemas: Error: @@ -1225,6 +1244,54 @@ components: format: date-time player: $ref: "#/components/schemas/PlayerMeta" + VillageMeta: + type: object + required: + - id + - fullName + - x + - y + - profileUrl + - continent + properties: + id: + $ref: "#/components/schemas/IntId" + fullName: + type: string + example: Village (450|450) K44 + x: + type: integer + example: 450 + y: + type: integer + example: 450 + profileUrl: + type: string + format: uri + continent: + type: string + example: K44 + player: + $ref: "#/components/schemas/PlayerMeta" + Ennoblement: + type: object + required: + - id + - points + - village + - createdAt + properties: + id: + $ref: "#/components/schemas/IntId" + points: + type: integer + newOwner: + $ref: "#/components/schemas/PlayerMeta" + village: + $ref: "#/components/schemas/VillageMeta" + createdAt: + type: string + format: date-time Cursor: type: object x-go-type-skip-optional-pointer: true @@ -1551,6 +1618,21 @@ components: properties: data: $ref: "#/components/schemas/Village" + ListEnnoblementsResponse: + description: "" + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/PaginationResponse" + - type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/Ennoblement" ErrorResponse: description: Default error response. content: diff --git a/cmd/twhelp/cmd_serve.go b/cmd/twhelp/cmd_serve.go index 39592d7..0357ed1 100644 --- a/cmd/twhelp/cmd_serve.go +++ b/cmd/twhelp/cmd_serve.go @@ -109,6 +109,7 @@ var cmdServe = &cli.Command{ tribeRepo := adapter.NewTribeBunRepository(bunDB) playerRepo := adapter.NewPlayerBunRepository(bunDB) villageRepo := adapter.NewVillageBunRepository(bunDB) + ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB) // services versionSvc := app.NewVersionService(versionRepo) @@ -116,6 +117,7 @@ var cmdServe = &cli.Command{ tribeSvc := app.NewTribeService(tribeRepo, nil, nil) playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil) villageSvc := app.NewVillageService(villageRepo, nil, nil) + ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil) // health h := health.New() @@ -144,6 +146,7 @@ var cmdServe = &cli.Command{ tribeSvc, playerSvc, villageSvc, + ennoblementSvc, port.WithOpenAPIConfig(oapiCfg), )) diff --git a/internal/domain/player.go b/internal/domain/player.go index c37b19e..30abdee 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -353,6 +353,16 @@ func (p PlayerMeta) WithRelations(tribe NullTribeMeta) PlayerMetaWithRelations { type NullPlayerMeta NullValue[PlayerMeta] +func (p NullPlayerMeta) WithRelations(tribe NullTribeMeta) NullPlayerMetaWithRelations { + return NullPlayerMetaWithRelations{ + V: PlayerMetaWithRelations{ + player: p.V, + tribe: tribe, + }, + Valid: !p.IsZero(), + } +} + func (p NullPlayerMeta) IsZero() bool { return !p.Valid } diff --git a/internal/domain/village.go b/internal/domain/village.go index 98c3a5b..666f6fc 100644 --- a/internal/domain/village.go +++ b/internal/domain/village.go @@ -313,6 +313,26 @@ 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 diff --git a/internal/health/config.go b/internal/health/config.go index bbbe273..536991c 100644 --- a/internal/health/config.go +++ b/internal/health/config.go @@ -1,5 +1,7 @@ package health +import "runtime" + type config struct { liveChecks []Checker readyChecks []Checker @@ -12,7 +14,7 @@ const maxConcurrentDefault = 5 func newConfig(opts ...Option) *config { cfg := &config{ - maxConcurrent: maxConcurrentDefault, + maxConcurrent: min(runtime.NumCPU(), maxConcurrentDefault), } for _, opt := range opts { diff --git a/internal/health/healthhttp/handler.go b/internal/health/healthhttp/handler.go index ba1ce91..3ed3cc6 100644 --- a/internal/health/healthhttp/handler.go +++ b/internal/health/healthhttp/handler.go @@ -39,7 +39,15 @@ type response struct { } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - result := h.check(r.Context()) + ctx := r.Context() + + result := h.check(ctx) + + select { + case <-ctx.Done(): + return + default: + } respChecks := make(map[string][]singleCheckResult, len(result.Checks)) for name, checks := range result.Checks { diff --git a/internal/port/handler_http_api.go b/internal/port/handler_http_api.go index 2c416d6..f6d7ff8 100644 --- a/internal/port/handler_http_api.go +++ b/internal/port/handler_http_api.go @@ -13,13 +13,14 @@ import ( ) type apiHTTPHandler struct { - versionSvc *app.VersionService - serverSvc *app.ServerService - tribeSvc *app.TribeService - playerSvc *app.PlayerService - villageSvc *app.VillageService - errorRenderer apiErrorRenderer - openAPISchema func() (*openapi3.T, error) + versionSvc *app.VersionService + serverSvc *app.ServerService + tribeSvc *app.TribeService + playerSvc *app.PlayerService + villageSvc *app.VillageService + ennoblementSvc *app.EnnoblementService + errorRenderer apiErrorRenderer + openAPISchema func() (*openapi3.T, error) } func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption { @@ -34,16 +35,18 @@ func NewAPIHTTPHandler( tribeSvc *app.TribeService, playerSvc *app.PlayerService, villageSvc *app.VillageService, + ennoblementSvc *app.EnnoblementService, opts ...APIHTTPHandlerOption, ) http.Handler { cfg := newAPIHTTPHandlerConfig(opts...) h := &apiHTTPHandler{ - versionSvc: versionSvc, - serverSvc: serverSvc, - tribeSvc: tribeSvc, - playerSvc: playerSvc, - villageSvc: villageSvc, + versionSvc: versionSvc, + serverSvc: serverSvc, + tribeSvc: tribeSvc, + playerSvc: playerSvc, + villageSvc: villageSvc, + ennoblementSvc: ennoblementSvc, openAPISchema: sync.OnceValues(func() (*openapi3.T, error) { return getOpenAPISchema(cfg.openAPI) }), diff --git a/internal/port/handler_http_api_ennoblement.go b/internal/port/handler_http_api_ennoblement.go new file mode 100644 index 0000000..f2c12dc --- /dev/null +++ b/internal/port/handler_http_api_ennoblement.go @@ -0,0 +1,65 @@ +package port + +import ( + "net/http" + + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" + "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel" +) + +func (h *apiHTTPHandler) ListEnnoblements( + w http.ResponseWriter, + r *http.Request, + _ apimodel.VersionCodePathParam, + serverKey apimodel.ServerKeyPathParam, + params apimodel.ListEnnoblementsParams, +) { + domainParams := domain.NewListEnnoblementsParams() + + if err := domainParams.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if err := domainParams.SetServerKeys([]string{serverKey}); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + + if params.Limit != nil { + if err := domainParams.SetLimit(*params.Limit); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + if params.Cursor != nil { + if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil { + h.errorRenderer.withErrorPathFormatter(formatListEnnoblementsErrorPath).render(w, r, err) + return + } + } + + res, err := h.ennoblementSvc.ListWithRelations(r.Context(), domainParams) + if err != nil { + h.errorRenderer.render(w, r, err) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewListEnnoblementsResponse(res)) +} + +func formatListEnnoblementsErrorPath(segments []domain.ErrorPathSegment) []string { + if segments[0].Model != "ListEnnoblementsParams" { + return nil + } + + switch segments[0].Field { + case "cursor": + return []string{"$query", "cursor"} + case "limit": + return []string{"$query", "limit"} + default: + return nil + } +} diff --git a/internal/port/handler_http_api_ennoblement_test.go b/internal/port/handler_http_api_ennoblement_test.go new file mode 100644 index 0000000..0d6df65 --- /dev/null +++ b/internal/port/handler_http_api_ennoblement_test.go @@ -0,0 +1,419 @@ +package port_test + +import ( + "cmp" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "strconv" + "strings" + "testing" + + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" + "gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest" + "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel" + "github.com/brianvoe/gofakeit/v7" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const endpointListEnnoblements = "/v2/versions/%s/servers/%s/ennoblements" + +func TestListEnnoblements(t *testing.T) { + t.Parallel() + + handler := newAPIHTTPHandler(t) + ennoblements := getAllEnnoblements(t, handler) + server := ennoblements[0].Server + + tests := []struct { + name string + reqModifier func(t *testing.T, req *http.Request) + assertResp func(t *testing.T, req *http.Request, resp *http.Response) + }{ + { + name: "OK: without params", + assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body) + assert.Zero(t, body.Cursor.Next) + assert.NotZero(t, body.Cursor.Self) + assert.NotZero(t, body.Data) + assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.Ennoblement) int { + return cmp.Compare(a.Id, b.Id) + })) + }, + }, + { + name: "OK: limit=1", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("limit", "1") + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body) + assert.NotZero(t, body.Cursor.Next) + assert.NotZero(t, body.Cursor.Self) + assert.NotZero(t, body.Data) + limit, err := strconv.Atoi(req.URL.Query().Get("limit")) + require.NoError(t, err) + assert.Len(t, body.Data, limit) + }, + }, + { + name: "OK: limit=1 cursor", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + + q := req.URL.Query() + q.Set("limit", "1") + req.URL.RawQuery = q.Encode() + + resp := doCustomRequest(handler, req.Clone(req.Context())) + defer resp.Body.Close() + body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body) + require.NotEmpty(t, body.Cursor.Next) + + q.Set("cursor", body.Cursor.Next) + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ListEnnoblementsResponse](t, resp.Body) + assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self) + limit, err := strconv.Atoi(req.URL.Query().Get("limit")) + require.NoError(t, err) + assert.Len(t, body.Data, limit) + }, + }, + { + name: "ERR: limit is not a string", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("limit", "asd") + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: "invalid-param-format", + Message: fmt.Sprintf( + "error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax", + req.URL.Query().Get("limit"), + ), + Path: []string{"$query", "limit"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: limit < 1", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("limit", "0") + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + limit, err := strconv.Atoi(req.URL.Query().Get("limit")) + require.NoError(t, err) + domainErr := domain.MinGreaterEqualError{ + Min: 1, + Current: limit, + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "current": float64(domainErr.Current), + "min": float64(domainErr.Min), + }, + Path: []string{"$query", "limit"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: limit > 200", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("limit", "201") + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + limit, err := strconv.Atoi(req.URL.Query().Get("limit")) + require.NoError(t, err) + domainErr := domain.MaxLessEqualError{ + Max: 200, + Current: limit, + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "current": float64(domainErr.Current), + "max": float64(domainErr.Max), + }, + Path: []string{"$query", "limit"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: len(cursor) < 1", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("cursor", "") + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + domainErr := domain.LenOutOfRangeError{ + Min: 1, + Max: 1000, + Current: len(req.URL.Query().Get("cursor")), + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "current": float64(domainErr.Current), + "max": float64(domainErr.Max), + "min": float64(domainErr.Min), + }, + Path: []string{"$query", "cursor"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: len(cursor) > 1000", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("cursor", gofakeit.LetterN(1001)) + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + domainErr := domain.LenOutOfRangeError{ + Min: 1, + Max: 1000, + Current: len(req.URL.Query().Get("cursor")), + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "current": float64(domainErr.Current), + "max": float64(domainErr.Max), + "min": float64(domainErr.Min), + }, + Path: []string{"$query", "cursor"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: invalid cursor", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + q := req.URL.Query() + q.Set("cursor", gofakeit.LetterN(100)) + req.URL.RawQuery = q.Encode() + }, + assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + var domainErr domain.Error + require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr) + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Path: []string{"$query", "cursor"}, + }, + }, + }, body) + }, + }, + { + name: "ERR: version not found", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf(endpointListEnnoblements, domaintest.RandVersionCode(), server.Key) + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + pathSegments := strings.Split(req.URL.Path, "/") + require.Len(t, pathSegments, 7) + domainErr := domain.VersionNotFoundError{ + VersionCode: pathSegments[3], + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "code": domainErr.VersionCode, + }, + }, + }, + }, body) + }, + }, + { + name: "ERR: server not found", + reqModifier: func(t *testing.T, req *http.Request) { + t.Helper() + req.URL.Path = fmt.Sprintf(endpointListEnnoblements, server.Version.Code, domaintest.RandServerKey()) + }, + assertResp: func(t *testing.T, req *http.Request, resp *http.Response) { + t.Helper() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + // body + body := decodeJSON[apimodel.ErrorResponse](t, resp.Body) + pathSegments := strings.Split(req.URL.Path, "/") + require.Len(t, pathSegments, 7) + domainErr := domain.ServerNotFoundError{ + Key: pathSegments[5], + } + assert.Equal(t, apimodel.ErrorResponse{ + Errors: []apimodel.Error{ + { + Code: domainErr.Code(), + Message: domainErr.Error(), + Params: map[string]any{ + "key": domainErr.Key, + }, + }, + }, + }, body) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest( + http.MethodGet, + fmt.Sprintf(endpointListEnnoblements, server.Version.Code, server.Key), + nil, + ) + if tt.reqModifier != nil { + tt.reqModifier(t, req) + } + + resp := doCustomRequest(handler, req) + defer resp.Body.Close() + tt.assertResp(t, req, resp) + }) + } +} + +type ennoblementWithServer struct { + apimodel.Ennoblement + Server serverWithVersion +} + +func getAllEnnoblements(tb testing.TB, h http.Handler) []ennoblementWithServer { + tb.Helper() + + servers := getAllServers(tb, h) + + var ennoblements []ennoblementWithServer + + for _, s := range servers { + resp := doRequest(h, http.MethodGet, fmt.Sprintf(endpointListEnnoblements, s.Version.Code, s.Key), nil) + require.Equal(tb, http.StatusOK, resp.StatusCode) + + for _, e := range decodeJSON[apimodel.ListEnnoblementsResponse](tb, resp.Body).Data { + ennoblements = append(ennoblements, ennoblementWithServer{ + Ennoblement: e, + Server: s, + }) + } + + _ = resp.Body.Close() + } + + require.NotZero(tb, ennoblements) + + return ennoblements +} diff --git a/internal/port/handler_http_api_test.go b/internal/port/handler_http_api_test.go index 6cfae14..9830964 100644 --- a/internal/port/handler_http_api_test.go +++ b/internal/port/handler_http_api_test.go @@ -38,6 +38,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h tribeRepo := adapter.NewTribeBunRepository(bunDB) playerRepo := adapter.NewPlayerBunRepository(bunDB) villageRepo := adapter.NewVillageBunRepository(bunDB) + ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB) return port.NewAPIHTTPHandler( app.NewVersionService(versionRepo), @@ -45,6 +46,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h app.NewTribeService(tribeRepo, nil, nil), app.NewPlayerService(playerRepo, nil, nil, nil), app.NewVillageService(villageRepo, nil, nil), + app.NewEnnoblementService(ennoblementRepo, nil, nil), cfg.options..., ) } diff --git a/internal/port/handler_http_meta.go b/internal/port/handler_http_meta.go index 960d7ba..15d9b6c 100644 --- a/internal/port/handler_http_meta.go +++ b/internal/port/handler_http_meta.go @@ -2,15 +2,22 @@ package port import ( "net/http" + "time" "gitea.dwysokinski.me/twhelp/corev3/internal/health" "gitea.dwysokinski.me/twhelp/corev3/internal/health/healthhttp" "github.com/go-chi/chi/v5" ) +const metaHandlerTimeout = 5 * time.Second + func NewMetaHTTPHandler(h *health.Health) http.Handler { r := chi.NewRouter() + r.Use(func(next http.Handler) http.Handler { + return http.TimeoutHandler(next, metaHandlerTimeout, "Timeout") + }) + r.Get("/livez", healthhttp.LiveHandler(h).ServeHTTP) r.Get("/readyz", healthhttp.ReadyHandler(h).ServeHTTP) diff --git a/internal/port/internal/apimodel/ennoblement.go b/internal/port/internal/apimodel/ennoblement.go new file mode 100644 index 0000000..14d5623 --- /dev/null +++ b/internal/port/internal/apimodel/ennoblement.go @@ -0,0 +1,36 @@ +package apimodel + +import ( + "gitea.dwysokinski.me/twhelp/corev3/internal/domain" +) + +func NewEnnoblement(withRelations domain.EnnoblementWithRelations) Ennoblement { + e := withRelations.Ennoblement() + return Ennoblement{ + CreatedAt: e.CreatedAt(), + Id: e.ID(), + Points: e.Points(), + Village: NewVillageMeta( + withRelations.Village().WithRelations(withRelations.OldOwner().WithRelations(withRelations.OldTribe())), + ), + NewOwner: NewNullPlayerMeta(withRelations.NewOwner().WithRelations(withRelations.NewTribe())), + } +} + +func NewListEnnoblementsResponse(res domain.ListEnnoblementsWithRelationsResult) ListEnnoblementsResponse { + ennoblements := res.Ennoblements() + + resp := ListEnnoblementsResponse{ + Data: make([]Ennoblement, 0, len(ennoblements)), + Cursor: Cursor{ + Next: res.Next().Encode(), + Self: res.Self().Encode(), + }, + } + + for _, e := range ennoblements { + resp.Data = append(resp.Data, NewEnnoblement(e)) + } + + return resp +} diff --git a/internal/port/internal/apimodel/village.go b/internal/port/internal/apimodel/village.go index 0bd6699..2e67402 100644 --- a/internal/port/internal/apimodel/village.go +++ b/internal/port/internal/apimodel/village.go @@ -21,18 +21,31 @@ func NewVillage(withRelations domain.VillageWithRelations) Village { } } +func NewVillageMeta(withRelations domain.VillageMetaWithRelations) VillageMeta { + v := withRelations.Village() + return VillageMeta{ + Continent: v.Continent(), + FullName: v.FullName(), + Id: v.ID(), + Player: NewNullPlayerMeta(withRelations.Player()), + ProfileUrl: v.ProfileURL().String(), + X: v.X(), + Y: v.Y(), + } +} + func NewListVillagesResponse(res domain.ListVillagesWithRelationsResult) ListVillagesResponse { - versions := res.Villages() + villages := res.Villages() resp := ListVillagesResponse{ - Data: make([]Village, 0, len(versions)), + Data: make([]Village, 0, len(villages)), Cursor: Cursor{ Next: res.Next().Encode(), Self: res.Self().Encode(), }, } - for _, v := range versions { + for _, v := range villages { resp.Data = append(resp.Data, NewVillage(v)) }