From a66bbb0b2f9d65b35e5d1438da6f7def1b3049dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Wysoki=C5=84ski?= Date: Thu, 1 Feb 2024 07:39:38 +0100 Subject: [PATCH] feat: api - add a new endpoint - GET /api/v2/versions/{versionCode} --- api/openapi3.yml | 54 +++++++++++++------ internal/adapter/repository_bun_version.go | 4 ++ internal/adapter/repository_version_test.go | 30 +++++++++++ internal/app/service_version.go | 21 ++++++++ internal/domain/version.go | 33 ++++++++++++ internal/port/handler_http_api_version.go | 10 ++++ .../apimodel/{apimodel.go => version.go} | 16 ++++-- 7 files changed, 149 insertions(+), 19 deletions(-) rename internal/port/internal/apimodel/{apimodel.go => version.go} (63%) diff --git a/api/openapi3.yml b/api/openapi3.yml index 2832c73..49e3ba4 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -36,6 +36,19 @@ paths: $ref: "#/components/responses/ListVersionsResponse" default: $ref: "#/components/responses/ErrorResponse" + /v2/versions/{versionCode}: + get: + operationId: getVersion + tags: + - versions + description: Get a version + parameters: + - $ref: "#/components/parameters/VersionCodePathParam" + responses: + 200: + $ref: "#/components/responses/GetVersionResponse" + default: + $ref: "#/components/responses/ErrorResponse" components: schemas: Error: @@ -81,29 +94,23 @@ components: timezone: type: string example: Europe/Warsaw - PaginationResponse: + Cursor: type: object + x-go-type-skip-optional-pointer: true properties: self: description: Cursor pointing to the current page. type: string x-go-type-skip-optional-pointer: true - first: - description: Cursor pointing to the first page. - type: string - x-go-type-skip-optional-pointer: true - prev: - description: Cursor pointing to the previous page. - type: string - x-go-type-skip-optional-pointer: true next: description: Cursor pointing to the next page. type: string x-go-type-skip-optional-pointer: true - last: - description: Cursor pointing to the last page. - type: string - x-go-type-skip-optional-pointer: true + PaginationResponse: + type: object + properties: + cursor: + $ref: "#/components/schemas/Cursor" parameters: CursorQueryParam: in: query @@ -117,6 +124,12 @@ components: schema: type: integer required: false + VersionCodePathParam: + in: path + name: versionCode + required: true + schema: + type: string responses: ListVersionsResponse: description: "" @@ -127,12 +140,23 @@ components: - $ref: "#/components/schemas/PaginationResponse" - type: object required: - - items + - data properties: - items: + data: type: array items: $ref: "#/components/schemas/Version" + GetVersionResponse: + description: "" + content: + application/json: + schema: + type: object + required: + - data + properties: + data: + $ref: "#/components/schemas/Version" ErrorResponse: description: Default error response. content: diff --git a/internal/adapter/repository_bun_version.go b/internal/adapter/repository_bun_version.go index bccc230..0e4e54b 100644 --- a/internal/adapter/repository_bun_version.go +++ b/internal/adapter/repository_bun_version.go @@ -45,6 +45,10 @@ type listVersionsParamsApplier struct { } func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { + if codes := a.params.Codes(); len(codes) > 0 { + q = q.Where("version.code IN (?)", bun.In(codes)) + } + for _, s := range a.params.Sort() { codeCursor := a.params.Cursor().Code() diff --git a/internal/adapter/repository_version_test.go b/internal/adapter/repository_version_test.go index 01c4d20..1144646 100644 --- a/internal/adapter/repository_version_test.go +++ b/internal/adapter/repository_version_test.go @@ -139,6 +139,36 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie require.NoError(t, err) }, }, + { + name: "OK: codes limit=1", + params: func(t *testing.T) domain.ListVersionsParams { + t.Helper() + + params := domain.NewListVersionsParams() + require.NoError(t, params.SetLimit(1)) + + res, err := repos.version.List(ctx, params) + require.NoError(t, err) + require.NotEmpty(t, res) + + require.NoError(t, params.SetCodes([]string{res.Versions()[0].Code()})) + + return params + }, + assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) { + t.Helper() + versions := res.Versions() + codes := params.Codes() + assert.Len(t, versions, len(codes)) + for _, v := range versions { + assert.Contains(t, codes, v.Code()) + } + }, + assertError: func(t *testing.T, err error) { + t.Helper() + require.NoError(t, err) + }, + }, { name: "OK: limit=5", params: func(t *testing.T) domain.ListVersionsParams { diff --git a/internal/app/service_version.go b/internal/app/service_version.go index 96e224a..cdde381 100644 --- a/internal/app/service_version.go +++ b/internal/app/service_version.go @@ -24,3 +24,24 @@ func (svc *VersionService) List( ) (domain.ListVersionsResult, error) { return svc.repo.List(ctx, params) } + +func (svc *VersionService) Get(ctx context.Context, code string) (domain.Version, error) { + params := domain.NewListVersionsParams() + if err := params.SetCodes([]string{code}); err != nil { + return domain.Version{}, err + } + + res, err := svc.repo.List(ctx, params) + if err != nil { + return domain.Version{}, err + } + + versions := res.Versions() + if len(versions) == 0 { + return domain.Version{}, domain.VersionNotFoundError{ + VersionCode: code, + } + } + + return versions[0], nil +} diff --git a/internal/domain/version.go b/internal/domain/version.go index 3d85c44..5216c91 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -2,6 +2,7 @@ package domain import ( "encoding/base64" + "fmt" "net/url" ) @@ -172,6 +173,7 @@ func (vc VersionCursor) Encode() string { } type ListVersionsParams struct { + codes []string sort []VersionSort cursor VersionCursor limit int @@ -189,6 +191,15 @@ func NewListVersionsParams() ListVersionsParams { } } +func (params *ListVersionsParams) Codes() []string { + return params.codes +} + +func (params *ListVersionsParams) SetCodes(codes []string) error { + params.codes = codes + return nil +} + const ( versionSortMinLength = 1 versionSortMaxLength = 1 @@ -308,3 +319,25 @@ func (res ListVersionsResult) Self() VersionCursor { func (res ListVersionsResult) Next() VersionCursor { return res.next } + +type VersionNotFoundError struct { + VersionCode string +} + +func (e VersionNotFoundError) Error() string { + return fmt.Sprintf("version with code %s not found", e.VersionCode) +} + +func (e VersionNotFoundError) Code() ErrorCode { + return ErrorCodeNotFound +} + +func (e VersionNotFoundError) Slug() string { + return "version-not-found" +} + +func (e VersionNotFoundError) Params() map[string]any { + return map[string]any{ + "Code": e.VersionCode, + } +} diff --git a/internal/port/handler_http_api_version.go b/internal/port/handler_http_api_version.go index 7a29d1e..37dedbe 100644 --- a/internal/port/handler_http_api_version.go +++ b/internal/port/handler_http_api_version.go @@ -33,6 +33,16 @@ func (h *apiHTTPHandler) ListVersions(w http.ResponseWriter, r *http.Request, pa renderJSON(w, r, http.StatusOK, apimodel.NewListVersionsResponse(res)) } +func (h *apiHTTPHandler) GetVersion(w http.ResponseWriter, r *http.Request, versionCode apimodel.VersionCodePathParam) { + version, err := h.versionSvc.Get(r.Context(), versionCode) + if err != nil { + apiErrorRenderer{errors: []error{err}}.render(w, r) + return + } + + renderJSON(w, r, http.StatusOK, apimodel.NewGetVersionResponse(version)) +} + func formatListVersionsParamsErrorPath(elems []errorPathElement) []string { if elems[0].model != "ListVersionsParams" { return nil diff --git a/internal/port/internal/apimodel/apimodel.go b/internal/port/internal/apimodel/version.go similarity index 63% rename from internal/port/internal/apimodel/apimodel.go rename to internal/port/internal/apimodel/version.go index b9f4161..7460bb1 100644 --- a/internal/port/internal/apimodel/apimodel.go +++ b/internal/port/internal/apimodel/version.go @@ -17,14 +17,22 @@ func NewListVersionsResponse(res domain.ListVersionsResult) ListVersionsResponse versions := res.Versions() resp := ListVersionsResponse{ - Items: make([]Version, 0, len(versions)), - Self: res.Self().Encode(), - Next: res.Next().Encode(), + Data: make([]Version, 0, len(versions)), + Cursor: Cursor{ + Next: res.Next().Encode(), + Self: res.Self().Encode(), + }, } for _, v := range versions { - resp.Items = append(resp.Items, NewVersion(v)) + resp.Data = append(resp.Data, NewVersion(v)) } return resp } + +func NewGetVersionResponse(v domain.Version) GetVersionResponse { + return GetVersionResponse{ + Data: NewVersion(v), + } +}