feat: add a new endpoint - GET /api/v2/versions (#53)

Reviewed-on: twhelp/corev3#53
This commit is contained in:
Dawid Wysokiński 2024-01-27 08:37:12 +00:00
parent aff2b077c0
commit 1409e0a1be
28 changed files with 747 additions and 114 deletions

View File

@ -20,7 +20,7 @@ servers:
- http
- https
hostname:
default: localhost:9913
default: localhost:9234
paths:
/v2/versions:
get:
@ -77,29 +77,24 @@ components:
type: object
properties:
self:
description: Pagination link pointing to the current page.
description: Cursor pointing to the current page.
type: string
format: uri
x-go-type-skip-optional-pointer: true
first:
description: Pagination link pointing to the first page.
description: Cursor pointing to the first page.
type: string
format: uri
x-go-type-skip-optional-pointer: true
prev:
description: Pagination link pointing to the previous page.
description: Cursor pointing to the previous page.
type: string
format: uri
x-go-type-skip-optional-pointer: true
next:
description: Pagination link pointing to the next page.
description: Cursor pointing to the next page.
type: string
format: uri
x-go-type-skip-optional-pointer: true
last:
description: Pagination link pointing to the last page.
description: Cursor pointing to the last page.
type: string
format: uri
x-go-type-skip-optional-pointer: true
parameters:
CursorQueryParam:

View File

@ -10,6 +10,8 @@ import (
"strings"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/chislog"
"gitea.dwysokinski.me/twhelp/corev3/internal/health"
"gitea.dwysokinski.me/twhelp/corev3/internal/port"
@ -87,6 +89,7 @@ var cmdServe = &cli.Command{
Action: func(c *cli.Context) error {
logger := loggerFromCtx(c.Context)
// deps
bunDB, err := newBunDBFromFlags(c)
if err != nil {
return err
@ -100,6 +103,13 @@ var cmdServe = &cli.Command{
}
}()
// adapters
versionRepo := adapter.NewVersionBunRepository(bunDB)
// services
versionSvc := app.NewVersionService(versionRepo)
// health
h := health.New()
server, err := newHTTPServer(
@ -120,7 +130,10 @@ var cmdServe = &cli.Command{
r.Mount(metaBasePath, port.NewMetaHTTPHandler(h))
r.Mount(apiBasePath, port.NewAPIHTTPHandler(port.WithOpenAPIConfig(oapiCfg)))
r.Mount(apiBasePath, port.NewAPIHTTPHandler(
versionSvc,
port.WithOpenAPIConfig(oapiCfg),
))
return nil
},

8
go.mod
View File

@ -8,11 +8,11 @@ require (
github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/cenkalti/backoff/v4 v4.2.1
github.com/elliotchance/phpserialize v1.3.3
github.com/getkin/kin-openapi v0.122.0
github.com/getkin/kin-openapi v0.123.0
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/render v1.0.3
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.5.0
github.com/google/uuid v1.6.0
github.com/oapi-codegen/runtime v1.1.1
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.8.4
@ -42,8 +42,8 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect

24
go.sum
View File

@ -30,7 +30,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
@ -52,17 +51,16 @@ github.com/elliotchance/phpserialize v1.3.3/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lc
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10=
github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw=
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@ -83,8 +81,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -157,8 +155,8 @@ github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc
github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -172,12 +170,10 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=

View File

@ -12,3 +12,14 @@ func appendODSetClauses(q *bun.InsertQuery) *bun.InsertQuery {
Set("rank_total = EXCLUDED.rank_total").
Set("score_total = EXCLUDED.score_total")
}
func separateListResultAndNext[T any](res []T, limit int) ([]T, T) {
var next T
if len(res) > limit {
next = res[limit]
res = res[:limit]
}
return res, next
}

View File

@ -19,29 +19,25 @@ func NewVersionBunRepository(db bun.IDB) *VersionBunRepository {
return &VersionBunRepository{db: db}
}
func (repo *VersionBunRepository) List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error) {
func (repo *VersionBunRepository) List(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.ListVersionsResult, error) {
var versions bunmodel.Versions
if err := repo.db.NewSelect().
Model(&versions).
Apply(listVersionsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select versions from the database: %w", err)
return domain.ListVersionsResult{}, fmt.Errorf("couldn't select versions from the database: %w", err)
}
return versions.ToDomain()
}
func (repo *VersionBunRepository) ListCount(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.Versions, int, error) {
versions, err := repo.List(ctx, params)
converted, err := versions.ToDomain()
if err != nil {
return nil, 0, err
return domain.ListVersionsResult{}, err
}
return versions, len(versions), nil
return domain.NewListVersionsResult(separateListResultAndNext(converted, params.Limit()))
}
type listVersionsParamsApplier struct {
@ -50,15 +46,25 @@ type listVersionsParamsApplier struct {
func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
for _, s := range a.params.Sort() {
codeCursor := a.params.Cursor().Code()
switch s {
case domain.VersionSortCodeASC:
if codeCursor.Valid {
q = q.Where("version.code >= ?", codeCursor.Value)
}
q = q.Order("version.code ASC")
case domain.VersionSortCodeDESC:
if codeCursor.Valid {
q = q.Where("version.code <= ?", codeCursor.Value)
}
q = q.Order("version.code DESC")
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q
return q.Limit(a.params.Limit() + 1)
}

View File

@ -57,10 +57,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("OK", func(t *testing.T) {
t.Parallel()
versions, err := repos.version.List(ctx, domain.NewListVersionsParams())
listVersionsRes, err := repos.version.List(ctx, domain.NewListVersionsParams())
require.NoError(t, err)
require.NotEmpty(t, versions)
version := versions[0]
require.NotEmpty(t, listVersionsRes)
version := listVersionsRes.Versions()[0]
serversToCreate := domain.BaseServers{
domaintest.NewBaseServer(t),

View File

@ -12,11 +12,7 @@ import (
)
type versionRepository interface {
List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error)
ListCount(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.Versions, int, error)
List(ctx context.Context, params domain.ListVersionsParams) (domain.ListVersionsResult, error)
}
type serverRepository interface {

View File

@ -7,6 +7,7 @@ import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -16,17 +17,16 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
ctx := context.Background()
t.Run("List & ListCount", func(t *testing.T) {
t.Run("List", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.ListVersionsParams
assertVersions func(t *testing.T, versions domain.Versions)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, total int)
name string
params func(t *testing.T) domain.ListVersionsParams
assertResult func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
@ -34,21 +34,18 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper()
return domain.NewListVersionsParams()
},
assertVersions: func(t *testing.T, versions domain.Versions) {
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.NotEmpty(t, len(versions))
assert.True(t, slices.IsSortedFunc(versions, func(a, b domain.Version) int {
assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code())
}))
assert.True(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[code DESC]",
@ -58,10 +55,10 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, params.SetSort([]domain.VersionSort{domain.VersionSortCodeDESC}))
return params
},
assertVersions: func(t *testing.T, versions domain.Versions) {
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.NotEmpty(t, len(versions))
assert.True(t, slices.IsSortedFunc(versions, func(a, b domain.Version) int {
assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code()) * -1
}))
},
@ -69,9 +66,95 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, total int) {
},
{
name: "OK: cursor sort=[code ASC]",
params: func(t *testing.T) domain.ListVersionsParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListVersionsParams()
require.NoError(t, params.SetSort([]domain.VersionSort{domain.VersionSortCodeASC}))
res, err := repos.version.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Versions()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVersionCursor(t, func(cfg *domaintest.VersionCursorConfig) {
cfg.Code = domain.NullString{
Value: res.Versions()[1].Code(),
Valid: true,
}
})))
return params
},
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code())
}))
for _, v := range res.Versions() {
assert.GreaterOrEqual(t, v.Code(), params.Cursor().Code().Value, v.Code())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: cursor sort=[code DESC]",
params: func(t *testing.T) domain.ListVersionsParams {
t.Helper()
params := domain.NewListVersionsParams()
require.NoError(t, params.SetSort([]domain.VersionSort{domain.VersionSortCodeDESC}))
res, err := repos.version.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Versions()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVersionCursor(t, func(cfg *domaintest.VersionCursorConfig) {
cfg.Code = domain.NullString{
Value: res.Versions()[1].Code(),
Valid: true,
}
})))
return params
},
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code()) * -1
}))
for _, v := range res.Versions() {
assert.LessOrEqual(t, v.Code(), params.Cursor().Code().Value, 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 {
t.Helper()
params := domain.NewListVersionsParams()
require.NoError(t, params.SetLimit(5))
return params
},
assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper()
assert.Len(t, res.Versions(), params.Limit())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
}
@ -86,12 +169,7 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
res, err := repos.version.List(ctx, params)
tt.assertError(t, err)
tt.assertVersions(t, res)
res, count, err := repos.version.ListCount(ctx, params)
tt.assertError(t, err)
tt.assertVersions(t, res)
tt.assertTotal(t, count)
tt.assertResult(t, params, res)
})
}
})

View File

@ -29,10 +29,11 @@ func NewDataSyncService(
}
func (svc *DataSyncService) Sync(ctx context.Context) error {
versions, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
listVersionsRes, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
if err != nil {
return err
}
versions := listVersionsRes.Versions()
payloads := make([]domain.SyncServersCmdPayload, 0, len(versions))
@ -49,10 +50,11 @@ func (svc *DataSyncService) Sync(ctx context.Context) error {
}
func (svc *DataSyncService) SyncEnnoblements(ctx context.Context) error {
versions, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
listVersionsRes, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
if err != nil {
return err
}
versions := listVersionsRes.Versions()
for _, v := range versions {
if err = svc.syncEnnoblementsForVersion(ctx, v); err != nil {

View File

@ -31,10 +31,11 @@ func NewSnapshotService(
}
func (svc *SnapshotService) Create(ctx context.Context) error {
versions, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
listVersionsRes, err := svc.versionSvc.List(ctx, domain.NewListVersionsParams())
if err != nil {
return err
}
versions := listVersionsRes.Versions()
for _, v := range versions {
loc, loopErr := time.LoadLocation(v.Timezone())

View File

@ -7,7 +7,7 @@ import (
)
type VersionRepository interface {
List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error)
List(ctx context.Context, params domain.ListVersionsParams) (domain.ListVersionsResult, error)
}
type VersionService struct {
@ -18,6 +18,9 @@ func NewVersionService(repo VersionRepository) *VersionService {
return &VersionService{repo: repo}
}
func (svc *VersionService) List(ctx context.Context, params domain.ListVersionsParams) (domain.Versions, error) {
func (svc *VersionService) List(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.ListVersionsResult, error) {
return svc.repo.List(ctx, params)
}

View File

@ -6,6 +6,30 @@ import (
"github.com/stretchr/testify/require"
)
type VersionCursorConfig struct {
Code domain.NullString
}
func NewVersionCursor(tb TestingTB, opts ...func(cfg *VersionCursorConfig)) domain.VersionCursor {
tb.Helper()
cfg := &VersionCursorConfig{
Code: domain.NullString{
Value: RandVersionCode(),
Valid: true,
},
}
for _, opt := range opts {
opt(cfg)
}
vc, err := domain.NewVersionCursor(cfg.Code)
require.NoError(tb, err)
return vc
}
type VersionConfig struct {
Code string
}
@ -21,7 +45,7 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
opt(cfg)
}
s, err := domain.UnmarshalVersionFromDatabase(
v, err := domain.UnmarshalVersionFromDatabase(
cfg.Code,
gofakeit.LetterN(10),
gofakeit.DomainName(),
@ -29,7 +53,7 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
)
require.NoError(tb, err)
return s
return v
}
func RandVersionCode() string {

View File

@ -165,8 +165,6 @@ const (
EnnoblementSortServerKeyDESC
)
const EnnoblementListMaxLimit = 200
type ListEnnoblementsParams struct {
serverKeys []string
sort []EnnoblementSort
@ -174,7 +172,10 @@ type ListEnnoblementsParams struct {
offset int
}
const listEnnoblementsParamsModelName = "ListEnnoblementsParams"
const (
EnnoblementListMaxLimit = 200
listEnnoblementsParamsModelName = "ListEnnoblementsParams"
)
func NewListEnnoblementsParams() ListEnnoblementsParams {
return ListEnnoblementsParams{

View File

@ -373,8 +373,6 @@ const (
PlayerSortServerKeyDESC
)
const PlayerListMaxLimit = 200
type ListPlayersParams struct {
ids []int
idGT NullInt
@ -385,7 +383,10 @@ type ListPlayersParams struct {
offset int
}
const listPlayersParamsModelName = "ListPlayersParams"
const (
PlayerListMaxLimit = 200
listPlayersParamsModelName = "ListPlayersParams"
)
func NewListPlayersParams() ListPlayersParams {
return ListPlayersParams{

View File

@ -464,8 +464,6 @@ const (
ServerSortOpenDESC
)
const ServerListMaxLimit = 500
type ListServersParams struct {
keys []string
keyGT NullString
@ -479,7 +477,10 @@ type ListServersParams struct {
offset int
}
const listServersParamsModelName = "ListServersParams"
const (
ServerListMaxLimit = 500
listServersParamsModelName = "ListServersParams"
)
func NewListServersParams() ListServersParams {
return ListServersParams{

View File

@ -368,8 +368,6 @@ const (
TribeSortServerKeyDESC
)
const TribeListMaxLimit = 200
type ListTribesParams struct {
ids []int
idGT NullInt
@ -380,7 +378,10 @@ type ListTribesParams struct {
offset int
}
const listTribesParamsModelName = "ListTribesParams"
const (
TribeListMaxLimit = 200
listTribesParamsModelName = "ListTribesParams"
)
func NewListTribesParams() ListTribesParams {
return ListTribesParams{

View File

@ -213,8 +213,6 @@ const (
TribeChangeSortServerKeyDESC
)
const TribeChangeListMaxLimit = 200
type ListTribeChangesParams struct {
serverKeys []string
sort []TribeChangeSort
@ -222,7 +220,10 @@ type ListTribeChangesParams struct {
offset int
}
const listTribeChangesParamsModelName = "ListTribeChangesParams"
const (
TribeChangeListMaxLimit = 200
listTribeChangesParamsModelName = "ListTribeChangesParams"
)
func NewListTribeChangesParams() ListTribeChangesParams {
return ListTribeChangesParams{

View File

@ -224,8 +224,6 @@ const (
TribeSnapshotSortServerKeyDESC
)
const TribeSnapshotListMaxLimit = 200
type ListTribeSnapshotsParams struct {
serverKeys []string
sort []TribeSnapshotSort
@ -233,7 +231,10 @@ type ListTribeSnapshotsParams struct {
offset int
}
const listTribeSnapshotsParamsModelName = "ListTribeSnapshotsParams"
const (
TribeSnapshotListMaxLimit = 200
listTribeSnapshotsParamsModelName = "ListTribeSnapshotsParams"
)
func NewListTribeSnapshotsParams() ListTribeSnapshotsParams {
return ListTribeSnapshotsParams{

View File

@ -1,6 +1,7 @@
package domain
import (
"encoding/base64"
"net/url"
)
@ -91,6 +92,10 @@ func (v Version) URL() *url.URL {
}
}
func (v Version) IsZero() bool {
return v == Version{}
}
type Versions []Version
type VersionSort uint8
@ -100,14 +105,77 @@ const (
VersionSortCodeDESC
)
type ListVersionsParams struct {
sort []VersionSort
type VersionCursor struct {
code NullString
}
const listVersionsParamsModelName = "ListVersionsParams"
func NewVersionCursor(code NullString) (VersionCursor, error) {
if !code.Valid {
return VersionCursor{}, nil
}
if err := validateVersionCode(code.Value); err != nil {
return VersionCursor{}, err
}
return VersionCursor{
code: code,
}, nil
}
const (
encodedVersionCursorMinLength = 1
encodedVersionCursorMaxLength = 5000
)
func decodeVersionCursor(encoded string) (VersionCursor, error) {
if err := validateStringLen(encoded, encodedVersionCursorMinLength, encodedVersionCursorMaxLength); err != nil {
return VersionCursor{}, err
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return VersionCursor{}, err
}
return NewVersionCursor(NullString{
Value: string(decoded),
Valid: true,
})
}
func (vc VersionCursor) Code() NullString {
return vc.code
}
func (vc VersionCursor) IsZero() bool {
return !vc.code.Valid
}
func (vc VersionCursor) Encode() string {
if vc.IsZero() {
return ""
}
return base64.StdEncoding.EncodeToString([]byte(vc.code.Value))
}
type ListVersionsParams struct {
sort []VersionSort
cursor VersionCursor
limit int
}
const (
VersionListMaxLimit = 200
listVersionsParamsModelName = "ListVersionsParams"
)
func NewListVersionsParams() ListVersionsParams {
return ListVersionsParams{sort: []VersionSort{VersionSortCodeASC}}
return ListVersionsParams{
sort: []VersionSort{VersionSortCodeASC},
limit: VersionListMaxLimit,
}
}
const (
@ -132,3 +200,100 @@ func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
return nil
}
func (params *ListVersionsParams) Cursor() VersionCursor {
return params.cursor
}
func (params *ListVersionsParams) SetCursor(vc VersionCursor) error {
params.cursor = vc
return nil
}
func (params *ListVersionsParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeVersionCursor(encoded)
if err != nil {
return ValidationError{
Model: listVersionsParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListVersionsParams) Limit() int {
return params.limit
}
func (params *ListVersionsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, VersionListMaxLimit); err != nil {
return ValidationError{
Model: listVersionsParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
type ListVersionsResult struct {
versions Versions
self VersionCursor
next VersionCursor
}
const listVersionsResultModelName = "ListVersionsResult"
func NewListVersionsResult(versions Versions, next Version) (ListVersionsResult, error) {
var err error
res := ListVersionsResult{
versions: versions,
}
if len(versions) > 0 {
res.self, err = NewVersionCursor(NullString{
Value: versions[0].Code(),
Valid: true,
})
if err != nil {
return ListVersionsResult{}, ValidationError{
Model: listVersionsResultModelName,
Field: "self",
Err: err,
}
}
}
res.next, err = NewVersionCursor(NullString{
Value: next.Code(),
Valid: !next.IsZero(),
})
if err != nil {
return ListVersionsResult{}, ValidationError{
Model: listVersionsResultModelName,
Field: "next",
Err: err,
}
}
return res, nil
}
func (res ListVersionsResult) Versions() Versions {
return res.versions
}
func (res ListVersionsResult) Self() VersionCursor {
return res.self
}
func (res ListVersionsResult) Next() VersionCursor {
return res.next
}

View File

@ -1,13 +1,83 @@
package domain_test
import (
"encoding/base64"
"fmt"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewVersionCursor(t *testing.T) {
t.Parallel()
validVersionCursor := domaintest.NewVersionCursor(t)
type args struct {
code domain.NullString
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
code: validVersionCursor.Code(),
},
expectedErr: nil,
},
{
name: "OK: nil code",
args: args{
code: domain.NullString{},
},
expectedErr: nil,
},
}
for _, versionCodeTest := range newVersionCodeValidationTests() {
tests = append(tests, test{
name: versionCodeTest.name,
args: args{
code: domain.NullString{
Value: versionCodeTest.code,
Valid: true,
},
},
expectedErr: versionCodeTest.expectedErr,
})
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
vc, err := domain.NewVersionCursor(tt.args.code)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.code, vc.Code())
if tt.args.code.Valid {
assert.NotEmpty(t, vc.Encode())
} else {
assert.Empty(t, vc.Encode())
}
})
}
}
func TestListVersionsParams_SetSort(t *testing.T) {
t.Parallel()
@ -80,6 +150,200 @@ func TestListVersionsParams_SetSort(t *testing.T) {
}
}
func TestListVersionsParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewVersionCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.VersionCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 5000,
Current: 0,
},
},
},
{
name: "ERR: len(cursor) > 5000",
args: args{
cursor: gofakeit.LetterN(5001),
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 5000,
Current: 5001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "cursor",
Err: base64.CorruptInputError(4),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVersionsParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
fmt.Println(tt.expectedErr.Error())
return
}
assert.True(t, params.Cursor().Code().Valid)
assert.Equal(t, tt.expectedCursor.Code(), params.Cursor().Code())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListVersionsParams_SetLimit(t *testing.T) {
t.Parallel()
type args struct {
limit int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
limit: domain.VersionListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.VersionListMaxLimit),
args: args{
limit: domain.VersionListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListVersionsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.VersionListMaxLimit,
Current: domain.VersionListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVersionsParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestNewListVersionsResult(t *testing.T) {
t.Parallel()
versions := domain.Versions{
domaintest.NewVersion(t),
domaintest.NewVersion(t),
domaintest.NewVersion(t),
}
next := domaintest.NewVersion(t)
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVersionsResult(versions, next)
require.NoError(t, err)
assert.Equal(t, versions, res.Versions())
assert.True(t, res.Self().Code().Valid)
assert.Equal(t, versions[0].Code(), res.Self().Code().Value)
assert.True(t, res.Next().Code().Valid)
assert.Equal(t, next.Code(), res.Next().Code().Value)
})
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVersionsResult(versions, domain.Version{})
require.NoError(t, err)
assert.Equal(t, versions, res.Versions())
assert.True(t, res.Self().Code().Valid)
assert.Equal(t, versions[0].Code(), res.Self().Code().Value)
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 versions", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListVersionsResult(nil, domain.Version{})
require.NoError(t, err)
assert.Zero(t, res.Versions())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}
type versionCodeValidationTest struct {
name string
code string

View File

@ -224,8 +224,6 @@ const (
VillageSortServerKeyDESC
)
const VillageListMaxLimit = 500
type ListVillagesParams struct {
ids []int
idGT NullInt
@ -235,7 +233,10 @@ type ListVillagesParams struct {
offset int
}
const listVillagesParamsModelName = "ListVillagesParams"
const (
VillageListMaxLimit = 500
listVillagesParamsModelName = "ListVillagesParams"
)
func NewListVillagesParams() ListVillagesParams {
return ListVillagesParams{

View File

@ -4,6 +4,7 @@ import (
"net/http"
"sync"
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/swgui"
"github.com/getkin/kin-openapi/openapi3"
@ -12,7 +13,7 @@ import (
)
type apiHTTPHandler struct {
apimodel.Unimplemented
versionSvc *app.VersionService
getOpenAPISchema func() (*openapi3.T, error)
}
@ -22,10 +23,11 @@ func WithOpenAPIConfig(oapiCfg OpenAPIConfig) APIHTTPHandlerOption {
}
}
func NewAPIHTTPHandler(opts ...APIHTTPHandlerOption) http.Handler {
func NewAPIHTTPHandler(versionSvc *app.VersionService, opts ...APIHTTPHandlerOption) http.Handler {
cfg := newAPIHTTPHandlerConfig(opts...)
h := &apiHTTPHandler{
versionSvc: versionSvc,
getOpenAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
return getOpenAPISchema(cfg.openAPI)
}),

View File

@ -35,8 +35,10 @@ func getOpenAPISchema(cfg OpenAPIConfig) (*openapi3.T, error) {
schema.Servers = make(openapi3.Servers, 0, len(cfg.Servers))
for _, s := range cfg.Servers {
u := *s.URL
u.Path = cfg.BasePath
schema.Servers = append(schema.Servers, &openapi3.Server{
URL: s.URL.String(),
URL: u.String(),
})
}

View File

@ -21,9 +21,11 @@ func TestOpenAPI(t *testing.T) {
t.Parallel()
u1, err := url.Parse(gofakeit.URL())
u1.Path = ""
require.NoError(t, err)
u2, err := url.Parse(gofakeit.URL())
u2.Path = ""
require.NoError(t, err)
servers := []port.OpenAPIConfigServer{
@ -35,10 +37,13 @@ func TestOpenAPI(t *testing.T) {
},
}
basePath := "/api"
handler := newAPIHTTPHandler(t, func(cfg *apiHTTPHandlerConfig) {
cfg.options = append(cfg.options, port.WithOpenAPIConfig(port.OpenAPIConfig{
Enabled: true,
Servers: servers,
Enabled: true,
BasePath: basePath,
Servers: servers,
}))
})
@ -52,7 +57,7 @@ func TestOpenAPI(t *testing.T) {
require.NoError(t, err)
for i, expected := range servers {
idx := slices.IndexFunc(body.Servers, func(s *openapi3.Server) bool {
return s.URL == expected.URL.String()
return s.URL == expected.URL.String()+basePath
})
assert.GreaterOrEqualf(t, idx, 0, "expected[%d] not found", i)
}

View File

@ -6,6 +6,9 @@ import (
"net/http/httptest"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/bun/buntest"
"gitea.dwysokinski.me/twhelp/corev3/internal/port"
)
@ -22,7 +25,12 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
opt(cfg)
}
return port.NewAPIHTTPHandler(cfg.options...)
versionRepo := adapter.NewVersionBunRepository(buntest.NewSQLiteDB(tb))
return port.NewAPIHTTPHandler(
app.NewVersionService(versionRepo),
cfg.options...,
)
}
func doRequest(h http.Handler, method, target string, body io.Reader) *http.Response {

View File

@ -0,0 +1,28 @@
package port
import (
"net/http"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/apimodel"
)
func (h *apiHTTPHandler) ListVersions(w http.ResponseWriter, r *http.Request, params apimodel.ListVersionsParams) {
domainParams := domain.NewListVersionsParams()
if params.Limit != nil {
_ = domainParams.SetLimit(*params.Limit)
}
if params.Cursor != nil {
_ = domainParams.SetEncodedCursor(*params.Cursor)
}
res, err := h.versionSvc.List(r.Context(), domainParams)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
h.renderJSON(w, r, http.StatusOK, apimodel.NewListVersionsResponse(res))
}

View File

@ -1,3 +1,30 @@
package apimodel
import "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
//go:generate oapi-codegen --config=config.yml ../../../../api/openapi3.yml
func NewVersion(v domain.Version) Version {
return Version{
Code: v.Code(),
Host: v.Host(),
Name: v.Name(),
Timezone: v.Timezone(),
}
}
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(),
}
for _, v := range versions {
resp.Items = append(resp.Items, NewVersion(v))
}
return resp
}