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:
42
openapi/codeTypes_test.go
Normal file
42
openapi/codeTypes_test.go
Normal 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
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}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user