Serveur Go : recherche GitHub par critères
- /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>
This commit is contained in:
1 parent
04379490f1
commit
1885488d05
9 files changed
+472
No files matched your search
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user