feat: new API endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/ennoblements (#22)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#22
This commit is contained in:
Dawid Wysokiński 2024-03-11 06:41:32 +00:00
parent 01ee81ab6e
commit b2fa484902
13 changed files with 687 additions and 17 deletions

View File

@ -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:

View File

@ -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),
))

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}),

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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...,
)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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))
}