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:
2026-09-23 20:42:38 +08:00
parent bef7ea2641
commit 7670b222ac
47 changed files with 2689 additions and 120 deletions

42
openapi/codeTypes_test.go Normal file
View File

@@ -0,0 +1,42 @@
package openapi_test
import (
"testing"
"git.yuntuo.hk/k2safe/go-omni/openapi"
"git.yuntuo.hk/k2safe/go-omni/response"
)
func TestResponseCodeSchemaUsesGoWidths(t *testing.T) {
for _, kind := range []string{"string", "int32", "int64", "integer"} {
cfg := response.Config{CodeType: kind}
wantType, wantFormat := "string", ""
if kind != "string" {
cfg.SuccessCode = 200
cfg.ErrorCode = 9999
wantType = "integer"
}
if kind == "int32" || kind == "int64" {
wantFormat = kind
}
for _, nested := range []bool{false, true} {
if nested {
cfg.Body = map[string]any{"result": map[string]any{"code": "${code}"}}
cfg.ErrorBody = cfg.Body
}
for _, data := range []*openapi.Schema{nil, {Type: "object"}} {
schema, err := openapi.EnvelopeWith(data, cfg)
if err != nil {
t.Fatal(err)
}
if nested {
schema = schema.Properties["result"]
}
code := schema.Properties["code"]
if code.Type != wantType || code.Format != wantFormat {
t.Fatalf("%s: %+v", kind, code)
}
}
}
}
}

114
openapi/envelope.go Normal file
View 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}
}

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/response"
"net/http"
"reflect"
"regexp"
@@ -36,6 +37,7 @@ type Schema struct {
Enum []any `json:"enum,omitempty"`
Default any `json:"default,omitempty"`
Not *Schema `json:"not,omitempty"`
AnyOf []*Schema `json:"anyOf,omitempty"`
}
type Parameter struct {
Name string `json:"name"`
@@ -181,7 +183,8 @@ func (a *API) Handler() httpx.Handler {
}
}
func Envelope(data *Schema) *Schema {
return &Schema{Type: "object", Required: []string{"code", "message"}, Properties: map[string]*Schema{"code": {Type: "string"}, "message": {Type: "string"}, "requestId": {Type: "string"}, "data": data}}
schema, _ := EnvelopeWith(data, response.Config{})
return schema
}
func SchemaOf[T any]() (*Schema, error) {
return schema(reflect.TypeOf((*T)(nil)).Elem(), map[reflect.Type]bool{}, 0)