package response import ( "encoding/json" "fmt" "strings" ) // Slot returns the name of a whole-value placeholder, or empty for constants. // Slots preserve the value's JSON type; they are not string interpolation. func Slot(value any) string { s, ok := value.(string) if ok && strings.HasPrefix(s, "${") && strings.HasSuffix(s, "}") { return s[2 : len(s)-1] } return "" } func validateTemplate(value any, depth int) error { if depth > 24 { return fmt.Errorf("response: body nesting exceeds 24 levels") } switch v := value.(type) { case map[string]any: for key, item := range v { if strings.TrimSpace(key) == "" { return fmt.Errorf("response: body keys must not be empty") } if err := validateTemplate(item, depth+1); err != nil { return err } } case []any: for _, item := range v { if err := validateTemplate(item, depth+1); err != nil { return err } } case string: if strings.HasPrefix(v, "${") { switch Slot(v) { case "code", "message", "data", "requestId", "ok", "status": default: return fmt.Errorf("response: unknown body slot %q", v) } } case nil, bool, json.Number, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: if _, err := json.Marshal(v); err != nil { return fmt.Errorf("response: invalid body constant: %w", err) } default: return fmt.Errorf("response: body accepts JSON values, got %T", value) } return nil } func cloneTemplate(value any, depth int) any { // Invalid cyclic Go configuration must still reach validation, not overflow. if depth > 24 { return value } switch v := value.(type) { case map[string]any: copy := make(map[string]any, len(v)) for key, item := range v { copy[key] = cloneTemplate(item, depth+1) } return copy case []any: copy := make([]any, len(v)) for i, item := range v { copy[i] = cloneTemplate(item, depth+1) } return copy default: return value } } func renderTemplate(value any, slots map[string]any, dataMode string) any { if name := Slot(value); name != "" { return slots[name] } switch v := value.(type) { case map[string]any: result := make(map[string]any, len(v)) for key, item := range v { slot := Slot(item) if slot == "requestId" && slots[slot] == "" || slot == "data" && slots[slot] == nil && dataMode != "always" { continue } result[key] = renderTemplate(item, slots, dataMode) } return result case []any: result := make([]any, len(v)) for i, item := range v { result[i] = renderTemplate(item, slots, dataMode) } return result case string: if strings.HasPrefix(v, "$${") { return v[1:] } return v default: return value } }