feat: add a new endpoint - GET /api/v1/versions/:code/servers

This commit is contained in:
Dawid Wysokiński 2022-08-25 06:40:30 +02:00
parent f291c2f22c
commit 4487aa8e06
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
9 changed files with 154 additions and 68 deletions

View File

@ -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) {

View File

@ -1,3 +1,3 @@
docs.go
swagger.json
swagger.yaml
swagger.json

View File

@ -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

View File

@ -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"`

View File

@ -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 {

View File

@ -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

View File

@ -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) {

View File

@ -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))
}

View File

@ -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)
})