diff --git a/api/openapi3.yml b/api/openapi3.yml index cb115e0..b528197 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -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: diff --git a/cmd/twhelp/cmd_serve.go b/cmd/twhelp/cmd_serve.go index b68b14c..03c703b 100644 --- a/cmd/twhelp/cmd_serve.go +++ b/cmd/twhelp/cmd_serve.go @@ -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 }, diff --git a/go.mod b/go.mod index b46613e..6031a32 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d2d0bdf..87a747e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/adapter/bun_utils.go b/internal/adapter/bun_utils.go index fd9bc37..2833d49 100644 --- a/internal/adapter/bun_utils.go +++ b/internal/adapter/bun_utils.go @@ -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 +} diff --git a/internal/adapter/repository_bun_version.go b/internal/adapter/repository_bun_version.go index 43363b5..bccc230 100644 --- a/internal/adapter/repository_bun_version.go +++ b/internal/adapter/repository_bun_version.go @@ -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) } diff --git a/internal/adapter/repository_server_test.go b/internal/adapter/repository_server_test.go index f57a4c7..166f94f 100644 --- a/internal/adapter/repository_server_test.go +++ b/internal/adapter/repository_server_test.go @@ -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), diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index 8849fe3..f889a02 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -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 { diff --git a/internal/adapter/repository_version_test.go b/internal/adapter/repository_version_test.go index 1d69418..01c4d20 100644 --- a/internal/adapter/repository_version_test.go +++ b/internal/adapter/repository_version_test.go @@ -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) }) } }) diff --git a/internal/app/service_data_sync.go b/internal/app/service_data_sync.go index 3e88ceb..713bca9 100644 --- a/internal/app/service_data_sync.go +++ b/internal/app/service_data_sync.go @@ -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 { diff --git a/internal/app/service_snapshot.go b/internal/app/service_snapshot.go index bee5fd7..f186010 100644 --- a/internal/app/service_snapshot.go +++ b/internal/app/service_snapshot.go @@ -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()) diff --git a/internal/app/service_version.go b/internal/app/service_version.go index b337090..96e224a 100644 --- a/internal/app/service_version.go +++ b/internal/app/service_version.go @@ -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) } diff --git a/internal/domain/domaintest/version.go b/internal/domain/domaintest/version.go index eed7efa..2bf5480 100644 --- a/internal/domain/domaintest/version.go +++ b/internal/domain/domaintest/version.go @@ -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 { diff --git a/internal/domain/ennoblement.go b/internal/domain/ennoblement.go index a8f770b..2f5b85b 100644 --- a/internal/domain/ennoblement.go +++ b/internal/domain/ennoblement.go @@ -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{ diff --git a/internal/domain/player.go b/internal/domain/player.go index 9833ce9..f5f6797 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -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{ diff --git a/internal/domain/server.go b/internal/domain/server.go index c12e6f6..2c2149f 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -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{ diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index 9fe8a52..3d90d7d 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -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{ diff --git a/internal/domain/tribe_change.go b/internal/domain/tribe_change.go index 1e1d4a7..6225da6 100644 --- a/internal/domain/tribe_change.go +++ b/internal/domain/tribe_change.go @@ -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{ diff --git a/internal/domain/tribe_snapshot.go b/internal/domain/tribe_snapshot.go index 73db13c..b97ea42 100644 --- a/internal/domain/tribe_snapshot.go +++ b/internal/domain/tribe_snapshot.go @@ -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{ diff --git a/internal/domain/version.go b/internal/domain/version.go index bcd7574..2cc155a 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -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 +} diff --git a/internal/domain/version_test.go b/internal/domain/version_test.go index d544d8f..9d6d496 100644 --- a/internal/domain/version_test.go +++ b/internal/domain/version_test.go @@ -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 diff --git a/internal/domain/village.go b/internal/domain/village.go index 5e59c78..0f304ea 100644 --- a/internal/domain/village.go +++ b/internal/domain/village.go @@ -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{ diff --git a/internal/port/handler_http_api.go b/internal/port/handler_http_api.go index 9defaa9..e9eb4d0 100644 --- a/internal/port/handler_http_api.go +++ b/internal/port/handler_http_api.go @@ -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) }), diff --git a/internal/port/handler_http_api_openapi.go b/internal/port/handler_http_api_openapi.go index 10dd5df..270657e 100644 --- a/internal/port/handler_http_api_openapi.go +++ b/internal/port/handler_http_api_openapi.go @@ -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(), }) } diff --git a/internal/port/handler_http_api_openapi_test.go b/internal/port/handler_http_api_openapi_test.go index c7cd193..4d61193 100644 --- a/internal/port/handler_http_api_openapi_test.go +++ b/internal/port/handler_http_api_openapi_test.go @@ -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) } diff --git a/internal/port/handler_http_api_test.go b/internal/port/handler_http_api_test.go index 0ad81bb..2de01b2 100644 --- a/internal/port/handler_http_api_test.go +++ b/internal/port/handler_http_api_test.go @@ -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 { diff --git a/internal/port/handler_http_api_version.go b/internal/port/handler_http_api_version.go new file mode 100644 index 0000000..45e5896 --- /dev/null +++ b/internal/port/handler_http_api_version.go @@ -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)) +} diff --git a/internal/port/internal/apimodel/apimodel.go b/internal/port/internal/apimodel/apimodel.go index 5e391ca..b9f4161 100644 --- a/internal/port/internal/apimodel/apimodel.go +++ b/internal/port/internal/apimodel/apimodel.go @@ -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 +}