feat: api - add a new endpoint - GET /api/v2/versions/{versionCode}

This commit is contained in:
Dawid Wysokiński 2024-02-01 07:39:38 +01:00
parent 28877f1b9b
commit a66bbb0b2f
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
7 changed files with 149 additions and 19 deletions

View File

@ -36,6 +36,19 @@ paths:
$ref: "#/components/responses/ListVersionsResponse" $ref: "#/components/responses/ListVersionsResponse"
default: default:
$ref: "#/components/responses/ErrorResponse" $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: components:
schemas: schemas:
Error: Error:
@ -81,29 +94,23 @@ components:
timezone: timezone:
type: string type: string
example: Europe/Warsaw example: Europe/Warsaw
PaginationResponse: Cursor:
type: object type: object
x-go-type-skip-optional-pointer: true
properties: properties:
self: self:
description: Cursor pointing to the current page. description: Cursor pointing to the current page.
type: string type: string
x-go-type-skip-optional-pointer: true 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: next:
description: Cursor pointing to the next page. description: Cursor pointing to the next page.
type: string type: string
x-go-type-skip-optional-pointer: true x-go-type-skip-optional-pointer: true
last: PaginationResponse:
description: Cursor pointing to the last page. type: object
type: string properties:
x-go-type-skip-optional-pointer: true cursor:
$ref: "#/components/schemas/Cursor"
parameters: parameters:
CursorQueryParam: CursorQueryParam:
in: query in: query
@ -117,6 +124,12 @@ components:
schema: schema:
type: integer type: integer
required: false required: false
VersionCodePathParam:
in: path
name: versionCode
required: true
schema:
type: string
responses: responses:
ListVersionsResponse: ListVersionsResponse:
description: "" description: ""
@ -127,12 +140,23 @@ components:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/PaginationResponse"
- type: object - type: object
required: required:
- items - data
properties: properties:
items: data:
type: array type: array
items: items:
$ref: "#/components/schemas/Version" $ref: "#/components/schemas/Version"
GetVersionResponse:
description: ""
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
$ref: "#/components/schemas/Version"
ErrorResponse: ErrorResponse:
description: Default error response. description: Default error response.
content: content:

View File

@ -45,6 +45,10 @@ type listVersionsParamsApplier struct {
} }
func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { 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() { for _, s := range a.params.Sort() {
codeCursor := a.params.Cursor().Code() codeCursor := a.params.Cursor().Code()

View File

@ -139,6 +139,36 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, err) 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", name: "OK: limit=5",
params: func(t *testing.T) domain.ListVersionsParams { params: func(t *testing.T) domain.ListVersionsParams {

View File

@ -24,3 +24,24 @@ func (svc *VersionService) List(
) (domain.ListVersionsResult, error) { ) (domain.ListVersionsResult, error) {
return svc.repo.List(ctx, params) 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
}

View File

@ -2,6 +2,7 @@ package domain
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"net/url" "net/url"
) )
@ -172,6 +173,7 @@ func (vc VersionCursor) Encode() string {
} }
type ListVersionsParams struct { type ListVersionsParams struct {
codes []string
sort []VersionSort sort []VersionSort
cursor VersionCursor cursor VersionCursor
limit int 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 ( const (
versionSortMinLength = 1 versionSortMinLength = 1
versionSortMaxLength = 1 versionSortMaxLength = 1
@ -308,3 +319,25 @@ func (res ListVersionsResult) Self() VersionCursor {
func (res ListVersionsResult) Next() VersionCursor { func (res ListVersionsResult) Next() VersionCursor {
return res.next 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,
}
}

View File

@ -33,6 +33,16 @@ func (h *apiHTTPHandler) ListVersions(w http.ResponseWriter, r *http.Request, pa
renderJSON(w, r, http.StatusOK, apimodel.NewListVersionsResponse(res)) 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 { func formatListVersionsParamsErrorPath(elems []errorPathElement) []string {
if elems[0].model != "ListVersionsParams" { if elems[0].model != "ListVersionsParams" {
return nil return nil

View File

@ -17,14 +17,22 @@ func NewListVersionsResponse(res domain.ListVersionsResult) ListVersionsResponse
versions := res.Versions() versions := res.Versions()
resp := ListVersionsResponse{ resp := ListVersionsResponse{
Items: make([]Version, 0, len(versions)), Data: make([]Version, 0, len(versions)),
Self: res.Self().Encode(), Cursor: Cursor{
Next: res.Next().Encode(), Next: res.Next().Encode(),
Self: res.Self().Encode(),
},
} }
for _, v := range versions { for _, v := range versions {
resp.Items = append(resp.Items, NewVersion(v)) resp.Data = append(resp.Data, NewVersion(v))
} }
return resp return resp
} }
func NewGetVersionResponse(v domain.Version) GetVersionResponse {
return GetVersionResponse{
Data: NewVersion(v),
}
}