// Package widgetapi exposes the bounded widget catalog and preview contract. package widgetapi import ( "encoding/json" "errors" "io" "net/http" "strings" "github.com/itworx/pulse/internal/problem" "github.com/itworx/pulse/internal/widget" "github.com/itworx/pulse/internal/widgetpreview" ) type Handler struct{ Registry widget.Registry } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := r.Context().Err(); err != nil { return } switch { case r.URL.Path == "/api/v1/widgets/catalog" && r.Method == http.MethodGet: write(w, http.StatusOK, map[string]any{"items": h.Registry.Catalog()}) case r.URL.Path == "/api/v1/widgets/preview" && r.Method == http.MethodPost: contentType := strings.ToLower(strings.TrimSpace(strings.Split(r.Header.Get("Content-Type"), ";")[0])) if contentType != "" && contentType != "application/json" { problem.Write(w, r, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE", "Unsupported media type", "Widget previews require application/json.", nil) return } var body struct { Widget map[string]any `json:"widget"` State string `json:"state"` } decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(&body); err != nil || body.Widget == nil { problem.Write(w, r, http.StatusBadRequest, "INVALID_PREVIEW", "Invalid preview", "A bounded widget configuration is required.", nil) return } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { problem.Write(w, r, http.StatusBadRequest, "INVALID_PREVIEW", "Invalid preview", "The request must contain one JSON document.", nil) return } preview, err := widgetpreview.Preview(body.Widget, body.State) if err != nil { var invalid widgetpreview.InvalidConfig if errors.As(err, &invalid) { problem.Write(w, r, http.StatusBadRequest, "INVALID_WIDGET_CONFIG", "Invalid widget", "The widget configuration is invalid.", invalid.Fields) return } problem.Write(w, r, http.StatusBadRequest, "INVALID_WIDGET_CONFIG", "Invalid widget", "The widget configuration is invalid.", nil) return } write(w, http.StatusOK, map[string]any{"preview": preview}) case r.URL.Path == "/api/v1/widgets/catalog" || r.URL.Path == "/api/v1/widgets/preview": problem.Write(w, r, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed", "The requested method is not supported for this widget route.", nil) default: problem.Write(w, r, http.StatusNotFound, "NOT_FOUND", "Not found", "The requested widget route does not exist.", nil) } } func write(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) }