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:
claude BotandClaude Opus 5.5 committed 2026-10-01 12:46:42 +02:00
1 parent 04379490f1
commit 1885488d05
9 files changed
+472

No files matched your search

+89
View File
@@ -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)
}