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.
110 lines
2.6 KiB
Go
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
|
|
}
|
|
}
|