// Package api wires together every HTTP-facing component of the service: // // - Tawhiri-compatible v1 endpoints generated from the OpenAPI spec (ogen); // - The new v2 prediction endpoint; // - Dataset and job admin endpoints under /api/v1/admin/; // - Optional Prometheus-format metrics endpoint. package api import ( "context" "fmt" "net/http" "time" "go.uber.org/zap" "predictor-refactored/internal/api/admin" "predictor-refactored/internal/api/middleware" "predictor-refactored/internal/api/tawhiri" v2 "predictor-refactored/internal/api/v2" "predictor-refactored/internal/datasets" "predictor-refactored/internal/elevation" "predictor-refactored/internal/metrics" apirest "predictor-refactored/pkg/rest" ) // Server is the top-level HTTP server. type Server struct { port int mux *http.ServeMux log *zap.Logger } // Deps are the runtime dependencies the API layer needs. type Deps struct { Manager *datasets.Manager Elevation *elevation.Dataset Metrics metrics.Sink MetricsHandler http.Handler // optional; mounted at MetricsPath when non-nil MetricsPath string Log *zap.Logger } // New wires the HTTP server. The returned Server is not yet started. func New(port int, d Deps) (*Server, error) { if d.Log == nil { d.Log = zap.NewNop() } if d.Metrics == nil { d.Metrics = metrics.Noop() } mux := http.NewServeMux() // ogen-generated server handles the Tawhiri-compat surface // (GET /api/v1/prediction and GET /ready). tw := tawhiri.New(d.Manager, d.Elevation, d.Metrics, d.Log) ogenSrv, err := apirest.NewServer(tw, apirest.WithMiddleware(middleware.OgenLogging(d.Log))) if err != nil { return nil, fmt.Errorf("create ogen server: %w", err) } // New primary prediction endpoint. v2h := v2.New(d.Manager, d.Elevation, d.Metrics, d.Log) mux.Handle("/api/v2/prediction", v2h) // Admin endpoints. adminH := admin.New(d.Manager, d.Log) adminH.Register(mux) // Metrics endpoint. if d.MetricsHandler != nil && d.MetricsPath != "" { mux.Handle(d.MetricsPath, d.MetricsHandler) } // Fallback to the ogen-generated routes (v1 + ready) for anything else. mux.Handle("/", ogenSrv) return &Server{ port: port, mux: mux, log: d.Log, }, nil } // Run starts the HTTP server and blocks until it returns. // // The handler chain is: CORS → request logger → mux. func (s *Server) Run(ctx context.Context) error { srv := &http.Server{ Addr: fmt.Sprintf(":%d", s.port), Handler: middleware.CORS(middleware.HTTPLogging(s.log, s.mux)), } errCh := make(chan error, 1) go func() { s.log.Info("HTTP server starting", zap.Int("port", s.port)) errCh <- srv.ListenAndServe() }() select { case err := <-errCh: return err case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) } }