package httpapi import ( "context" "encoding/json" "net/http" "strings" "github.com/metamorphosis-dev/kafka/internal/contracts" "github.com/metamorphosis-dev/kafka/internal/search" "github.com/metamorphosis-dev/kafka/internal/views" ) type Handler struct { searchSvc *search.Service autocompleteSvc func(ctx context.Context, query string) ([]string, error) } func NewHandler(searchSvc *search.Service, autocompleteSuggestions func(ctx context.Context, query string) ([]string, error)) *Handler { return &Handler{ searchSvc: searchSvc, autocompleteSvc: autocompleteSuggestions, } } func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) } // Index renders the homepage with the search box. func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } if err := views.RenderIndex(w); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // OpenSearch serves the OpenSearch description XML. func (h *Handler) OpenSearch(baseURL string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { xml, err := views.OpenSearchXML(baseURL) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/opensearchdescription+xml; charset=utf-8") w.Write(xml) } } func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { // For HTML format with no query, redirect to homepage. if r.FormValue("q") == "" && (r.FormValue("format") == "" || r.FormValue("format") == "html") { http.Redirect(w, r, "/", http.StatusFound) return } req, err := search.ParseSearchRequest(r) if err != nil { // For HTML, render error on the results page. if req.Format == contracts.FormatHTML || r.FormValue("format") == "html" { pd := views.PageData{Query: r.FormValue("q")} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { views.RenderSearch(w, pd) } return } http.Error(w, err.Error(), http.StatusBadRequest) return } resp, err := h.searchSvc.Search(r.Context(), req) if err != nil { if req.Format == contracts.FormatHTML { pd := views.PageData{Query: req.Query} if views.IsHTMXRequest(r) { views.RenderSearchFragment(w, pd) } else { views.RenderSearch(w, pd) } return } http.Error(w, err.Error(), http.StatusInternalServerError) return } if req.Format == contracts.FormatHTML { pd := views.FromResponse(resp, req.Query, req.Pageno) if err := views.RenderSearchAuto(w, r, pd); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if err := search.WriteSearchResponse(w, req.Format, resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // Autocompleter returns search suggestions for the given query. func (h *Handler) Autocompleter(w http.ResponseWriter, r *http.Request) { query := strings.TrimSpace(r.FormValue("q")) if query == "" { w.WriteHeader(http.StatusBadRequest) return } suggestions, err := h.autocompleteSvc(r.Context(), query) if err != nil { // Return empty list on error rather than an error status. suggestions = []string{} } w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(suggestions) }