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:
85
httpx/response.go
Normal file
85
httpx/response.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
|
||||
// SetResponseEncoder overrides this request's success AND error encoding. A
|
||||
// cloned context inherits it, including buffered/idempotent requests.
|
||||
func (c *Context) SetResponseEncoder(encoder response.Encoder) { c.encoder = encoder }
|
||||
|
||||
// WithResponse applies a policy to one route/group. It deliberately stays set
|
||||
// until the outer error boundary finishes handling this request.
|
||||
func WithResponse(encoder response.Encoder) Middleware {
|
||||
return func(next Handler) Handler {
|
||||
return func(c *Context) error { c.SetResponseEncoder(encoder); return next(c) }
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseRegistrar installs the response policy before application middleware,
|
||||
// so early auth/protection failures and buffered clones use the route contract.
|
||||
type ResponseRegistrar interface {
|
||||
HandleWithResponse(string, string, response.Encoder, Handler, ...Middleware)
|
||||
}
|
||||
|
||||
// HandleWithResponse uses the registrar's early policy hook when available.
|
||||
// Custom registrars without this hook get an ordinary leading route middleware.
|
||||
func HandleWithResponse(router interface {
|
||||
Handle(string, string, Handler, ...Middleware)
|
||||
}, method, path string, encoder response.Encoder, handler Handler, middleware ...Middleware) {
|
||||
if r, ok := router.(ResponseRegistrar); ok {
|
||||
r.HandleWithResponse(method, path, encoder, handler, middleware...)
|
||||
return
|
||||
}
|
||||
all := append([]Middleware{WithResponse(encoder)}, middleware...)
|
||||
router.Handle(method, path, handler, all...)
|
||||
}
|
||||
|
||||
func (c *Context) writeResult(result response.Result) (err error) {
|
||||
if c.responded {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = apierrors.Internal(fmt.Errorf("response encoder panic: %v", recovered))
|
||||
}
|
||||
}()
|
||||
encoder := c.encoder
|
||||
if encoder == nil {
|
||||
encoder = response.Default()
|
||||
}
|
||||
payload, err := encoder.Encode(result)
|
||||
if err != nil {
|
||||
return apierrors.Internal(err)
|
||||
}
|
||||
if payload.Status < 200 || payload.Status > 599 {
|
||||
return apierrors.Internal(fmt.Errorf("response encoder returned invalid status %d", payload.Status))
|
||||
}
|
||||
if (payload.Status == 204 || payload.Status == 304) && len(payload.Body) > 0 {
|
||||
return apierrors.Internal(fmt.Errorf("response encoder returned a body for status %d", payload.Status))
|
||||
}
|
||||
return c.WriteResponse(payload.Status, payload.Header, payload.Body)
|
||||
}
|
||||
|
||||
// Bytes, Text and XML are explicit wire responses; they bypass any envelope.
|
||||
func (c *Context) Bytes(status int, contentType string, body []byte) error {
|
||||
return c.WriteResponse(status, http.Header{"Content-Type": {contentType}}, body)
|
||||
}
|
||||
func (c *Context) Text(status int, body string) error {
|
||||
return c.Bytes(status, "text/plain; charset=utf-8", []byte(body))
|
||||
}
|
||||
func (c *Context) XML(status int, value any) error {
|
||||
if c.responded {
|
||||
return nil
|
||||
}
|
||||
body, err := xml.Marshal(value)
|
||||
if err != nil {
|
||||
return apierrors.Internal(err)
|
||||
}
|
||||
return c.Bytes(status, "application/xml; charset=utf-8", body)
|
||||
}
|
||||
Reference in New Issue
Block a user