4.5 KB
app.go
package application
import (
"cmp"
"context"
"embed"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"reflect"
"syscall"
"time"
)
// Middleware wraps an http.Handler (e.g. for request interception).
type Middleware func(http.Handler) http.Handler
// App is the main application container
type App struct {
Emailer
funcs template.FuncMap
viewsFS fs.FS // Store FS for on-demand loading
base *template.Template // Only layouts + partials
controllers map[string]Controller
middlewares []Middleware
}
// Bouncer is a function that gates route access.
// Returns true to allow the request, false to block it.
type Bouncer func(app *App, w http.ResponseWriter, r *http.Request) bool
// Serve creates a View handler that renders a template
func (app *App) Serve(templateName string, bouncer Bouncer) *View {
return &View{app, templateName, bouncer}
}
// Func registers a template function with the application.
// This must be called before templates are parsed (typically in options).
func (app *App) Func(name string, fn any) {
app.funcs[name] = fn
}
// Controller registers a controller with the application.
// If the controller has a Setup method, it will be called.
func (app *App) Controller(name string, controller Controller) {
app.controllers[name] = controller
if setupper, ok := controller.(interface{ Setup(*App) }); ok {
setupper.Setup(app)
}
}
// Method creates a handler that calls a controller method with optional bouncer
func (app *App) Method(controller any, methodName string, bouncer Bouncer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if bouncer != nil && !bouncer(app, w, r) {
return
}
method := reflect.ValueOf(controller).MethodByName(methodName)
if !method.IsValid() {
http.Error(w, "Method not found: "+methodName, http.StatusInternalServerError)
return
}
method.Call([]reflect.Value{reflect.ValueOf(w), reflect.ValueOf(r)})
})
}
// New creates an App and applies the given options.
// This triggers controller Setup functions, useful for build-time tasks.
func New(opts ...Option) *App {
app := &App{
funcs: template.FuncMap{},
controllers: map[string]Controller{},
}
for _, opt := range opts {
opt(app)
}
return app
}
// Serve starts the HTTP server with the given views and options
func Serve(views embed.FS, opts ...Option) {
app := New(opts...)
// Store FS for on-demand loading, parse base templates (layouts + partials only)
app.viewsFS = views
app.base = app.parseBaseTemplates(views)
// Serve static files
if staticFS, err := fs.Sub(views, "views/static"); err == nil {
http.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
}
port := cmp.Or(os.Getenv("PORT"), "5000")
tlsCert := os.Getenv("TLS_CERT")
tlsKey := os.Getenv("TLS_KEY")
// Build handler chain: custom middlewares innermost, logging outermost
var handler http.Handler = http.DefaultServeMux
for _, mw := range app.middlewares {
handler = mw(handler)
}
handler = LoggingMiddleware(handler)
// Create HTTP server
httpServer := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: handler,
}
// Start HTTP server
go func() {
log.Printf("HTTP server starting on http://0.0.0.0:%s", port)
if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Start HTTPS server if TLS configured and certs exist
var httpsServer *http.Server
if tlsCert != "" && tlsKey != "" {
if _, err := os.Stat(tlsCert); err == nil {
httpsServer = &http.Server{
Addr: "0.0.0.0:443",
Handler: handler,
}
go func() {
log.Printf("HTTPS server starting on https://0.0.0.0:443")
if err := httpsServer.ListenAndServeTLS(tlsCert, tlsKey); err != http.ErrServerClosed {
log.Fatalf("HTTPS server error: %v", err)
}
}()
} else {
log.Printf("TLS cert not found at %s, skipping HTTPS", tlsCert)
}
}
// Wait for shutdown signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Println("Shutting down gracefully...")
// Give in-flight requests 30 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
if httpsServer != nil {
if err := httpsServer.Shutdown(ctx); err != nil {
log.Printf("HTTPS shutdown error: %v", err)
}
}
log.Println("Server stopped")
}