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:
25
app/app.go
25
app/app.go
@@ -15,6 +15,7 @@ import (
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/middleware"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,7 @@ type App struct {
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
clientIPResolver *httpx.ClientIPResolver
|
||||
responseEncoder response.Encoder
|
||||
}
|
||||
|
||||
// Runner is the small lifecycle boundary used by an HTTP server
|
||||
@@ -64,6 +66,15 @@ func configure(options ...config.Option) config.Config {
|
||||
}
|
||||
|
||||
func newApp(router httpx.Router, settings config.Config) *App {
|
||||
settings.Response = settings.Response.Clone()
|
||||
encoder, responseErr := response.New(settings.Response)
|
||||
if responseErr != nil {
|
||||
panic(responseErr)
|
||||
}
|
||||
var responseEncoder response.Encoder = encoder
|
||||
if settings.ResponseEncoder != nil {
|
||||
responseEncoder = settings.ResponseEncoder
|
||||
}
|
||||
settings.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
||||
settings.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...)
|
||||
resolver, err := httpx.NewClientIPResolver(settings.TrustedProxies...)
|
||||
@@ -75,7 +86,16 @@ func newApp(router httpx.Router, settings config.Config) *App {
|
||||
middlewares = append(middlewares, middleware.AccessLog(middleware.WithSlowRequestThreshold(settings.SlowRequestThreshold)))
|
||||
}
|
||||
middlewares = append(middlewares, middleware.Recovery())
|
||||
return &App{router: router, config: settings, middlewares: middlewares, clientIPResolver: resolver}
|
||||
application := &App{router: router, config: settings, middlewares: middlewares, clientIPResolver: resolver, responseEncoder: responseEncoder}
|
||||
if fallback, ok := router.(httpx.FallbackRouter); ok {
|
||||
adaptError := func(status int, code, message string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
application.adapt(func(*httpx.Context) error { return apierrors.New(status, code, message) }).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
fallback.SetFallbacks(adaptError(404, "NOT_FOUND", "route not found"), adaptError(405, "METHOD_NOT_ALLOWED", "method not allowed"))
|
||||
}
|
||||
return application
|
||||
}
|
||||
|
||||
// Router returns the transport contract used by this application.
|
||||
@@ -93,6 +113,7 @@ func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
// Config returns the effective application configuration.
|
||||
func (a *App) Config() config.Config {
|
||||
settings := a.config
|
||||
settings.Response = settings.Response.Clone()
|
||||
settings.TrustedProxies = append([]string(nil), settings.TrustedProxies...)
|
||||
settings.Log.RedactKeys = append([]string(nil), settings.Log.RedactKeys...)
|
||||
return settings
|
||||
@@ -235,5 +256,5 @@ func (a *App) writeError(ctx *httpx.Context, err error) {
|
||||
if err == nil || ctx.Responded() {
|
||||
return
|
||||
}
|
||||
ctx.WriteError(apierrors.From(err))
|
||||
ctx.WriteError(err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
|
||||
func (a *App) handle(routes httpx.RouterGroup, method, path string, handler httpx.Handler, middleware []httpx.Middleware) {
|
||||
@@ -11,6 +12,10 @@ func (a *App) handle(routes httpx.RouterGroup, method, path string, handler http
|
||||
}
|
||||
|
||||
func (a *App) adapt(handler httpx.Handler, middleware ...httpx.Middleware) http.Handler {
|
||||
return a.adaptResponse(a.responseEncoder, handler, middleware...)
|
||||
}
|
||||
|
||||
func (a *App) adaptResponse(encoder response.Encoder, handler httpx.Handler, middleware ...httpx.Middleware) http.Handler {
|
||||
wrapped := handler
|
||||
all := make([]httpx.Middleware, 0, len(a.middlewares)+len(middleware))
|
||||
all = append(all, a.middlewares...)
|
||||
@@ -25,6 +30,7 @@ func (a *App) adapt(handler httpx.Handler, middleware ...httpx.Middleware) http.
|
||||
request = request.WithContext(httpx.WithClientIPResolver(request.Context(), a.clientIPResolver))
|
||||
}
|
||||
requestContext := httpx.NewContext(request, writer, httpx.RouteParams(request.Context()))
|
||||
requestContext.SetResponseEncoder(encoder)
|
||||
if err := wrapped(requestContext); err != nil {
|
||||
a.writeError(requestContext, err)
|
||||
}
|
||||
@@ -37,6 +43,14 @@ func (a *App) Handle(method, path string, handler httpx.Handler, middleware ...h
|
||||
a.handle(a.router, method, path, handler, middleware)
|
||||
}
|
||||
|
||||
// HandleWithResponse selects the route policy before all application middleware.
|
||||
func (a *App) HandleWithResponse(method, path string, encoder response.Encoder, handler httpx.Handler, middleware ...httpx.Middleware) {
|
||||
if encoder == nil {
|
||||
encoder = a.responseEncoder
|
||||
}
|
||||
a.router.Handle(method, path, a.adaptResponse(encoder, handler, middleware...))
|
||||
}
|
||||
|
||||
func (a *App) GET(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
|
||||
a.handle(a.router, http.MethodGet, path, handler, middleware)
|
||||
}
|
||||
@@ -94,6 +108,15 @@ func (g *RouterGroup) Handle(method, path string, handler httpx.Handler, middlew
|
||||
g.route(method, path, handler, middleware)
|
||||
}
|
||||
|
||||
func (g *RouterGroup) HandleWithResponse(method, path string, encoder response.Encoder, handler httpx.Handler, middleware ...httpx.Middleware) {
|
||||
if encoder == nil {
|
||||
encoder = g.app.responseEncoder
|
||||
}
|
||||
combined := append([]httpx.Middleware{}, g.middlewares...)
|
||||
combined = append(combined, middleware...)
|
||||
g.group.Handle(method, path, g.app.adaptResponse(encoder, handler, combined...))
|
||||
}
|
||||
|
||||
func (g *RouterGroup) GET(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
|
||||
g.route(http.MethodGet, path, handler, middleware)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user