mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
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
107 lines
2.9 KiB
Go
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})
|
|
}
|