This commit is contained in:
Martino Ferrari
2026-04-25 22:56:09 +02:00
parent 8b548ba1c2
commit 986f6cd6d8
85 changed files with 11479 additions and 5050 deletions
+137 -12
View File
@@ -3,22 +3,28 @@ package api
import (
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"github.com/uopi/uopi/internal/broker"
"github.com/uopi/uopi/internal/datasource"
"github.com/uopi/uopi/internal/datasource/synthetic"
"github.com/uopi/uopi/internal/storage"
)
// Handler holds the dependencies shared across all API endpoints.
type Handler struct {
broker *broker.Broker
log *slog.Logger
broker *broker.Broker
synthetic *synthetic.Synthetic // nil if not enabled
store *storage.Store
log *slog.Logger
}
// New creates an API Handler.
func New(b *broker.Broker, log *slog.Logger) *Handler {
return &Handler{broker: b, log: log}
// New creates an API Handler. synth may be nil if the synthetic DS is disabled.
func New(b *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, log *slog.Logger) *Handler {
return &Handler{broker: b, synthetic: synth, store: store, log: log}
}
// Register mounts all API routes onto mux under the given prefix.
@@ -32,6 +38,10 @@ func (h *Handler) Register(mux *http.ServeMux, prefix string) {
mux.HandleFunc("PUT "+prefix+"/interfaces/{id}", h.updateInterface)
mux.HandleFunc("DELETE "+prefix+"/interfaces/{id}", h.deleteInterface)
mux.HandleFunc("POST "+prefix+"/interfaces/{id}/clone", h.cloneInterface)
// Synthetic signal CRUD
mux.HandleFunc("GET "+prefix+"/synthetic", h.listSynthetic)
mux.HandleFunc("POST "+prefix+"/synthetic", h.createSynthetic)
mux.HandleFunc("DELETE "+prefix+"/synthetic/{name}", h.deleteSynthetic)
}
// ── /datasources ──────────────────────────────────────────────────────────────
@@ -91,30 +101,145 @@ func (h *Handler) listSignals(w http.ResponseWriter, r *http.Request) {
}
// ── /interfaces ───────────────────────────────────────────────────────────────
// Stub implementations — full persistence lands in Phase 6.
func (h *Handler) listInterfaces(w http.ResponseWriter, r *http.Request) {
jsonOK(w, []any{})
list, err := h.store.List()
if err != nil {
h.log.Error("list interfaces", "err", err)
jsonError(w, http.StatusInternalServerError, err.Error())
return
}
jsonOK(w, list)
}
func (h *Handler) createInterface(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
if err != nil {
jsonError(w, http.StatusBadRequest, "read body: "+err.Error())
return
}
id, err := h.store.Create(body)
if err != nil {
h.log.Error("create interface", "err", err)
jsonError(w, http.StatusBadRequest, err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]string{"id": id})
}
func (h *Handler) getInterface(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusNotFound, "interface not found")
id := r.PathValue("id")
data, err := h.store.Get(id)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
jsonError(w, http.StatusNotFound, "interface not found: "+id)
} else {
jsonError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write(data)
}
func (h *Handler) updateInterface(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
id := r.PathValue("id")
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
if err != nil {
jsonError(w, http.StatusBadRequest, "read body: "+err.Error())
return
}
if err := h.store.Update(id, body); err != nil {
if errors.Is(err, storage.ErrNotFound) {
jsonError(w, http.StatusNotFound, "interface not found: "+id)
} else {
h.log.Error("update interface", "id", id, "err", err)
jsonError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) deleteInterface(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
id := r.PathValue("id")
if err := h.store.Delete(id); err != nil {
if errors.Is(err, storage.ErrNotFound) {
jsonError(w, http.StatusNotFound, "interface not found: "+id)
} else {
jsonError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) cloneInterface(w http.ResponseWriter, r *http.Request) {
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
id := r.PathValue("id")
newID, err := h.store.Clone(id)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
jsonError(w, http.StatusNotFound, "interface not found: "+id)
} else {
h.log.Error("clone interface", "id", id, "err", err)
jsonError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]string{"id": newID})
}
// ── /synthetic ────────────────────────────────────────────────────────────────
func (h *Handler) listSynthetic(w http.ResponseWriter, _ *http.Request) {
if h.synthetic == nil {
jsonOK(w, []any{})
return
}
jsonOK(w, h.synthetic.GetDefs())
}
func (h *Handler) createSynthetic(w http.ResponseWriter, r *http.Request) {
if h.synthetic == nil {
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
return
}
var def synthetic.SignalDef
if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
return
}
if err := h.synthetic.AddSignal(def); err != nil {
if errors.Is(err, datasource.ErrNotFound) {
jsonError(w, http.StatusConflict, err.Error())
} else {
jsonError(w, http.StatusBadRequest, err.Error())
}
return
}
w.WriteHeader(http.StatusCreated)
jsonOK(w, def)
}
func (h *Handler) deleteSynthetic(w http.ResponseWriter, r *http.Request) {
if h.synthetic == nil {
jsonError(w, http.StatusServiceUnavailable, "synthetic data source not enabled")
return
}
name := r.PathValue("name")
if err := h.synthetic.RemoveSignal(name); err != nil {
if errors.Is(err, datasource.ErrNotFound) {
jsonError(w, http.StatusNotFound, "synthetic signal not found: "+name)
} else {
jsonError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.WriteHeader(http.StatusNoContent)
}
// ── Helpers ───────────────────────────────────────────────────────────────────