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"
|
||||
"time"
|
||||
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/rest"
|
||||
|
||||
|
@ -19,9 +19,6 @@ import (
|
|||
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
|
||||
"github.com/Kichiyaki/appmode/v2"
|
||||
"github.com/Kichiyaki/chizap"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
@ -29,8 +26,6 @@ import (
|
|||
|
||||
"gitea.dwysokinski.me/twhelp/core/cmd/twhelp/internal"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
_ "gitea.dwysokinski.me/twhelp/core/cmd/twhelp/internal/serve/internal/docs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -40,7 +35,6 @@ const (
|
|||
writeTimeout = 2 * time.Second
|
||||
idleTimeout = 2 * time.Second
|
||||
serverShutdownTimeout = 10 * time.Second
|
||||
corsMaxAge = 300
|
||||
)
|
||||
|
||||
func New() *cli.Command {
|
||||
|
@ -59,8 +53,9 @@ func New() *cli.Command {
|
|||
logger := zap.L()
|
||||
|
||||
srv, err := newServer(serverConfig{
|
||||
logger: logger,
|
||||
db: db,
|
||||
appVersion: c.App.Version,
|
||||
logger: logger,
|
||||
db: db,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("newServer: %w", err)
|
||||
|
@ -83,24 +78,41 @@ func New() *cli.Command {
|
|||
}
|
||||
|
||||
type serverConfig struct {
|
||||
logger *zap.Logger
|
||||
db *bun.DB
|
||||
appVersion string
|
||||
logger *zap.Logger
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
func newServer(cfg serverConfig) (*http.Server, error) {
|
||||
apiCfg, err := newAPIConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newAPIConfig: %w", err)
|
||||
}
|
||||
|
||||
// repos
|
||||
versionRepo := bundb.NewVersion(cfg.db)
|
||||
serverRepo := bundb.NewServer(cfg.db)
|
||||
|
||||
// services
|
||||
versionSvc := service.NewVersion(versionRepo)
|
||||
serverSvc := service.NewServer(serverRepo, internal.NewTWClient(cfg.appVersion))
|
||||
|
||||
// router
|
||||
r := chi.NewRouter()
|
||||
r.Use(getChiMiddlewares(cfg.logger)...)
|
||||
|
||||
rest.NewVersion(versionSvc).Register(r)
|
||||
|
||||
r.Get("/swagger/*", httpSwagger.Handler())
|
||||
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
|
||||
VersionService: versionSvc,
|
||||
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{
|
||||
Addr: ":" + defaultPort,
|
||||
|
@ -112,30 +124,30 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
|||
}, 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 {
|
||||
mws := chi.Middlewares{
|
||||
return chi.Middlewares{
|
||||
middleware.RealIP,
|
||||
chizap.Logger(logger),
|
||||
middleware.Recoverer,
|
||||
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) {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
docs.go
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
swagger.json
|
|
@ -1,7 +1,7 @@
|
|||
package model
|
||||
|
||||
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"`
|
||||
} // @name ApiError
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
)
|
||||
|
||||
type Server struct {
|
||||
Key string `json:"key"`
|
||||
URL string `json:"url"`
|
||||
Key string `json:"key" example:"pl151"`
|
||||
URL string `json:"url" example:"https://pl151.plemiona.pl"`
|
||||
Open bool `json:"open"`
|
||||
NumPlayers int64 `json:"numPlayers"`
|
||||
NumTribes int64 `json:"numTribes"`
|
||||
|
|
|
@ -3,10 +3,10 @@ package model
|
|||
import "gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||
|
||||
type Version struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Timezone string `json:"timezone"`
|
||||
Code string `json:"code" example:"pl"`
|
||||
Name string `json:"name" example:"Poland"`
|
||||
Host string `json:"host" example:"plemiona.pl"`
|
||||
Timezone string `json:"timezone" example:"Europe/Warsaw"`
|
||||
} // @name 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
|
||||
// @version 1.0
|
|
@ -6,9 +6,16 @@ import (
|
|||
"net/http"
|
||||
"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/domain"
|
||||
_ "gitea.dwysokinski.me/twhelp/core/internal/rest/internal/docs"
|
||||
)
|
||||
|
||||
//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) {
|
||||
var convError domain.Error
|
||||
if !errors.As(err, &convError) {
|
||||
|
|
|
@ -17,19 +17,10 @@ type VersionService interface {
|
|||
GetByCode(ctx context.Context, code string) (domain.Version, error)
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
type version struct {
|
||||
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
|
||||
// @Summary List versions
|
||||
// @Description List all versions
|
||||
|
@ -39,7 +30,7 @@ func (v *Version) Register(r chi.Router) {
|
|||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Header 200 {integer} X-Total-Count "Total number of records"
|
||||
// @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})
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
|
@ -59,12 +50,12 @@ func (v *Version) list(w http.ResponseWriter, r *http.Request) {
|
|||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Param versionCode path string true "Version code"
|
||||
// @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()
|
||||
version, err := v.svc.GetByCode(ctx, chi.URLParamFromCtx(ctx, "versionCode"))
|
||||
ver, err := v.svc.GetByCode(ctx, chi.URLParamFromCtx(ctx, "versionCode"))
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
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/rest/internal/mock"
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/rest/internal/model"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
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.Parallel()
|
||||
|
||||
svc := mock.FakeVersionService{}
|
||||
svc := &mock.FakeVersionService{}
|
||||
if tt.setup != nil {
|
||||
tt.setup(&svc)
|
||||
tt.setup(svc)
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
rest.NewVersion(&svc).Register(router)
|
||||
router := rest.NewRouter(rest.RouterConfig{
|
||||
VersionService: svc,
|
||||
})
|
||||
|
||||
resp := doRequest(router, http.MethodGet, "/api/v1/versions", nil)
|
||||
resp := doRequest(router, http.MethodGet, "/v1/versions", nil)
|
||||
defer resp.Body.Close()
|
||||
assertTotalCount(t, resp.Header, tt.expectedTotalCount)
|
||||
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.Parallel()
|
||||
|
||||
svc := mock.FakeVersionService{}
|
||||
svc := &mock.FakeVersionService{}
|
||||
if tt.setup != nil {
|
||||
tt.setup(&svc)
|
||||
tt.setup(svc)
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
rest.NewVersion(&svc).Register(router)
|
||||
router := rest.NewRouter(rest.RouterConfig{
|
||||
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()
|
||||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||
})
|
||||
|
|
Reference in New Issue
Block a user