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

View File

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

8
go.mod
View File

@ -8,11 +8,11 @@ require (
github.com/brianvoe/gofakeit/v6 v6.28.0 github.com/brianvoe/gofakeit/v6 v6.28.0
github.com/cenkalti/backoff/v4 v4.2.1 github.com/cenkalti/backoff/v4 v4.2.1
github.com/elliotchance/phpserialize v1.3.3 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/chi/v5 v5.0.11
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/google/go-cmp v0.6.0 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/oapi-codegen/runtime v1.1.1
github.com/ory/dockertest/v3 v3.10.0 github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
@ -42,8 +42,8 @@ require (
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.8 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/errwrap v1.1.0 // 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.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 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 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/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.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= 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 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 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.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.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 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 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/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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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.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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/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.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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= 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("rank_total = EXCLUDED.rank_total").
Set("score_total = EXCLUDED.score_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} 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 var versions bunmodel.Versions
if err := repo.db.NewSelect(). if err := repo.db.NewSelect().
Model(&versions). Model(&versions).
Apply(listVersionsParamsApplier{params: params}.apply). Apply(listVersionsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { 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() converted, err := versions.ToDomain()
}
func (repo *VersionBunRepository) ListCount(
ctx context.Context,
params domain.ListVersionsParams,
) (domain.Versions, int, error) {
versions, err := repo.List(ctx, params)
if err != nil { 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 { type listVersionsParamsApplier struct {
@ -50,15 +46,25 @@ type listVersionsParamsApplier struct {
func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { func (a listVersionsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
for _, s := range a.params.Sort() { for _, s := range a.params.Sort() {
codeCursor := a.params.Cursor().Code()
switch s { switch s {
case domain.VersionSortCodeASC: case domain.VersionSortCodeASC:
if codeCursor.Valid {
q = q.Where("version.code >= ?", codeCursor.Value)
}
q = q.Order("version.code ASC") q = q.Order("version.code ASC")
case domain.VersionSortCodeDESC: case domain.VersionSortCodeDESC:
if codeCursor.Valid {
q = q.Where("version.code <= ?", codeCursor.Value)
}
q = q.Order("version.code DESC") q = q.Order("version.code DESC")
default: default:
return q.Err(errors.New("unsupported sort value")) 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.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
versions, err := repos.version.List(ctx, domain.NewListVersionsParams()) listVersionsRes, err := repos.version.List(ctx, domain.NewListVersionsParams())
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, versions) require.NotEmpty(t, listVersionsRes)
version := versions[0] version := listVersionsRes.Versions()[0]
serversToCreate := domain.BaseServers{ serversToCreate := domain.BaseServers{
domaintest.NewBaseServer(t), domaintest.NewBaseServer(t),

View File

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

View File

@ -7,6 +7,7 @@ import (
"testing" "testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain" "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -16,17 +17,16 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
ctx := context.Background() ctx := context.Background()
t.Run("List & ListCount", func(t *testing.T) { t.Run("List", func(t *testing.T) {
t.Parallel() t.Parallel()
repos := newRepos(t) repos := newRepos(t)
tests := []struct { tests := []struct {
name string name string
params func(t *testing.T) domain.ListVersionsParams params func(t *testing.T) domain.ListVersionsParams
assertVersions func(t *testing.T, versions domain.Versions) assertResult func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult)
assertError func(t *testing.T, err error) assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, total int)
}{ }{
{ {
name: "OK: default params", name: "OK: default params",
@ -34,21 +34,18 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Helper() t.Helper()
return domain.NewListVersionsParams() return domain.NewListVersionsParams()
}, },
assertVersions: func(t *testing.T, versions domain.Versions) { assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper() t.Helper()
assert.NotEmpty(t, len(versions)) assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(versions, func(a, b domain.Version) int { assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code()) return cmp.Compare(a.Code(), b.Code())
})) }))
assert.True(t, res.Next().IsZero())
}, },
assertError: func(t *testing.T, err error) { assertError: func(t *testing.T, err error) {
t.Helper() t.Helper()
require.NoError(t, err) require.NoError(t, err)
}, },
assertTotal: func(t *testing.T, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
}, },
{ {
name: "OK: sort=[code DESC]", 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})) require.NoError(t, params.SetSort([]domain.VersionSort{domain.VersionSortCodeDESC}))
return params return params
}, },
assertVersions: func(t *testing.T, versions domain.Versions) { assertResult: func(t *testing.T, params domain.ListVersionsParams, res domain.ListVersionsResult) {
t.Helper() t.Helper()
assert.NotEmpty(t, len(versions)) assert.NotEmpty(t, res.Versions())
assert.True(t, slices.IsSortedFunc(versions, func(a, b domain.Version) int { assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code()) * -1 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() t.Helper()
require.NoError(t, err) 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() 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) res, err := repos.version.List(ctx, params)
tt.assertError(t, err) tt.assertError(t, err)
tt.assertVersions(t, res) tt.assertResult(t, params, res)
res, count, err := repos.version.ListCount(ctx, params)
tt.assertError(t, err)
tt.assertVersions(t, res)
tt.assertTotal(t, count)
}) })
} }
}) })

View File

@ -29,10 +29,11 @@ func NewDataSyncService(
} }
func (svc *DataSyncService) Sync(ctx context.Context) error { 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 { if err != nil {
return err return err
} }
versions := listVersionsRes.Versions()
payloads := make([]domain.SyncServersCmdPayload, 0, len(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 { 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 { if err != nil {
return err return err
} }
versions := listVersionsRes.Versions()
for _, v := range versions { for _, v := range versions {
if err = svc.syncEnnoblementsForVersion(ctx, v); err != nil { if err = svc.syncEnnoblementsForVersion(ctx, v); err != nil {

View File

@ -31,10 +31,11 @@ func NewSnapshotService(
} }
func (svc *SnapshotService) Create(ctx context.Context) error { 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 { if err != nil {
return err return err
} }
versions := listVersionsRes.Versions()
for _, v := range versions { for _, v := range versions {
loc, loopErr := time.LoadLocation(v.Timezone()) loc, loopErr := time.LoadLocation(v.Timezone())

View File

@ -7,7 +7,7 @@ import (
) )
type VersionRepository interface { 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 { type VersionService struct {
@ -18,6 +18,9 @@ func NewVersionService(repo VersionRepository) *VersionService {
return &VersionService{repo: repo} 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) return svc.repo.List(ctx, params)
} }

View File

@ -6,6 +6,30 @@ import (
"github.com/stretchr/testify/require" "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 { type VersionConfig struct {
Code string Code string
} }
@ -21,7 +45,7 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
opt(cfg) opt(cfg)
} }
s, err := domain.UnmarshalVersionFromDatabase( v, err := domain.UnmarshalVersionFromDatabase(
cfg.Code, cfg.Code,
gofakeit.LetterN(10), gofakeit.LetterN(10),
gofakeit.DomainName(), gofakeit.DomainName(),
@ -29,7 +53,7 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
) )
require.NoError(tb, err) require.NoError(tb, err)
return s return v
} }
func RandVersionCode() string { func RandVersionCode() string {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package domain package domain
import ( import (
"encoding/base64"
"net/url" "net/url"
) )
@ -91,6 +92,10 @@ func (v Version) URL() *url.URL {
} }
} }
func (v Version) IsZero() bool {
return v == Version{}
}
type Versions []Version type Versions []Version
type VersionSort uint8 type VersionSort uint8
@ -100,14 +105,77 @@ const (
VersionSortCodeDESC VersionSortCodeDESC
) )
type ListVersionsParams struct { type VersionCursor struct {
sort []VersionSort 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 { func NewListVersionsParams() ListVersionsParams {
return ListVersionsParams{sort: []VersionSort{VersionSortCodeASC}} return ListVersionsParams{
sort: []VersionSort{VersionSortCodeASC},
limit: VersionListMaxLimit,
}
} }
const ( const (
@ -132,3 +200,100 @@ func (params *ListVersionsParams) SetSort(sort []VersionSort) error {
return nil 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 package domain_test
import ( import (
"encoding/base64"
"fmt"
"testing" "testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain" "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/assert"
"github.com/stretchr/testify/require" "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) { func TestListVersionsParams_SetSort(t *testing.T) {
t.Parallel() 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 { type versionCodeValidationTest struct {
name string name string
code string code string

View File

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

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"sync" "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/apimodel"
"gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/swgui" "gitea.dwysokinski.me/twhelp/corev3/internal/port/internal/swgui"
"github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3"
@ -12,7 +13,7 @@ import (
) )
type apiHTTPHandler struct { type apiHTTPHandler struct {
apimodel.Unimplemented versionSvc *app.VersionService
getOpenAPISchema func() (*openapi3.T, error) 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...) cfg := newAPIHTTPHandlerConfig(opts...)
h := &apiHTTPHandler{ h := &apiHTTPHandler{
versionSvc: versionSvc,
getOpenAPISchema: sync.OnceValues(func() (*openapi3.T, error) { getOpenAPISchema: sync.OnceValues(func() (*openapi3.T, error) {
return getOpenAPISchema(cfg.openAPI) 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)) schema.Servers = make(openapi3.Servers, 0, len(cfg.Servers))
for _, s := range cfg.Servers { for _, s := range cfg.Servers {
u := *s.URL
u.Path = cfg.BasePath
schema.Servers = append(schema.Servers, &openapi3.Server{ 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() t.Parallel()
u1, err := url.Parse(gofakeit.URL()) u1, err := url.Parse(gofakeit.URL())
u1.Path = ""
require.NoError(t, err) require.NoError(t, err)
u2, err := url.Parse(gofakeit.URL()) u2, err := url.Parse(gofakeit.URL())
u2.Path = ""
require.NoError(t, err) require.NoError(t, err)
servers := []port.OpenAPIConfigServer{ servers := []port.OpenAPIConfigServer{
@ -35,10 +37,13 @@ func TestOpenAPI(t *testing.T) {
}, },
} }
basePath := "/api"
handler := newAPIHTTPHandler(t, func(cfg *apiHTTPHandlerConfig) { handler := newAPIHTTPHandler(t, func(cfg *apiHTTPHandlerConfig) {
cfg.options = append(cfg.options, port.WithOpenAPIConfig(port.OpenAPIConfig{ cfg.options = append(cfg.options, port.WithOpenAPIConfig(port.OpenAPIConfig{
Enabled: true, Enabled: true,
Servers: servers, BasePath: basePath,
Servers: servers,
})) }))
}) })
@ -52,7 +57,7 @@ func TestOpenAPI(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
for i, expected := range servers { for i, expected := range servers {
idx := slices.IndexFunc(body.Servers, func(s *openapi3.Server) bool { 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) assert.GreaterOrEqualf(t, idx, 0, "expected[%d] not found", i)
} }

View File

@ -6,6 +6,9 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "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" "gitea.dwysokinski.me/twhelp/corev3/internal/port"
) )
@ -22,7 +25,12 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
opt(cfg) 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 { 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 package apimodel
import "gitea.dwysokinski.me/twhelp/corev3/internal/domain"
//go:generate oapi-codegen --config=config.yml ../../../../api/openapi3.yml //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
}