feat: new API endpoint - GET /api/v2/versions/{versionCode}/servers/{serverKey}/village/{villageId}
This commit is contained in:
parent
b0f1a3d1bd
commit
eb2c19cc8a
|
@ -246,6 +246,24 @@ paths:
|
||||||
$ref: "#/components/responses/ListVillagesResponse"
|
$ref: "#/components/responses/ListVillagesResponse"
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/ErrorResponse"
|
$ref: "#/components/responses/ErrorResponse"
|
||||||
|
/v2/versions/{versionCode}/servers/{serverKey}/villages/{villageId}:
|
||||||
|
get:
|
||||||
|
operationId: getVillage
|
||||||
|
tags:
|
||||||
|
- versions
|
||||||
|
- servers
|
||||||
|
- villages
|
||||||
|
description: Get a village
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/VersionCodePathParam"
|
||||||
|
- $ref: "#/components/parameters/ServerKeyPathParam"
|
||||||
|
- $ref: "#/components/parameters/VillageIdPathParam"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
$ref: "#/components/responses/GetVillageResponse"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/ErrorResponse"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Error:
|
Error:
|
||||||
|
@ -1324,6 +1342,12 @@ components:
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/IntId"
|
$ref: "#/components/schemas/IntId"
|
||||||
|
VillageIdPathParam:
|
||||||
|
in: path
|
||||||
|
name: villageId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/IntId"
|
||||||
responses:
|
responses:
|
||||||
ListVersionsResponse:
|
ListVersionsResponse:
|
||||||
description: ""
|
description: ""
|
||||||
|
@ -1477,6 +1501,17 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Village"
|
$ref: "#/components/schemas/Village"
|
||||||
|
GetVillageResponse:
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: "#/components/schemas/Village"
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
description: Default error response.
|
description: Default error response.
|
||||||
content:
|
content:
|
||||||
|
|
|
@ -130,3 +130,32 @@ func (svc *VillageService) ListWithRelations(
|
||||||
) (domain.ListVillagesWithRelationsResult, error) {
|
) (domain.ListVillagesWithRelationsResult, error) {
|
||||||
return svc.repo.ListWithRelations(ctx, params)
|
return svc.repo.ListWithRelations(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *VillageService) GetWithRelations(
|
||||||
|
ctx context.Context,
|
||||||
|
id int,
|
||||||
|
serverKey string,
|
||||||
|
) (domain.VillageWithRelations, error) {
|
||||||
|
params := domain.NewListVillagesParams()
|
||||||
|
if err := params.SetIDs([]int{id}); err != nil {
|
||||||
|
return domain.VillageWithRelations{}, err
|
||||||
|
}
|
||||||
|
if err := params.SetServerKeys([]string{serverKey}); err != nil {
|
||||||
|
return domain.VillageWithRelations{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := svc.repo.ListWithRelations(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return domain.VillageWithRelations{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
villages := res.Villages()
|
||||||
|
if len(villages) == 0 {
|
||||||
|
return domain.VillageWithRelations{}, domain.VillageNotFoundError{
|
||||||
|
ID: id,
|
||||||
|
ServerKey: serverKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return villages[0], nil
|
||||||
|
}
|
||||||
|
|
|
@ -662,3 +662,29 @@ func (res ListVillagesWithRelationsResult) Self() VillageCursor {
|
||||||
func (res ListVillagesWithRelationsResult) Next() VillageCursor {
|
func (res ListVillagesWithRelationsResult) Next() VillageCursor {
|
||||||
return res.next
|
return res.next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VillageNotFoundError struct {
|
||||||
|
ID int
|
||||||
|
ServerKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ErrorWithParams = VillageNotFoundError{}
|
||||||
|
|
||||||
|
func (e VillageNotFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("village with id %d and server key %s not found", e.ID, e.ServerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e VillageNotFoundError) Type() ErrorType {
|
||||||
|
return ErrorTypeNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e VillageNotFoundError) Code() string {
|
||||||
|
return "village-not-found"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e VillageNotFoundError) Params() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"ID": e.ID,
|
||||||
|
"ServerKey": e.ServerKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ func NewAPIHTTPHandler(
|
||||||
h.serverMiddleware,
|
h.serverMiddleware,
|
||||||
h.tribeMiddleware,
|
h.tribeMiddleware,
|
||||||
h.playerMiddleware,
|
h.playerMiddleware,
|
||||||
|
h.villageMiddleware,
|
||||||
},
|
},
|
||||||
ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
|
ErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
h.errorRenderer.render(w, r, err)
|
h.errorRenderer.render(w, r, err)
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package port
|
package port
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
|
||||||
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *apiHTTPHandler) ListVillages(
|
func (h *apiHTTPHandler) ListVillages(
|
||||||
|
@ -57,6 +60,59 @@ func (h *apiHTTPHandler) ListVillages(
|
||||||
renderJSON(w, r, http.StatusOK, apimodel.NewListVillagesResponse(res))
|
renderJSON(w, r, http.StatusOK, apimodel.NewListVillagesResponse(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *apiHTTPHandler) GetVillage(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
_ apimodel.VersionCodePathParam,
|
||||||
|
_ apimodel.ServerKeyPathParam,
|
||||||
|
_ apimodel.VillageIdPathParam,
|
||||||
|
) {
|
||||||
|
village, _ := villageFromContext(r.Context())
|
||||||
|
renderJSON(w, r, http.StatusOK, apimodel.NewGetVillageResponse(village))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *apiHTTPHandler) villageMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
routeCtx := chi.RouteContext(ctx)
|
||||||
|
server, serverOK := serverFromContext(ctx)
|
||||||
|
villageIDIdx := slices.Index(routeCtx.URLParams.Keys, "villageId")
|
||||||
|
|
||||||
|
if !serverOK || villageIDIdx < 0 {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
villageID, err := strconv.Atoi(routeCtx.URLParams.Values[villageIDIdx])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
village, err := h.villageSvc.GetWithRelations(
|
||||||
|
ctx,
|
||||||
|
villageID,
|
||||||
|
server.Key(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
h.errorRenderer.withErrorPathFormatter(formatGetVillageErrorPath).render(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(villageToContext(ctx, village)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type villageCtxKey struct{}
|
||||||
|
|
||||||
|
func villageToContext(ctx context.Context, v domain.VillageWithRelations) context.Context {
|
||||||
|
return context.WithValue(ctx, villageCtxKey{}, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func villageFromContext(ctx context.Context) (domain.VillageWithRelations, bool) {
|
||||||
|
v, ok := ctx.Value(villageCtxKey{}).(domain.VillageWithRelations)
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
func formatListVillagesErrorPath(segments []domain.ErrorPathSegment) []string {
|
func formatListVillagesErrorPath(segments []domain.ErrorPathSegment) []string {
|
||||||
if segments[0].Model != "ListVillagesParams" {
|
if segments[0].Model != "ListVillagesParams" {
|
||||||
return nil
|
return nil
|
||||||
|
@ -77,3 +133,16 @@ func formatListVillagesErrorPath(segments []domain.ErrorPathSegment) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatGetVillageErrorPath(segments []domain.ErrorPathSegment) []string {
|
||||||
|
if segments[0].Model != "ListVillagesParams" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch segments[0].Field {
|
||||||
|
case "ids":
|
||||||
|
return []string{"$path", "villageId"}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -490,6 +490,196 @@ func TestListVillages(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endpointGetVillage = "/v2/versions/%s/servers/%s/villages/%d"
|
||||||
|
|
||||||
|
func TestGetVillage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
handler := newAPIHTTPHandler(t)
|
||||||
|
village := randVillage(t, handler)
|
||||||
|
|
||||||
|
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",
|
||||||
|
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// body
|
||||||
|
body := decodeJSON[apimodel.GetVillageResponse](t, resp.Body)
|
||||||
|
assert.Equal(t, village.Village, body.Data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: id < 1",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
req.URL.Path = fmt.Sprintf(
|
||||||
|
endpointGetVillage,
|
||||||
|
village.Server.Version.Code,
|
||||||
|
village.Server.Key,
|
||||||
|
domaintest.RandID()*-1,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
pathSegments := strings.Split(req.URL.Path, "/")
|
||||||
|
require.Len(t, pathSegments, 8)
|
||||||
|
id, err := strconv.Atoi(pathSegments[7])
|
||||||
|
require.NoError(t, err)
|
||||||
|
domainErr := domain.MinGreaterEqualError{
|
||||||
|
Min: 1,
|
||||||
|
Current: id,
|
||||||
|
}
|
||||||
|
assert.Equal(t, apimodel.ErrorResponse{
|
||||||
|
Errors: []apimodel.Error{
|
||||||
|
{
|
||||||
|
Code: domainErr.Code(),
|
||||||
|
Message: domainErr.Error(),
|
||||||
|
Params: map[string]any{
|
||||||
|
"min": float64(domainErr.Min),
|
||||||
|
"current": float64(domainErr.Current),
|
||||||
|
},
|
||||||
|
Path: []string{"$path", "villageId"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: version not found",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
req.URL.Path = fmt.Sprintf(endpointGetVillage, domaintest.RandVersionCode(), village.Server.Key, village.Id)
|
||||||
|
},
|
||||||
|
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, 8)
|
||||||
|
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(endpointGetVillage, village.Server.Version.Code, domaintest.RandServerKey(), village.Id)
|
||||||
|
},
|
||||||
|
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, 8)
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ERR: village not found",
|
||||||
|
reqModifier: func(t *testing.T, req *http.Request) {
|
||||||
|
t.Helper()
|
||||||
|
req.URL.Path = fmt.Sprintf(
|
||||||
|
endpointGetVillage,
|
||||||
|
village.Server.Version.Code,
|
||||||
|
village.Server.Key,
|
||||||
|
domaintest.RandID(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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, 8)
|
||||||
|
id, err := strconv.Atoi(pathSegments[7])
|
||||||
|
require.NoError(t, err)
|
||||||
|
domainErr := domain.VillageNotFoundError{
|
||||||
|
ID: id,
|
||||||
|
ServerKey: pathSegments[5],
|
||||||
|
}
|
||||||
|
assert.Equal(t, apimodel.ErrorResponse{
|
||||||
|
Errors: []apimodel.Error{
|
||||||
|
{
|
||||||
|
Code: domainErr.Code(),
|
||||||
|
Message: domainErr.Error(),
|
||||||
|
Params: map[string]any{
|
||||||
|
"id": float64(domainErr.ID),
|
||||||
|
"serverKey": domainErr.ServerKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf(endpointGetVillage, village.Server.Version.Code, village.Server.Key, village.Id),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if tt.reqModifier != nil {
|
||||||
|
tt.reqModifier(t, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := doCustomRequest(handler, req)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
tt.assertResp(t, req, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type villageWithServer struct {
|
type villageWithServer struct {
|
||||||
apimodel.Village
|
apimodel.Village
|
||||||
Server serverWithVersion
|
Server serverWithVersion
|
||||||
|
@ -520,3 +710,9 @@ func getAllVillages(tb testing.TB, h http.Handler) []villageWithServer {
|
||||||
|
|
||||||
return villages
|
return villages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func randVillage(tb testing.TB, h http.Handler) villageWithServer {
|
||||||
|
tb.Helper()
|
||||||
|
villages := getAllVillages(tb, h)
|
||||||
|
return villages[gofakeit.IntRange(0, len(villages)-1)]
|
||||||
|
}
|
||||||
|
|
|
@ -38,3 +38,9 @@ func NewListVillagesResponse(res domain.ListVillagesWithRelationsResult) ListVil
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewGetVillageResponse(v domain.VillageWithRelations) GetVillageResponse {
|
||||||
|
return GetVillageResponse{
|
||||||
|
Data: NewVillage(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue