Files
ez-api/internal/api/feature_handler.go
zenfun 11f6e81798 feat(api): add feature flag management endpoints
Add FeatureHandler to manage lightweight runtime configuration toggles
stored in the Redis `meta:features` hash. This enables dynamic control
over system behavior (e.g., storage backends) via the admin API.

- Add `GET /admin/features` to list flags
- Add `PUT /admin/features` to update flags
- Update README with feature flag documentation
2025-12-15 15:01:01 +08:00

107 lines
2.9 KiB
Go

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})
}