api/api.go
Snider de63217168 feat: AX v0.8.0 — Core primitives, Result returns, zero disallowed imports
- api.go: errors.Is → core.Is
- openapi.go: Build returns core.Result, encoding/json → core.JSONMarshal
- export.go: rewrite with core.Fs, core.JSONUnmarshal, returns core.Result
- codegen.go: rewrite with c.Process(), core.Fs, App.Find — no os/exec
- sse.go: encoding/json → core.JSONMarshalString, fmt → core.Sprintf
- swagger.go: fmt → core.Sprintf, Build caller updated for core.Result
- middleware.go: strings → core.HasPrefix/SplitN/Lower
- authentik.go: strings → core.HasPrefix/TrimPrefix/Split/Trim/Contains
- brotli.go: strings → core.Contains

Transport boundary files (net/http, io for compression) retained.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 08:39:59 +00:00

192 lines
4.6 KiB
Go

// 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"
"iter"
"net/http"
"slices"
"time"
core "dappco.re/go/core"
"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
}
// GroupsIter returns an iterator over all registered route groups.
func (e *Engine) GroupsIter() iter.Seq[RouteGroup] {
return slices.Values(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
}
// ChannelsIter returns an iterator over WebSocket channel names from registered StreamGroups.
func (e *Engine) ChannelsIter() iter.Seq[string] {
return func(yield func(string) bool) {
for _, g := range e.groups {
if sg, ok := g.(StreamGroup); ok {
for _, c := range sg.Channels() {
if !yield(c) {
return
}
}
}
}
}
}
// 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 && !core.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
}