Files
go-omni/openapi/envelope.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

115 lines
3.1 KiB
Go

package openapi
import (
"encoding/json"
"git.yuntuo.hk/k2safe/go-omni/response"
"sort"
"strings"
)
// EnvelopeWith describes the same configurable envelope used at runtime. data
// describes the business value; it is never embedded under a fixed field name.
// A nil data schema describes an error response and selects ErrorBody if set.
func EnvelopeWith(data *Schema, cfg response.Config) (*Schema, error) {
f, err := response.New(cfg)
if err != nil {
return nil, err
}
c := f.Config()
body := c.Body
if data == nil && c.ErrorBody != nil {
body = c.ErrorBody
}
if body != nil {
return templateSchema(body, data, c), nil
}
s := &Schema{Type: "object", Properties: map[string]*Schema{}}
if c.CodeField != "-" {
s.Properties[c.CodeField] = codeSchema(c.CodeType)
s.Required = append(s.Required, c.CodeField)
}
if c.MessageField != "-" {
s.Properties[c.MessageField] = &Schema{Type: "string"}
s.Required = append(s.Required, c.MessageField)
}
if c.RequestIDField != "-" {
s.Properties[c.RequestIDField] = &Schema{Type: "string"}
}
if c.DataField != "-" && (data != nil || c.DataMode == "always") {
if data == nil {
data = &Schema{Type: "object", Nullable: true, Enum: []any{nil}}
}
s.Properties[c.DataField] = data
s.Required = append(s.Required, c.DataField)
}
return s, nil
}
func templateSchema(value any, data *Schema, cfg response.Config) *Schema {
switch response.Slot(value) {
case "code":
return codeSchema(cfg.CodeType)
case "message", "requestId":
return &Schema{Type: "string"}
case "ok":
return &Schema{Type: "boolean"}
case "status":
return &Schema{Type: "integer"}
case "data":
if data != nil {
return data
}
return &Schema{Type: "object", Nullable: true, Enum: []any{nil}}
}
switch v := value.(type) {
case map[string]any:
s := &Schema{Type: "object", Properties: map[string]*Schema{}}
keys := make([]string, 0, len(v))
for key := range v {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
item := v[key]
if response.Slot(item) == "data" && data == nil && cfg.DataMode != "always" {
continue
}
s.Properties[key] = templateSchema(item, data, cfg)
if response.Slot(item) != "requestId" {
s.Required = append(s.Required, key)
}
}
return s
case []any:
items := &Schema{}
for _, item := range v {
items.AnyOf = append(items.AnyOf, templateSchema(item, data, cfg))
}
size := len(v)
return &Schema{Type: "array", Items: items, MinItems: &size, MaxItems: &size}
case string:
if strings.HasPrefix(v, "$${") {
v = v[1:]
}
return &Schema{Type: "string", Enum: []any{v}}
case bool:
return &Schema{Type: "boolean", Enum: []any{v}}
case nil:
return &Schema{Type: "object", Nullable: true, Enum: []any{nil}}
default:
typeName := "number"
encoded, _ := json.Marshal(v)
if _, err := json.Number(string(encoded)).Int64(); err == nil {
typeName = "integer"
}
return &Schema{Type: typeName, Enum: []any{v}}
}
}
func codeSchema(kind string) *Schema {
if kind == "int32" || kind == "int64" {
return &Schema{Type: "integer", Format: kind}
}
return &Schema{Type: kind}
}