feat: add a new endpoint - GET /api/v1/versions/:code/servers
This commit is contained in:
parent
f291c2f22c
commit
4487aa8e06
|
@ -10,7 +10,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httpSwagger "github.com/swaggo/http-swagger"
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/rest"
|
"gitea.dwysokinski.me/twhelp/core/internal/rest"
|
||||||
|
|
||||||
|
@ -19,9 +19,6 @@ import (
|
||||||
|
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
|
|
||||||
"github.com/go-chi/cors"
|
|
||||||
|
|
||||||
"github.com/Kichiyaki/appmode/v2"
|
|
||||||
"github.com/Kichiyaki/chizap"
|
"github.com/Kichiyaki/chizap"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
@ -29,8 +26,6 @@ import (
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/core/cmd/twhelp/internal"
|
"gitea.dwysokinski.me/twhelp/core/cmd/twhelp/internal"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
_ "gitea.dwysokinski.me/twhelp/core/cmd/twhelp/internal/serve/internal/docs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -40,7 +35,6 @@ const (
|
||||||
writeTimeout = 2 * time.Second
|
writeTimeout = 2 * time.Second
|
||||||
idleTimeout = 2 * time.Second
|
idleTimeout = 2 * time.Second
|
||||||
serverShutdownTimeout = 10 * time.Second
|
serverShutdownTimeout = 10 * time.Second
|
||||||
corsMaxAge = 300
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func New() *cli.Command {
|
func New() *cli.Command {
|
||||||
|
@ -59,8 +53,9 @@ func New() *cli.Command {
|
||||||
logger := zap.L()
|
logger := zap.L()
|
||||||
|
|
||||||
srv, err := newServer(serverConfig{
|
srv, err := newServer(serverConfig{
|
||||||
logger: logger,
|
appVersion: c.App.Version,
|
||||||
db: db,
|
logger: logger,
|
||||||
|
db: db,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("newServer: %w", err)
|
return fmt.Errorf("newServer: %w", err)
|
||||||
|
@ -83,24 +78,41 @@ func New() *cli.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
logger *zap.Logger
|
appVersion string
|
||||||
db *bun.DB
|
logger *zap.Logger
|
||||||
|
db *bun.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(cfg serverConfig) (*http.Server, error) {
|
func newServer(cfg serverConfig) (*http.Server, error) {
|
||||||
|
apiCfg, err := newAPIConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newAPIConfig: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// repos
|
// repos
|
||||||
versionRepo := bundb.NewVersion(cfg.db)
|
versionRepo := bundb.NewVersion(cfg.db)
|
||||||
|
serverRepo := bundb.NewServer(cfg.db)
|
||||||
|
|
||||||
// services
|
// services
|
||||||
versionSvc := service.NewVersion(versionRepo)
|
versionSvc := service.NewVersion(versionRepo)
|
||||||
|
serverSvc := service.NewServer(serverRepo, internal.NewTWClient(cfg.appVersion))
|
||||||
|
|
||||||
// router
|
// router
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(getChiMiddlewares(cfg.logger)...)
|
r.Use(getChiMiddlewares(cfg.logger)...)
|
||||||
|
|
||||||
rest.NewVersion(versionSvc).Register(r)
|
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
|
||||||
|
VersionService: versionSvc,
|
||||||
r.Get("/swagger/*", httpSwagger.Handler())
|
ServerService: serverSvc,
|
||||||
|
CORS: rest.CORSConfig{
|
||||||
|
Enabled: apiCfg.CORSEnabled,
|
||||||
|
AllowedOrigins: nil,
|
||||||
|
AllowedMethods: apiCfg.CORSAllowedMethods,
|
||||||
|
AllowCredentials: apiCfg.CORSAllowCredentials,
|
||||||
|
MaxAge: int(apiCfg.CORSMaxAge),
|
||||||
|
},
|
||||||
|
Swagger: apiCfg.EnableSwagger,
|
||||||
|
}))
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
Addr: ":" + defaultPort,
|
Addr: ":" + defaultPort,
|
||||||
|
@ -112,30 +124,30 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiConfig struct {
|
||||||
|
EnableSwagger bool `envconfig:"ENABLE_SWAGGER" default:"true"`
|
||||||
|
CORSEnabled bool `envconfig:"CORS_ENABLED" default:"false"`
|
||||||
|
CORSAllowedOrigins []string `envconfig:"CORS_ALLOWED_ORIGINS" default:""`
|
||||||
|
CORSAllowCredentials bool `envconfig:"CORS_ALLOW_CREDENTIALS" default:"false"`
|
||||||
|
CORSAllowedMethods []string `envconfig:"CORS_ALLOWED_METHODS" default:"HEAD,GET"`
|
||||||
|
CORSMaxAge uint32 `envconfig:"CORS_MAX_AGE" default:"300"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAPIConfig() (apiConfig, error) {
|
||||||
|
var cfg apiConfig
|
||||||
|
if err := envconfig.Process("API", &cfg); err != nil {
|
||||||
|
return apiConfig{}, fmt.Errorf("envconfig.Process")
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getChiMiddlewares(logger *zap.Logger) chi.Middlewares {
|
func getChiMiddlewares(logger *zap.Logger) chi.Middlewares {
|
||||||
mws := chi.Middlewares{
|
return chi.Middlewares{
|
||||||
middleware.RealIP,
|
middleware.RealIP,
|
||||||
chizap.Logger(logger),
|
chizap.Logger(logger),
|
||||||
middleware.Recoverer,
|
middleware.Recoverer,
|
||||||
middleware.Heartbeat("/health"),
|
middleware.Heartbeat("/health"),
|
||||||
}
|
}
|
||||||
if appmode.Equal(appmode.DevelopmentMode) {
|
|
||||||
mws = append(mws, newCORSMiddlewareForDevMode())
|
|
||||||
}
|
|
||||||
return mws
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCORSMiddlewareForDevMode() func(next http.Handler) http.Handler {
|
|
||||||
return cors.Handler(cors.Options{
|
|
||||||
AllowOriginFunc: func(*http.Request, string) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
AllowCredentials: true,
|
|
||||||
ExposedHeaders: []string{"Authorization"},
|
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"},
|
|
||||||
AllowedHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
|
|
||||||
MaxAge: corsMaxAge,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServer(srv *http.Server, logger *zap.Logger) {
|
func runServer(srv *http.Server, logger *zap.Logger) {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
docs.go
|
docs.go
|
||||||
swagger.json
|
|
||||||
swagger.yaml
|
swagger.yaml
|
||||||
|
swagger.json
|
|
@ -1,7 +1,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type APIError struct {
|
type APIError struct {
|
||||||
Code string `json:"code" enums:"entity-not-found,validation-error,internal-server-error"`
|
Code string `json:"code" enums:"entity-not-found,route-not-found,method-not-allowed,validation-error,internal-server-error"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
} // @name ApiError
|
} // @name ApiError
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key" example:"pl151"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url" example:"https://pl151.plemiona.pl"`
|
||||||
Open bool `json:"open"`
|
Open bool `json:"open"`
|
||||||
NumPlayers int64 `json:"numPlayers"`
|
NumPlayers int64 `json:"numPlayers"`
|
||||||
NumTribes int64 `json:"numTribes"`
|
NumTribes int64 `json:"numTribes"`
|
||||||
|
|
|
@ -3,10 +3,10 @@ package model
|
||||||
import "gitea.dwysokinski.me/twhelp/core/internal/domain"
|
import "gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
|
||||||
type Version struct {
|
type Version struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code" example:"pl"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" example:"Poland"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host" example:"plemiona.pl"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone" example:"Europe/Warsaw"`
|
||||||
} // @name Version
|
} // @name Version
|
||||||
|
|
||||||
func NewVersion(v domain.Version) Version {
|
func NewVersion(v domain.Version) Version {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package serve
|
package rest
|
||||||
|
|
||||||
//go:generate swag init -g openapi.go -d "./,../../../../internal/rest" -o "./internal/docs"
|
//go:generate swag init -g openapi.go -o "./internal/docs"
|
||||||
|
|
||||||
// @title TWHelp API
|
// @title TWHelp API
|
||||||
// @version 1.0
|
// @version 1.0
|
|
@ -6,9 +6,16 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
httpSwagger "github.com/swaggo/http-swagger"
|
||||||
|
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
|
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
|
||||||
|
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
|
_ "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/docs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate counterfeiter -generate
|
//go:generate counterfeiter -generate
|
||||||
|
@ -20,6 +27,82 @@ var (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CORSConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
AllowedOrigins []string
|
||||||
|
AllowedMethods []string
|
||||||
|
AllowCredentials bool
|
||||||
|
MaxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouterConfig struct {
|
||||||
|
VersionService VersionService
|
||||||
|
ServerService ServerService
|
||||||
|
CORS CORSConfig
|
||||||
|
Swagger bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||||
|
// handlers
|
||||||
|
versionHandler := &version{
|
||||||
|
svc: cfg.VersionService,
|
||||||
|
}
|
||||||
|
serverHandler := &Server{
|
||||||
|
svc: cfg.ServerService,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
if cfg.CORS.Enabled {
|
||||||
|
router.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: cfg.CORS.AllowedOrigins,
|
||||||
|
AllowCredentials: cfg.CORS.AllowCredentials,
|
||||||
|
AllowedMethods: cfg.CORS.AllowedMethods,
|
||||||
|
AllowedHeaders: []string{"Origin", "Content-Length", "Content-Type"},
|
||||||
|
MaxAge: cfg.CORS.MaxAge,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
router.NotFound(routeNotFoundHandler)
|
||||||
|
router.MethodNotAllowed(methodNotAllowedHandler)
|
||||||
|
|
||||||
|
router.Route("/v1", func(r chi.Router) {
|
||||||
|
r.Route("/versions", func(r chi.Router) {
|
||||||
|
r.Get("/", versionHandler.list)
|
||||||
|
r.Route("/{versionCode}", func(r chi.Router) {
|
||||||
|
r.Get("/", versionHandler.getByCode)
|
||||||
|
r.Route("/servers", func(r chi.Router) {
|
||||||
|
r.Get("/", serverHandler.list)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if cfg.Swagger {
|
||||||
|
router.Get("/swagger/*", httpSwagger.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeNotFoundHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
renderJSON(w, http.StatusNotFound, model.ErrorResp{
|
||||||
|
Error: model.APIError{
|
||||||
|
Code: "route-not-found",
|
||||||
|
Message: "route not found",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func methodNotAllowedHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
renderJSON(w, http.StatusMethodNotAllowed, model.ErrorResp{
|
||||||
|
Error: model.APIError{
|
||||||
|
Code: "method-not-allowed",
|
||||||
|
Message: "method not allowed",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func renderErr(w http.ResponseWriter, err error) {
|
func renderErr(w http.ResponseWriter, err error) {
|
||||||
var convError domain.Error
|
var convError domain.Error
|
||||||
if !errors.As(err, &convError) {
|
if !errors.As(err, &convError) {
|
||||||
|
|
|
@ -17,19 +17,10 @@ type VersionService interface {
|
||||||
GetByCode(ctx context.Context, code string) (domain.Version, error)
|
GetByCode(ctx context.Context, code string) (domain.Version, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Version struct {
|
type version struct {
|
||||||
svc VersionService
|
svc VersionService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVersion(svc VersionService) *Version {
|
|
||||||
return &Version{svc: svc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Version) Register(r chi.Router) {
|
|
||||||
r.Get("/api/v1/versions", v.list)
|
|
||||||
r.Get("/api/v1/versions/{versionCode}", v.getByCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ID listVersions
|
// @ID listVersions
|
||||||
// @Summary List versions
|
// @Summary List versions
|
||||||
// @Description List all versions
|
// @Description List all versions
|
||||||
|
@ -39,7 +30,7 @@ func (v *Version) Register(r chi.Router) {
|
||||||
// @Failure 500 {object} model.ErrorResp
|
// @Failure 500 {object} model.ErrorResp
|
||||||
// @Header 200 {integer} X-Total-Count "Total number of records"
|
// @Header 200 {integer} X-Total-Count "Total number of records"
|
||||||
// @Router /versions [get]
|
// @Router /versions [get]
|
||||||
func (v *Version) list(w http.ResponseWriter, r *http.Request) {
|
func (v *version) list(w http.ResponseWriter, r *http.Request) {
|
||||||
versions, count, err := v.svc.List(r.Context(), domain.ListVersionsParams{Count: true})
|
versions, count, err := v.svc.List(r.Context(), domain.ListVersionsParams{Count: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderErr(w, err)
|
renderErr(w, err)
|
||||||
|
@ -59,12 +50,12 @@ func (v *Version) list(w http.ResponseWriter, r *http.Request) {
|
||||||
// @Failure 500 {object} model.ErrorResp
|
// @Failure 500 {object} model.ErrorResp
|
||||||
// @Param versionCode path string true "Version code"
|
// @Param versionCode path string true "Version code"
|
||||||
// @Router /versions/{versionCode} [get]
|
// @Router /versions/{versionCode} [get]
|
||||||
func (v *Version) getByCode(w http.ResponseWriter, r *http.Request) {
|
func (v *version) getByCode(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
version, err := v.svc.GetByCode(ctx, chi.URLParamFromCtx(ctx, "versionCode"))
|
ver, err := v.svc.GetByCode(ctx, chi.URLParamFromCtx(ctx, "versionCode"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderErr(w, err)
|
renderErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderJSON(w, http.StatusOK, model.NewGetVersionByCodeResp(version))
|
renderJSON(w, http.StatusOK, model.NewGetVersionByCodeResp(ver))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,6 @@ import (
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock"
|
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/mock"
|
||||||
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
|
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVersionHandler_list(t *testing.T) {
|
func TestVersionHandler_list(t *testing.T) {
|
||||||
|
@ -83,15 +81,16 @@ func TestVersionHandler_list(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc := mock.FakeVersionService{}
|
svc := &mock.FakeVersionService{}
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(&svc)
|
tt.setup(svc)
|
||||||
}
|
}
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := rest.NewRouter(rest.RouterConfig{
|
||||||
rest.NewVersion(&svc).Register(router)
|
VersionService: svc,
|
||||||
|
})
|
||||||
|
|
||||||
resp := doRequest(router, http.MethodGet, "/api/v1/versions", nil)
|
resp := doRequest(router, http.MethodGet, "/v1/versions", nil)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
assertTotalCount(t, resp.Header, tt.expectedTotalCount)
|
assertTotalCount(t, resp.Header, tt.expectedTotalCount)
|
||||||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||||
|
@ -155,15 +154,16 @@ func TestVersion_getByCode(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc := mock.FakeVersionService{}
|
svc := &mock.FakeVersionService{}
|
||||||
if tt.setup != nil {
|
if tt.setup != nil {
|
||||||
tt.setup(&svc)
|
tt.setup(svc)
|
||||||
}
|
}
|
||||||
|
|
||||||
router := chi.NewRouter()
|
router := rest.NewRouter(rest.RouterConfig{
|
||||||
rest.NewVersion(&svc).Register(router)
|
VersionService: svc,
|
||||||
|
})
|
||||||
|
|
||||||
resp := doRequest(router, http.MethodGet, "/api/v1/versions/"+tt.code, nil)
|
resp := doRequest(router, http.MethodGet, "/v1/versions/"+tt.code, nil)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||||
})
|
})
|
||||||
|
|
Reference in New Issue
Block a user