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 <link rel="search">.
func writeOSD(w http.ResponseWriter, body []byte) {
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
_, _ = w.Write(body)
}