Files
public-api/pkg/api/router.go
itzaname 76b6bee69f
All checks were successful
continuous-integration/drone/push Build is passing
Add batch time placement endpoint (#14)
Closes #3

Reviewed-on: #14
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2025-07-06 23:07:31 +00:00

188 lines
4.2 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
"git.itzana.me/strafesnet/public-api/docs"
"git.itzana.me/strafesnet/public-api/pkg/api/handlers"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"net/http"
"time"
)
// Option defines a function that configures a Router
type Option func(*RouterConfig)
// RouterConfig holds all router configuration
type RouterConfig struct {
port int
devClient *grpc.ClientConn
dataClient *grpc.ClientConn
context *cli.Context
shutdownTimeout time.Duration
}
// WithPort sets the port for the server£
func WithPort(port int) Option {
return func(cfg *RouterConfig) {
cfg.port = port
}
}
// WithContext sets the context for the server
func WithContext(ctx *cli.Context) Option {
return func(cfg *RouterConfig) {
cfg.context = ctx
}
}
// WithDevClient sets the dev gRPC client
func WithDevClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.devClient = conn
}
}
// WithDataClient sets the data gRPC client
func WithDataClient(conn *grpc.ClientConn) Option {
return func(cfg *RouterConfig) {
cfg.dataClient = conn
}
}
// WithShutdownTimeout sets the graceful shutdown timeout
func WithShutdownTimeout(timeout time.Duration) Option {
return func(cfg *RouterConfig) {
cfg.shutdownTimeout = timeout
}
}
func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
r := gin.Default()
r.ForwardedByClientIP = true
r.Use(gin.Logger())
r.Use(gin.Recovery())
handlerOptions := []handlers.HandlerOption{
handlers.WithDataClient(cfg.dataClient),
}
// Times handler
timesHandler, err := handlers.NewTimesHandler(handlerOptions...)
if err != nil {
return nil, err
}
// Users handler
usersHandler, err := handlers.NewUserHandler(handlerOptions...)
if err != nil {
return nil, err
}
// Maps handler
mapsHandler, err := handlers.NewMapHandler(handlerOptions...)
if err != nil {
return nil, err
}
// Rank handler
rankHandler, err := handlers.NewRankHandler(handlerOptions...)
if err != nil {
return nil, err
}
docs.SwaggerInfo.BasePath = "/api/v1"
v1 := r.Group("/api/v1")
{
// Auth middleware
v1.Use(middleware.ValidateRequest("Data", "Read", cfg.devClient))
// Times
v1.GET("/time", timesHandler.List)
v1.GET("/time/worldrecord", timesHandler.WrList)
v1.GET("/time/placement", timesHandler.GetPlacements)
v1.GET("/time/:id", timesHandler.Get)
// Users
v1.GET("/user", usersHandler.List)
v1.GET("/user/:id", usersHandler.Get)
v1.GET("/user/:id/rank", usersHandler.GetRank)
// Maps
v1.GET("/map", mapsHandler.List)
v1.GET("/map/:id", mapsHandler.Get)
// Rank
v1.GET("/rank", rankHandler.List)
}
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
r.GET("/", func(ctx *gin.Context) {
ctx.Redirect(http.StatusPermanentRedirect, "/docs/index.html")
})
return r, nil
}
// NewRouter creates a new router with the given options
func NewRouter(options ...Option) error {
// Default configuration
cfg := &RouterConfig{
port: 8080, // Default port
context: nil,
shutdownTimeout: 5 * time.Second,
}
// Apply options
for _, option := range options {
option(cfg)
}
// Validate configuration
if cfg.context == nil {
return errors.New("context is required")
}
if cfg.devClient == nil {
return errors.New("dev client is required")
}
routes, err := setupRoutes(cfg)
if err != nil {
return err
}
log.Info("Starting server")
return runServer(cfg.context.Context, fmt.Sprint(":", cfg.port), routes, cfg.shutdownTimeout)
}
func runServer(ctx context.Context, addr string, r *gin.Engine, shutdownTimeout time.Duration) error {
srv := &http.Server{
Addr: addr,
Handler: r,
}
// Run the server in a separate goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.WithError(err).Fatal("web server exit")
}
}()
// Wait for a shutdown signal
<-ctx.Done()
// Shutdown server gracefully
ctxShutdown, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
return srv.Shutdown(ctxShutdown)
}