- /api/search traduit les filtres en requête GitHub (langage, étoiles, activité, création, licence, topic, forks, archivés, good first issues) - cache mémoire des recherches identiques (CACHE_TTL) et suivi du quota - /api/status, /healthz, authentification basique optionnelle - Dockerfile et docker-compose.yml Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
90 lines
2.1 KiB
Go
90 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
)
|
|
|
|
type API struct {
|
|
gh *GitHub
|
|
}
|
|
|
|
func (a *API) Routes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /healthz", a.health)
|
|
mux.HandleFunc("GET /api/search", a.search)
|
|
mux.HandleFunc("GET /api/status", a.status)
|
|
}
|
|
|
|
// codedError carries a stable code that the UI translates.
|
|
type codedError struct {
|
|
code string
|
|
msg string
|
|
detail string
|
|
}
|
|
|
|
func (e *codedError) Error() string {
|
|
if e.detail != "" {
|
|
return e.msg + ": " + e.detail
|
|
}
|
|
return e.msg
|
|
}
|
|
|
|
// GET /api/search?q=&language=&stars=&maxstars=&minforks=&topic=&license=
|
|
// &pushed=&created=&in=&archived=1&forks=1&goodfirst=1&sort=&order=&page=&per_page=
|
|
func (a *API) search(w http.ResponseWriter, r *http.Request) {
|
|
c, err := ParseCriteria(r.URL.Query())
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
res, err := a.gh.Search(r.Context(), c)
|
|
if err != nil {
|
|
status := http.StatusBadGateway
|
|
var ce *codedError
|
|
if errors.As(err, &ce) {
|
|
switch ce.code {
|
|
case "rate_limited":
|
|
status = http.StatusTooManyRequests
|
|
case "bad_query":
|
|
status = http.StatusBadRequest
|
|
}
|
|
}
|
|
writeErr(w, status, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, res)
|
|
}
|
|
|
|
// GET /api/status: token configured and last known search quota (footer).
|
|
func (a *API) status(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"token": a.gh.HasToken(),
|
|
"rate": a.gh.Rate(),
|
|
})
|
|
}
|
|
|
|
func (a *API) health(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("ok"))
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// writeErr returns {"error": "...", "code": "...", "detail": "..."}; the UI
|
|
// translates known codes and falls back to the English message.
|
|
func writeErr(w http.ResponseWriter, status int, err error) {
|
|
body := map[string]string{"error": err.Error()}
|
|
var ce *codedError
|
|
if errors.As(err, &ce) {
|
|
body["code"] = ce.code
|
|
if ce.detail != "" {
|
|
body["detail"] = ce.detail
|
|
}
|
|
}
|
|
writeJSON(w, status, body)
|
|
}
|