// 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-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 } // 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) } // 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) } return r }