// SPDX-License-Identifier: EUPL-1.2 // Package api provides a Gin-based REST framework with OpenAPI generation. // Subsystems implement RouteGroup to register their own endpoints. package api import ( "context" "errors" "net/http" "time" "github.com/gin-contrib/expvar" "github.com/gin-contrib/pprof" "github.com/gin-gonic/gin" ) const defaultAddr = ":8080" // shutdownTimeout is the maximum duration to wait for in-flight requests // to complete during graceful shutdown. const shutdownTimeout = 10 * time.Second // Engine is the central API server managing route groups and middleware. type Engine struct { addr string groups []RouteGroup middlewares []gin.HandlerFunc wsHandler http.Handler sseBroker *SSEBroker swaggerEnabled bool swaggerTitle string swaggerDesc string swaggerVersion string pprofEnabled bool expvarEnabled bool graphql *graphqlConfig } // New creates an Engine with the given options. // The default listen address is ":8080". func New(opts ...Option) (*Engine, error) { e := &Engine{ addr: defaultAddr, } for _, opt := range opts { opt(e) } return e, nil } // Addr returns the configured listen address. func (e *Engine) Addr() string { return e.addr } // Groups returns all registered route groups. func (e *Engine) Groups() []RouteGroup { return e.groups } // Register adds a route group to the engine. func (e *Engine) Register(group RouteGroup) { e.groups = append(e.groups, group) } // Channels returns all WebSocket channel names from registered StreamGroups. // Groups that do not implement StreamGroup are silently skipped. func (e *Engine) Channels() []string { var channels []string for _, g := range e.groups { if sg, ok := g.(StreamGroup); ok { channels = append(channels, sg.Channels()...) } } return channels } // Handler builds the Gin engine and returns it as an http.Handler. // Each call produces a fresh handler reflecting the current set of groups. func (e *Engine) Handler() http.Handler { return e.build() } // Serve starts the HTTP server and blocks until the context is cancelled, // then performs a graceful shutdown allowing in-flight requests to complete. func (e *Engine) Serve(ctx context.Context) error { srv := &http.Server{ Addr: e.addr, Handler: e.build(), } errCh := make(chan error, 1) go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } close(errCh) }() // Block until context is cancelled. <-ctx.Done() // Graceful shutdown with timeout. shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { return err } // Return any listen error that occurred before shutdown. return <-errCh } // build creates a configured Gin engine with recovery middleware, // user-supplied middleware, the health endpoint, and all registered route groups. func (e *Engine) build() *gin.Engine { r := gin.New() r.Use(gin.Recovery()) // Apply user-supplied middleware after recovery but before routes. for _, mw := range e.middlewares { r.Use(mw) } // Built-in health check. r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, OK("healthy")) }) // Mount each registered group at its base path. for _, g := range e.groups { rg := r.Group(g.BasePath()) g.RegisterRoutes(rg) } // Mount WebSocket handler if configured. if e.wsHandler != nil { r.GET("/ws", wrapWSHandler(e.wsHandler)) } // Mount SSE endpoint if configured. if e.sseBroker != nil { r.GET("/events", e.sseBroker.Handler()) } // Mount GraphQL endpoint if configured. if e.graphql != nil { mountGraphQL(r, e.graphql) } // Mount Swagger UI if enabled. if e.swaggerEnabled { registerSwagger(r, e.swaggerTitle, e.swaggerDesc, e.swaggerVersion, e.groups) } // Mount pprof profiling endpoints if enabled. if e.pprofEnabled { pprof.Register(r) } // Mount expvar runtime metrics endpoint if enabled. if e.expvarEnabled { r.GET("/debug/vars", expvar.Handler()) } return r }