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:
@@ -1,11 +1,15 @@
|
||||
package apigen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
|
||||
var errorCode = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`)
|
||||
@@ -16,6 +20,9 @@ var credentialName = regexp.MustCompile("^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
||||
func prepareMetadata(original *Spec) (*Spec, error) {
|
||||
spec := *original
|
||||
spec.Routes = append([]Route(nil), original.Routes...)
|
||||
if err := validateResponses(&spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitions := map[string]ErrorDefinition{}
|
||||
codes, symbols := map[string]bool{}, map[string]bool{}
|
||||
for _, e := range spec.Errors {
|
||||
@@ -124,6 +131,8 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
doc["x-omni-errors"] = spec.Errors
|
||||
}
|
||||
for _, r := range spec.Routes {
|
||||
policy := response.MustNew(responseConfig(spec, r))
|
||||
cfg := policy.Config()
|
||||
parts := strings.Split(r.Path, "/")
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
@@ -131,6 +140,9 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
}
|
||||
}
|
||||
op := doc["paths"].(map[string]any)[strings.Join(parts, "/")].(map[string]any)[r.Method].(map[string]any)
|
||||
if r.ResponseFormat != "" {
|
||||
op["x-omni-response"] = r.ResponseFormat
|
||||
}
|
||||
if len(r.Middleware) > 0 {
|
||||
op["x-omni-middleware"] = r.Middleware
|
||||
}
|
||||
@@ -152,7 +164,7 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
}
|
||||
responses := op["responses"].(map[string]any)
|
||||
if r.Auth != "" && r.Auth != "none" {
|
||||
responses["401"] = genericErrorResponse("Authentication required")
|
||||
responses["401"] = genericErrorResponse("Authentication required", cfg)
|
||||
}
|
||||
grouped := map[int][]ErrorDefinition{}
|
||||
for _, name := range r.Errors {
|
||||
@@ -166,32 +178,43 @@ func decorateDocument(doc map[string]any, spec *Spec) {
|
||||
key := fmt.Sprint(status)
|
||||
response, exists := responses[key].(map[string]any)
|
||||
if !exists {
|
||||
response = genericErrorResponse(http.StatusText(status))
|
||||
response = genericErrorResponse(http.StatusText(status), cfg)
|
||||
responses[key] = response
|
||||
}
|
||||
media := response["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
examples := map[string]any{}
|
||||
codes := []any{}
|
||||
seenCodes := map[string]bool{}
|
||||
description := []string{}
|
||||
sort.Slice(errs, func(i, j int) bool { return errs[i].Name < errs[j].Name })
|
||||
for _, e := range errs {
|
||||
examples[e.Name] = map[string]any{"summary": e.Message, "value": map[string]any{"code": e.Code, "message": e.Message, "requestId": "exampleRequest"}}
|
||||
codes = append(codes, e.Code)
|
||||
value := policy.Value(responseResult(e))
|
||||
examples[e.Name] = map[string]any{"summary": e.Message, "value": value}
|
||||
codeKey := fmt.Sprint(value[cfg.CodeField])
|
||||
if !seenCodes[codeKey] {
|
||||
codes = append(codes, value[cfg.CodeField])
|
||||
seenCodes[codeKey] = true
|
||||
}
|
||||
description = append(description, e.Code+": "+e.Message)
|
||||
}
|
||||
media["examples"] = examples
|
||||
if !exists {
|
||||
media["schema"].(map[string]any)["properties"].(map[string]any)["code"].(map[string]any)["enum"] = codes
|
||||
if !exists && cfg.Body == nil && cfg.CodeField != "-" {
|
||||
media["schema"].(map[string]any)["properties"].(map[string]any)[cfg.CodeField].(map[string]any)["enum"] = codes
|
||||
}
|
||||
response["description"] = strings.Join(description, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
func genericErrorResponse(description string) map[string]any {
|
||||
func responseResult(e ErrorDefinition) response.Result {
|
||||
return response.Result{Status: e.Status, Code: e.Code, Message: e.Message, RequestID: "exampleRequest"}
|
||||
}
|
||||
func genericErrorResponse(description string, cfg response.Config) map[string]any {
|
||||
if description == "" {
|
||||
description = "Error"
|
||||
}
|
||||
return map[string]any{"description": description, "content": map[string]any{"application/json": map[string]any{"schema": map[string]any{
|
||||
"type": "object", "required": []string{"code", "message"}, "properties": map[string]any{"code": map[string]any{"type": "string"}, "message": map[string]any{"type": "string"}, "requestId": map[string]any{"type": "string"}},
|
||||
}}}}
|
||||
schema, _ := openapi.EnvelopeWith(nil, cfg)
|
||||
data, _ := json.Marshal(schema)
|
||||
var value map[string]any
|
||||
_ = json.Unmarshal(data, &value)
|
||||
return map[string]any{"description": description, "content": map[string]any{"application/json": map[string]any{"schema": value}}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user