Files
go-omni/response/template.go
k2safe 7670b222ac feat: release configurable API response contracts
Define reusable response bodies in .api with typed code mappings, per-route overrides and matching OpenAPI schemas. Add application and callback encoders, early response policy selection, and isolated remote-consumer release verification.

Release v0.8.0-preview after full race, vet, build and local MySQL workflow acceptance.
2026-09-23 20:42:38 +08:00

110 lines
2.6 KiB
Go

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