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

166
app/app.go Normal file
View File

@@ -0,0 +1,166 @@
package app
import (
"context"
"errors"
"net/http"
"sync"
"time"
"git.yuntuo.hk/k2safe/go-omni/config"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/middleware"
ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin"
)
// App is the top-level GoOmni application. Its public behavior depends on
// httpx.Router, leaving the network implementation replaceable.
type App struct {
router httpx.Router
config config.Config
middlewares []httpx.Middleware
closers []func() error
closeOnce sync.Once
closeErr error
}
// New creates an application using the default Gin adapter.
func New(options ...config.Option) *App {
settings := configure(options...)
return newApp(ginrouter.New(settings.Mode), settings)
}
// NewWithRouter creates an application with a custom network layer. A custom
// router only needs to implement httpx.Router; handlers and business code stay
// unchanged when the transport changes.
func NewWithRouter(router httpx.Router, options ...config.Option) *App {
if router == nil {
panic("go-omni: nil router")
}
return newApp(router, configure(options...))
}
func configure(options ...config.Option) config.Config {
settings := config.Default()
for _, option := range options {
if option != nil {
option(&settings)
}
}
return settings
}
func newApp(router httpx.Router, settings config.Config) *App {
middlewares := []httpx.Middleware{middleware.RequestID(settings.RequestIDHeader)}
if settings.EnableLogger {
middlewares = append(middlewares, middleware.AccessLog())
}
middlewares = append(middlewares, middleware.Recovery())
return &App{router: router, config: settings, middlewares: middlewares}
}
// Router returns the transport contract used by this application.
func (a *App) Router() httpx.Router { return a.router }
// Engine is kept as a compatibility name for the old API. It returns a
// standard http.Handler, so callers no longer need a Gin type.
func (a *App) Engine() http.Handler { return a.router }
// ServeHTTP implements http.Handler for standard servers and HTTP tests.
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
a.router.ServeHTTP(writer, request)
}
// Config returns the effective application configuration.
func (a *App) Config() config.Config { return a.config }
// Use adds middleware to routes registered after this call.
func (a *App) Use(middleware ...httpx.Middleware) *App {
a.middlewares = append(a.middlewares, middleware...)
return a
}
// OnShutdown registers a cleanup function owned by the application. Cleanup
// functions run in reverse registration order when Close or RunContext ends.
// This keeps omni.New as the only public startup entry point while allowing
// projects to attach databases, loggers and other resources to its lifecycle.
func (a *App) OnShutdown(cleanup func() error) *App {
if cleanup != nil {
a.closers = append(a.closers, cleanup)
}
return a
}
// Close releases application resources. It is safe to call more than once.
func (a *App) Close() error {
a.closeOnce.Do(func() {
for index := len(a.closers) - 1; index >= 0; index-- {
if err := a.closers[index](); err != nil {
a.closeErr = errors.Join(a.closeErr, err)
}
}
})
return a.closeErr
}
// Run starts the HTTP server. The default address is :8080.
func (a *App) Run(address ...string) error {
return a.RunContext(context.Background(), address...)
}
// NewServer creates a standard HTTP server around the application.
func (a *App) NewServer(address string) *http.Server {
if address == "" {
address = a.config.Address
if address == "" {
address = ":8080"
}
}
return &http.Server{Addr: address, Handler: a.router}
}
// RunContext serves until the context is canceled, then performs a graceful shutdown.
func (a *App) RunContext(ctx context.Context, address ...string) error {
if ctx == nil {
ctx = context.Background()
}
listenOn := a.config.Address
if listenOn == "" {
listenOn = ":8080"
}
if len(address) > 0 && address[0] != "" {
listenOn = address[0]
}
server := a.NewServer(listenOn)
serveErr := make(chan error, 1)
go func() { serveErr <- server.ListenAndServe() }()
select {
case err := <-serveErr:
closeErr := a.Close()
if err == http.ErrServerClosed {
return closeErr
}
return errors.Join(err, closeErr)
case <-ctx.Done():
shutdownTimeout := a.config.ShutdownTimeout
if shutdownTimeout <= 0 {
shutdownTimeout = 10 * time.Second
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
shutdownErr := server.Shutdown(shutdownCtx)
if err := <-serveErr; err != nil && err != http.ErrServerClosed {
shutdownErr = errors.Join(shutdownErr, err)
}
return errors.Join(shutdownErr, a.Close())
}
}
func (a *App) writeError(ctx *httpx.Context, err error) {
if err == nil || ctx.Responded() {
return
}
ctx.WriteError(apierrors.From(err))
}

107
app/router.go Normal file
View File

@@ -0,0 +1,107 @@
package app
import (
"net/http"
"git.yuntuo.hk/k2safe/go-omni/httpx"
)
func (a *App) handle(routes httpx.RouterGroup, method, path string, handler httpx.Handler, middleware []httpx.Middleware) {
routes.Handle(method, path, a.adapt(handler, middleware...))
}
func (a *App) adapt(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...)
all = append(all, middleware...)
for index := len(all) - 1; index >= 0; index-- {
if all[index] != nil {
wrapped = all[index](wrapped)
}
}
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
requestContext := httpx.NewContext(request, writer, httpx.RouteParams(request.Context()))
if err := wrapped(requestContext); err != nil {
a.writeError(requestContext, err)
}
})
}
func (a *App) GET(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.handle(a.router, http.MethodGet, path, handler, middleware)
}
func (a *App) POST(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.handle(a.router, http.MethodPost, path, handler, middleware)
}
func (a *App) PUT(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.handle(a.router, http.MethodPut, path, handler, middleware)
}
func (a *App) PATCH(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.handle(a.router, http.MethodPatch, path, handler, middleware)
}
func (a *App) DELETE(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.handle(a.router, http.MethodDelete, path, handler, middleware)
}
func (a *App) Any(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
a.router.Any(path, a.adapt(handler, middleware...))
}
// RouterGroup scopes a route prefix and optional middleware.
type RouterGroup struct {
group httpx.RouterGroup
app *App
middlewares []httpx.Middleware
}
func (a *App) Group(path string, middleware ...httpx.Middleware) *RouterGroup {
return &RouterGroup{group: a.router.Group(path), app: a, middlewares: middleware}
}
func (g *RouterGroup) Use(middleware ...httpx.Middleware) *RouterGroup {
g.middlewares = append(g.middlewares, middleware...)
return g
}
func (g *RouterGroup) Group(path string, middleware ...httpx.Middleware) *RouterGroup {
combined := append([]httpx.Middleware{}, g.middlewares...)
combined = append(combined, middleware...)
return &RouterGroup{group: g.group.Group(path), app: g.app, middlewares: combined}
}
func (g *RouterGroup) route(method, path string, handler httpx.Handler, middleware []httpx.Middleware) {
combined := append([]httpx.Middleware{}, g.middlewares...)
combined = append(combined, middleware...)
g.app.handle(g.group, method, path, handler, combined)
}
func (g *RouterGroup) GET(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
g.route(http.MethodGet, path, handler, middleware)
}
func (g *RouterGroup) POST(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
g.route(http.MethodPost, path, handler, middleware)
}
func (g *RouterGroup) PUT(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
g.route(http.MethodPut, path, handler, middleware)
}
func (g *RouterGroup) PATCH(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
g.route(http.MethodPatch, path, handler, middleware)
}
func (g *RouterGroup) DELETE(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
g.route(http.MethodDelete, path, handler, middleware)
}
func (g *RouterGroup) Any(path string, handler httpx.Handler, middleware ...httpx.Middleware) {
combined := append([]httpx.Middleware{}, g.middlewares...)
combined = append(combined, middleware...)
g.group.Any(path, g.app.adapt(handler, combined...))
}