Phase 2/4 done, working on phase 3/5
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// Package api implements the REST API handlers for uopi.
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/uopi/uopi/internal/broker"
|
||||
"github.com/uopi/uopi/internal/datasource"
|
||||
)
|
||||
|
||||
// Handler holds the dependencies shared across all API endpoints.
|
||||
type Handler struct {
|
||||
broker *broker.Broker
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New creates an API Handler.
|
||||
func New(b *broker.Broker, log *slog.Logger) *Handler {
|
||||
return &Handler{broker: b, log: log}
|
||||
}
|
||||
|
||||
// Register mounts all API routes onto mux under the given prefix.
|
||||
// Requires Go 1.22+ for method+path routing.
|
||||
func (h *Handler) Register(mux *http.ServeMux, prefix string) {
|
||||
mux.HandleFunc("GET "+prefix+"/datasources", h.listDataSources)
|
||||
mux.HandleFunc("GET "+prefix+"/signals", h.listSignals)
|
||||
mux.HandleFunc("GET "+prefix+"/interfaces", h.listInterfaces)
|
||||
mux.HandleFunc("POST "+prefix+"/interfaces", h.createInterface)
|
||||
mux.HandleFunc("GET "+prefix+"/interfaces/{id}", h.getInterface)
|
||||
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)
|
||||
}
|
||||
|
||||
// ── /datasources ──────────────────────────────────────────────────────────────
|
||||
|
||||
type dsInfo struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (h *Handler) listDataSources(w http.ResponseWriter, r *http.Request) {
|
||||
sources := h.broker.DataSources()
|
||||
out := make([]dsInfo, len(sources))
|
||||
for i, ds := range sources {
|
||||
out[i] = dsInfo{Name: ds.Name(), Status: "connected"}
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
// ── /signals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type signalInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DisplayLow float64 `json:"displayLow"`
|
||||
DisplayHigh float64 `json:"displayHigh"`
|
||||
Writable bool `json:"writable"`
|
||||
EnumStrings []string `json:"enumStrings,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) listSignals(w http.ResponseWriter, r *http.Request) {
|
||||
dsName := r.URL.Query().Get("ds")
|
||||
if dsName == "" {
|
||||
jsonError(w, http.StatusBadRequest, "query parameter 'ds' is required")
|
||||
return
|
||||
}
|
||||
|
||||
ds, ok := h.broker.Source(dsName)
|
||||
if !ok {
|
||||
jsonError(w, http.StatusNotFound, "unknown data source: "+dsName)
|
||||
return
|
||||
}
|
||||
|
||||
metas, err := ds.ListSignals(r.Context())
|
||||
if err != nil {
|
||||
h.log.Error("list signals", "ds", dsName, "err", err)
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]signalInfo, len(metas))
|
||||
for i, m := range metas {
|
||||
out[i] = metaToSignalInfo(m)
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
// ── /interfaces ───────────────────────────────────────────────────────────────
|
||||
// Stub implementations — full persistence lands in Phase 6.
|
||||
|
||||
func (h *Handler) listInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, []any{})
|
||||
}
|
||||
|
||||
func (h *Handler) createInterface(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
|
||||
}
|
||||
|
||||
func (h *Handler) getInterface(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusNotFound, "interface not found")
|
||||
}
|
||||
|
||||
func (h *Handler) updateInterface(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
|
||||
}
|
||||
|
||||
func (h *Handler) deleteInterface(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
|
||||
}
|
||||
|
||||
func (h *Handler) cloneInterface(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusNotImplemented, "interface storage not yet implemented")
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func metaToSignalInfo(m datasource.Metadata) signalInfo {
|
||||
return signalInfo{
|
||||
Name: m.Name,
|
||||
Type: dataTypeName(m.Type),
|
||||
Unit: m.Unit,
|
||||
Description: m.Description,
|
||||
DisplayLow: m.DisplayLow,
|
||||
DisplayHigh: m.DisplayHigh,
|
||||
Writable: m.Writable,
|
||||
EnumStrings: m.EnumStrings,
|
||||
}
|
||||
}
|
||||
|
||||
func dataTypeName(t datasource.DataType) string {
|
||||
switch t {
|
||||
case datasource.TypeFloat64:
|
||||
return "float64"
|
||||
case datasource.TypeFloat64Array:
|
||||
return "float64[]"
|
||||
case datasource.TypeString:
|
||||
return "string"
|
||||
case datasource.TypeInt64:
|
||||
return "int64"
|
||||
case datasource.TypeBool:
|
||||
return "bool"
|
||||
case datasource.TypeEnum:
|
||||
return "enum"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func jsonOK(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func jsonError(w http.ResponseWriter, code int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
Reference in New Issue
Block a user