167 lines
4.6 KiB
Go
167 lines
4.6 KiB
Go
package realtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
|
)
|
|
|
|
type Handler struct {
|
|
repository *Repository
|
|
}
|
|
|
|
type OnlineIndexHandler struct {
|
|
repository *Repository
|
|
}
|
|
|
|
type PipelineDebugHandler struct {
|
|
repository *Repository
|
|
}
|
|
|
|
func NewHandler(repository *Repository) *Handler {
|
|
if repository == nil {
|
|
panic("realtime repository must not be nil")
|
|
}
|
|
return &Handler{repository: repository}
|
|
}
|
|
|
|
func NewOnlineIndexHandler(repository *Repository) *OnlineIndexHandler {
|
|
if repository == nil {
|
|
panic("online index repository must not be nil")
|
|
}
|
|
return &OnlineIndexHandler{repository: repository}
|
|
}
|
|
|
|
func NewPipelineDebugHandler(repository *Repository) *PipelineDebugHandler {
|
|
if repository == nil {
|
|
panic("pipeline debug repository must not be nil")
|
|
}
|
|
return &PipelineDebugHandler{repository: repository}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
path := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/realtime/vehicles/"), "/")
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" {
|
|
writeError(w, http.StatusNotFound, "vehicle not found")
|
|
return
|
|
}
|
|
vin := parts[0]
|
|
switch {
|
|
case len(parts) == 1:
|
|
snapshot, err := h.repository.GetMerged(r.Context(), vin)
|
|
writeResult(w, snapshot, err)
|
|
case len(parts) == 2 && parts[1] == "online":
|
|
status, err := h.repository.IsOnline(r.Context(), vin)
|
|
writeResult(w, status, err)
|
|
case len(parts) == 3 && parts[1] == "protocols":
|
|
snapshot, err := h.repository.GetProtocol(r.Context(), vin, envelope.Protocol(strings.ToUpper(parts[2])))
|
|
writeResult(w, snapshot, err)
|
|
case len(parts) == 3 && parts[1] == "realtime-raw":
|
|
payload, err := h.repository.GetRealtimeRaw(r.Context(), vin, envelope.Protocol(strings.ToUpper(parts[2])))
|
|
writeResult(w, payload, err)
|
|
default:
|
|
writeError(w, http.StatusNotFound, "route not found")
|
|
}
|
|
}
|
|
|
|
func (h *OnlineIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if strings.Trim(r.URL.Path, "/") != "api/realtime/online" {
|
|
writeError(w, http.StatusNotFound, "route not found")
|
|
return
|
|
}
|
|
query, err := parseOnlineListQuery(r)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
items, total, err := h.repository.ListOnline(r.Context(), query)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"items": items,
|
|
"total": total,
|
|
"limit": query.Limit,
|
|
"offset": query.Offset,
|
|
})
|
|
}
|
|
|
|
func (h *PipelineDebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
if strings.Trim(r.URL.Path, "/") != "api/debug/pipeline" {
|
|
writeError(w, http.StatusNotFound, "route not found")
|
|
return
|
|
}
|
|
summary, err := h.repository.PipelineSummary(r.Context())
|
|
writeResult(w, summary, err)
|
|
}
|
|
|
|
func parseOnlineListQuery(r *http.Request) (OnlineListQuery, error) {
|
|
values := r.URL.Query()
|
|
limit, err := parseBoundedInt(values.Get("limit"), 100, 1, 1000, "limit")
|
|
if err != nil {
|
|
return OnlineListQuery{}, err
|
|
}
|
|
offset, err := parseBoundedInt(values.Get("offset"), 0, 0, 1_000_000, "offset")
|
|
if err != nil {
|
|
return OnlineListQuery{}, err
|
|
}
|
|
return normalizeOnlineListQuery(OnlineListQuery{
|
|
Protocol: envelope.Protocol(values.Get("protocol")),
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}), nil
|
|
}
|
|
|
|
func parseBoundedInt(raw string, fallback int, min int, max int, name string) (int, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback, nil
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil || value < min || value > max {
|
|
return 0, fmt.Errorf("%s must be between %d and %d", name, min, max)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func writeResult(w http.ResponseWriter, value any, err error) {
|
|
if err != nil {
|
|
if errors.Is(err, redis.Nil) {
|
|
writeError(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": message})
|
|
}
|