feat: bootstrap reusable GoOmni framework

This commit is contained in:
2026-09-20 22:06:24 +08:00
commit 8b19aa79ee
60 changed files with 4923 additions and 0 deletions

227
httpx/context.go Normal file
View File

@@ -0,0 +1,227 @@
package httpx
import (
"context"
"encoding/json"
"net/http"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/logx"
"git.yuntuo.hk/k2safe/go-omni/response"
"git.yuntuo.hk/k2safe/go-omni/validate"
)
// Handler is the function signature used by GoOmni routes.
type Handler func(*Context) error
// Middleware wraps a Handler and may enforce cross-cutting behavior.
type Middleware func(Handler) Handler
// Router is the small contract the application needs from a network layer.
// The default implementation is backed by Gin, but another router or a
// future GoOmni transport can implement this interface without changing
// business handlers.
type Router interface {
http.Handler
Handle(method, path string, handler http.Handler)
Any(path string, handler http.Handler)
Group(path string) RouterGroup
}
// RouterGroup is the route-registration portion of Router.
type RouterGroup interface {
Handle(method, path string, handler http.Handler)
Any(path string, handler http.Handler)
Group(path string) RouterGroup
}
type requestContextKey uint8
const (
routeParamsKey requestContextKey = iota + 1
routePatternKey
)
// WithRouteParams lets a router adapter expose path parameters to the
// transport-neutral Context.
func WithRouteParams(ctx context.Context, params map[string]string) context.Context {
copyOfParams := make(map[string]string, len(params))
for key, value := range params {
copyOfParams[key] = value
}
return context.WithValue(ctx, routeParamsKey, copyOfParams)
}
// RouteParams returns path parameters installed by a router adapter.
func RouteParams(ctx context.Context) map[string]string {
if ctx == nil {
return nil
}
params, _ := ctx.Value(routeParamsKey).(map[string]string)
return params
}
// WithRoutePattern records the router's matched route pattern for access logs.
func WithRoutePattern(ctx context.Context, pattern string) context.Context {
return context.WithValue(ctx, routePatternKey, pattern)
}
// RoutePattern returns the matched route pattern, when the adapter provides it.
func RoutePattern(ctx context.Context) string {
if ctx == nil {
return ""
}
pattern, _ := ctx.Value(routePatternKey).(string)
return pattern
}
// Context provides the business-facing request and response API. It contains
// only standard-library HTTP types, so it remains usable when Gin is replaced.
type Context struct {
request *http.Request
writer http.ResponseWriter
params map[string]string
values map[string]any
responded bool
status int
}
// NewContext creates a transport-neutral GoOmni context.
func NewContext(request *http.Request, writer http.ResponseWriter, params map[string]string) *Context {
if request == nil {
request = &http.Request{}
}
return &Context{
request: request,
writer: writer,
params: params,
values: make(map[string]any),
}
}
func (c *Context) Request() *http.Request { return c.request }
// Context returns the standard request context. It carries GoOmni
// correlation values such as the request ID and can be passed to services.
func (c *Context) Context() context.Context {
if c.request == nil {
return context.Background()
}
return c.request.Context()
}
// Raw returns the response writer supplied by the active network adapter.
// Prefer the transport-neutral methods on Context for portable handlers.
func (c *Context) Raw() http.ResponseWriter { return c.writer }
func (c *Context) Param(name string) string { return c.params[name] }
func (c *Context) Query(name string) string {
if c.request == nil {
return ""
}
return c.request.URL.Query().Get(name)
}
func (c *Context) Header(name string) string {
if c.request == nil {
return ""
}
return c.request.Header.Get(name)
}
func (c *Context) RequestID() string { return logx.RequestID(c.Context()) }
func (c *Context) Set(key string, value any) { c.values[key] = value }
func (c *Context) Get(key string) (any, bool) {
value, ok := c.values[key]
return value, ok
}
// WithContext replaces the standard request context while preserving the
// request and its headers.
func (c *Context) WithContext(ctx context.Context) {
if c.request != nil {
c.request = c.request.WithContext(ctx)
}
}
// SetResponseHeader sets a header on the underlying response.
func (c *Context) SetResponseHeader(name, value string) {
if c.writer != nil {
c.writer.Header().Set(name, value)
}
}
func (c *Context) BindJSON(value any) error { return validate.JSON(c.request, value) }
func (c *Context) BindQuery(value any) error { return validate.Query(c.request, value) }
func (c *Context) BindURI(value any) error { return validate.URI(c.params, value) }
// JSON is an escape hatch for responses that do not use the standard envelope.
func (c *Context) JSON(status int, value any) error {
if c.writer == nil {
return apierrors.Internal(nil)
}
c.writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.writer.WriteHeader(status)
if err := json.NewEncoder(c.writer).Encode(value); err != nil {
return apierrors.Internal(err)
}
c.responded = true
c.status = status
return nil
}
func (c *Context) OK(data any) error { return c.respond(http.StatusOK, "OK", "success", data) }
func (c *Context) Created(data any) error {
return c.respond(http.StatusCreated, "CREATED", "created", data)
}
func (c *Context) Accepted(data any) error {
return c.respond(http.StatusAccepted, "ACCEPTED", "accepted", data)
}
func (c *Context) NoContent() error {
c.writer.WriteHeader(http.StatusNoContent)
c.responded = true
c.status = http.StatusNoContent
return nil
}
func (c *Context) respond(status int, code, message string, data any) error {
return c.JSON(status, response.Response{
Code: code,
Message: message,
Data: data,
RequestID: c.RequestID(),
})
}
// Status returns the response status observed by Context.
func (c *Context) Status() int {
if c.status == 0 {
return http.StatusOK
}
return c.status
}
// Responded reports whether this handler has already written a response.
func (c *Context) Responded() bool { return c.responded }
// WriteError writes a stable public error response. Internal causes are not
// serialized and a partially written response is preserved.
func (c *Context) WriteError(err error) {
if c.responded {
return
}
apiErr := apierrors.From(err)
_ = c.JSON(apiErr.Status, response.Response{
Code: apiErr.Code,
Message: apiErr.Message,
RequestID: c.RequestID(),
})
}