package web import ( "fmt" "io/fs" "net/http" "time" "github.com/nmoniz/any2anexoj/internal/ofigi" ) // Server wires the HTTP handlers, embedded static assets, and shared state // behind a single http.Handler. Use NewServer to construct one. type Server struct { state *State router *http.ServeMux staticFS fs.FS indexHTML []byte figiClient *ofigi.Client } // Option mutates a Server during construction. Use WithFigiClient (mainly in // tests) to inject a custom OpenFIGI client so tests can run offline against // a stub while production uses a real network client. type Option func(*Server) // WithFigiClient overrides the OpenFIGI client used by the report pipeline. // Production should leave this unset so NewServer constructs a real client // (no API key, 5 s timeout) that gracefully falls back to NatureUnknown // when OpenFIGI calls fail. Tests pass a stub backed by an httptest-style // transport to keep the test suite offline and deterministic. func WithFigiClient(c *ofigi.Client) Option { return func(s *Server) { s.figiClient = c } } // NewServer constructs a Server with all routes registered. The staticFS is // the embedded subtree rooted at the directory that should be served under // /static/* (i.e. it must contain an "index.html" file). The returned // Server's Handler() method exposes the configured http.Handler. func NewServer(state *State, staticFS fs.FS, opts ...Option) (*Server, error) { indexBytes, err := fs.ReadFile(staticFS, "index.html") if err != nil { return nil, fmt.Errorf("web: missing index.html in static FS: %w", err) } s := &Server{ state: state, router: http.NewServeMux(), staticFS: staticFS, indexHTML: indexBytes, figiClient: ofigi.NewOpenFIGI(&http.Client{Timeout: 5 * time.Second}, ""), } for _, opt := range opts { opt(s) } s.routes() return s, nil } // Handler returns the configured HTTP handler, wrapped in the auth middleware // stub so future auth wiring only has to change auth.go. func (s *Server) Handler() http.Handler { return AuthMiddleware(s.router) } // routes registers the HTTP routes handled by this server. func (s *Server) routes() { // API routes. s.router.HandleFunc("POST /api/upload", s.handleUpload) s.router.HandleFunc("GET /api/table", s.handleGetTable) // Serve the embedded static assets under /static/*. s.router.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFS)))) // Serve the embedded index page at "/". s.router.HandleFunc("GET /{$}", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write(s.indexHTML) }) }