package api import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" ) const featuresKey = "meta:features" // FeatureHandler manages lightweight feature flags stored in Redis for DP/CP runtime toggles. // Values are stored as plain strings in a Redis hash. type FeatureHandler struct { rdb *redis.Client } func NewFeatureHandler(rdb *redis.Client) *FeatureHandler { return &FeatureHandler{rdb: rdb} } // ListFeatures godoc // @Summary List feature flags // @Description Returns all feature flags stored in Redis (meta:features) // @Tags admin // @Produce json // @Security AdminAuth // @Success 200 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/features [get] func (h *FeatureHandler) ListFeatures(c *gin.Context) { if h.rdb == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "redis not configured"}) return } m, err := h.rdb.HGetAll(c.Request.Context(), featuresKey).Result() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read features", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"features": m}) } type UpdateFeaturesRequest map[string]any // UpdateFeatures godoc // @Summary Update feature flags // @Description Updates selected feature flags (meta:features). Values are stored as strings. // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param request body object true "Feature map" // @Success 200 {object} gin.H // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/features [put] func (h *FeatureHandler) UpdateFeatures(c *gin.Context) { if h.rdb == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "redis not configured"}) return } var req UpdateFeaturesRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updates := make(map[string]string, len(req)) for k, v := range req { key := strings.TrimSpace(k) if key == "" { continue } switch vv := v.(type) { case string: updates[key] = strings.TrimSpace(vv) case bool: if vv { updates[key] = "true" } else { updates[key] = "false" } case float64: // JSON numbers decode as float64; keep as integer-looking string when possible. updates[key] = fmt.Sprintf("%v", vv) default: updates[key] = fmt.Sprintf("%v", vv) } } if len(updates) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no valid feature updates"}) return } if err := h.rdb.HSet(c.Request.Context(), featuresKey, updates).Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update features", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"updated": updates}) }