package search import ( "net/http" "github.com/go-chi/chi/v5" "sourcecraft.dev/bigbes/huntsman/internal/pkg/apierror" "sourcecraft.dev/bigbes/huntsman/internal/pkg/httputil" ) // Handler exposes the search service over HTTP. type Handler struct { svc *Service } // NewHandler wires a Service into an HTTP handler. func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // RegisterRoutes attaches the search-related routes to a chi router. // // Routes: // // GET /search?q=... → 302 to the chosen provider // GET /providers → JSON list of providers // GET /opensearch.xml → unified OSD pointing at /search // GET /opensearch/{provider}.xml → OSD pointing directly at provider func (h *Handler) RegisterRoutes(r chi.Router) { r.Get("/search", h.Search) r.Get("/providers", h.ListProviders) r.Get("/opensearch.xml", h.OpenSearchRouter) r.Get("/opensearch/{provider}.xml", h.OpenSearchProvider) } // Search routes the query string parameter "q" to the matching provider // and returns a 302 redirect to the upstream search URL. func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { q := r.URL.Query().Get("q") provider, cleaned, err := h.svc.Route(q) if err != nil { httputil.Error(w, r, err) return } target := provider.SearchURLFor(cleaned) http.Redirect(w, r, target, http.StatusFound) } // providerView is the JSON shape returned by /providers. type providerView struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` HomeURL string `json:"home_url"` OpenSearchURL string `json:"opensearch_url"` } // ListProviders returns the registered providers as JSON. func (h *Handler) ListProviders(w http.ResponseWriter, _ *http.Request) { providers := h.svc.Providers() out := make([]providerView, 0, len(providers)) for _, p := range providers { out = append(out, providerView{ ID: p.ID, Name: p.Name, Description: p.Description, HomeURL: p.HomeURL, OpenSearchURL: h.svc.PublicURL() + "/opensearch/" + p.ID + ".xml", }) } httputil.OK(w, map[string]any{ "default": h.svc.defaultProvider, "providers": out, }) } // OpenSearchRouter serves the unified OSD XML document, which makes the // browser send searches to /search where prefix routing kicks in. func (h *Handler) OpenSearchRouter(w http.ResponseWriter, r *http.Request) { osd := DescriptionForRouter(h.svc.PublicURL()) body, err := Marshal(osd) if err != nil { httputil.Error(w, r, err) return } writeOSD(w, body) } // OpenSearchProvider serves an OSD document for a single upstream provider, // useful when the user wants to add e.g. urbandictionary directly. func (h *Handler) OpenSearchProvider(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "provider") provider, ok := h.svc.Lookup(id) if !ok { httputil.Error(w, r, apierror.NotFound("provider "+id)) return } body, err := Marshal(DescriptionForProvider(provider)) if err != nil { httputil.Error(w, r, err) return } writeOSD(w, body) } // writeOSD writes a marshaled OSD document with the correct MIME type. // // The opensearchdescription+xml MIME type is what browsers look for when // auto-detecting search engines via . func writeOSD(w http.ResponseWriter, body []byte) { w.Header().Set("Content-Type", "application/opensearchdescription+xml") _, _ = w.Write(body) }