feat: bootstrap reusable GoOmni framework
This commit is contained in:
166
app/app.go
Normal file
166
app/app.go
Normal 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
107
app/router.go
Normal 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...))
|
||||
}
|
||||
Reference in New Issue
Block a user