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.
This commit is contained in:
114
openapi/envelope.go
Normal file
114
openapi/envelope.go
Normal file
@@ -0,0 +1,114 @@
|
||||
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}
|
||||
}
|
||||
Reference in New Issue
Block a user