go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/web/app.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package web 9 10 import ( 11 "context" 12 "net/http" 13 "time" 14 15 "go.charczuk.com/sdk/errutil" 16 "go.charczuk.com/sdk/viewutil" 17 ) 18 19 // New returns a new app with defaults configured and applies 20 // a given set of options to it. 21 // 22 // You can alternately use the `app := new(App)` form but it's 23 // highly recommended to use this constructor. 24 func New() *App { 25 app := new(App) 26 app.Views.FuncMap = viewutil.Funcs 27 app.NotFoundHandler = app.ActionHandler(defaultNotFoundHandler) 28 app.PanicAction = defaultPanicAction 29 return app 30 } 31 32 func defaultNotFoundHandler(ctx Context) Result { 33 return AcceptedProvider(ctx).NotFound() 34 } 35 36 func defaultPanicAction(ctx Context, r any) Result { 37 return AcceptedProvider(ctx).InternalError(errutil.New(r)) 38 } 39 40 // App is the container type for the application and its resources. 41 // 42 // Typical usage is just to instantiate it with a bare constructor: 43 // 44 // app := new(web.App) 45 // app.Get("/", func(ctx web.Context) web.Result { 46 // return &web.JSONResult{StatusCode: http.StatusOK, Response: "OK!" } 47 // } 48 type App struct { 49 Auth 50 RouteTree 51 Server 52 53 Views Views 54 Localization Localization 55 56 BaseURL string 57 Headers http.Header 58 PanicAction PanicAction 59 60 middleware []Middleware 61 onRequest []func(RequestEvent) 62 onError []func(Context, error) 63 } 64 65 // RegisterControllers registers controllers. 66 func (a *App) RegisterControllers(controllers ...Controller) { 67 for _, c := range controllers { 68 c.Register(a) 69 } 70 } 71 72 // RegisterMiddleware registers global middleware. 73 func (a *App) RegisterMiddleware(middleware ...Middleware) { 74 a.middleware = append(a.middleware, middleware...) 75 } 76 77 // RegisterConfig registers the config. 78 func (a *App) RegisterConfig(cfg Config) { 79 cfg.ApplyTo(a) 80 } 81 82 // RegisterLoggerListeners registers a logger with the listen, 83 // request, and error handler lists. 84 func (a *App) RegisterLoggerListeners(log Logger) { 85 a.RegisterOnListen(LogOnListen(a, log)) 86 a.RegisterOnRequest(LogOnRequest(log)) 87 a.RegisterOnError(LogOnError(log)) 88 } 89 90 // RegisterOnRequest adds an on request hook. 91 func (a *App) RegisterOnRequest(fn func(RequestEvent)) { 92 a.onRequest = append(a.onRequest, fn) 93 } 94 95 // RegisterOnError adds an on request hook. 96 func (a *App) RegisterOnError(fn func(Context, error)) { 97 a.onError = append(a.onError, fn) 98 } 99 100 // Get registers a GET request route handler. 101 // 102 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 103 func (a *App) Get(path string, action Action) { 104 a.HandleAction(http.MethodGet, path, action) 105 } 106 107 // Options registers a OPTIONS request route handler. 108 // 109 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 110 func (a *App) Options(path string, action Action) { 111 a.HandleAction(http.MethodOptions, path, action) 112 } 113 114 // Head registers a HEAD request route handler. 115 // 116 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 117 func (a *App) Head(path string, action Action) { 118 a.HandleAction(http.MethodHead, path, action) 119 } 120 121 // Put registers a PUT request route handler. 122 // 123 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 124 func (a *App) Put(path string, action Action) { 125 a.HandleAction(http.MethodPut, path, action) 126 } 127 128 // Patch registers a PATCH request route handler. 129 // 130 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 131 func (a *App) Patch(path string, action Action) { 132 a.HandleAction(http.MethodPatch, path, action) 133 } 134 135 // Post registers a POST request route handler. 136 // 137 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 138 func (a *App) Post(path string, action Action) { 139 a.HandleAction(http.MethodPost, path, action) 140 } 141 142 // Delete registers a DELETE request route handler. 143 // 144 // To add additional middleware, use `web.NextMiddleware(action, ...middleware)`. 145 func (a *App) Delete(path string, action Action) { 146 a.HandleAction(http.MethodDelete, path, action) 147 } 148 149 // HandleAction registers an action for a given method and path with the given middleware. 150 func (a *App) HandleAction(method string, path string, action Action) { 151 a.Handle(method, path, a.ActionHandler(NestMiddleware(action, a.middleware...))) 152 } 153 154 // Lookup finds the route data for a given method and path. 155 func (a *App) Lookup(method, path string) (route *Route, params RouteParameters, skipSlashRedirect bool) { 156 if root := a.Routes[method]; root != nil { 157 route, params, skipSlashRedirect = root.getPath(path) 158 return 159 } 160 return 161 } 162 163 // ActionHandler is the translation step from Action to Handler. 164 func (a *App) ActionHandler(action Action) Handler { 165 return func(w http.ResponseWriter, r *http.Request, route *Route, p RouteParameters) { 166 ctx := &baseContext{ 167 app: a, 168 req: r, 169 res: NewStatusResponseWriter(w), 170 route: route, 171 routeParams: p, 172 started: time.Now().UTC(), 173 } 174 if a.PanicAction != nil { 175 defer func() { 176 if r := recover(); r != nil { 177 if res := a.PanicAction(ctx, r); res != nil { 178 a.renderResult(ctx, res) 179 } 180 a.handleError(ctx, errutil.New(r)) 181 } 182 }() 183 } 184 for key, value := range a.Headers { 185 ctx.Response().Header()[key] = value 186 } 187 if res := action(ctx); res != nil { 188 a.renderResult(ctx, res) 189 } 190 if len(a.onRequest) > 0 { 191 re := NewRequestEvent(ctx) 192 for _, reqHandler := range a.onRequest { 193 reqHandler(re) 194 } 195 } 196 } 197 } 198 199 // Initialize calls initializers on the localization engine 200 // and the views cache. 201 // 202 // If you don't use either of these facilities (e.g. if you're only 203 // implementing JSON api routes, you can skip calling this function). 204 func (a *App) Initialize() error { 205 if err := a.Localization.Initialize(); err != nil { 206 return err 207 } 208 if err := a.Views.Initialize(); err != nil { 209 return err 210 } 211 return nil 212 } 213 214 // Start implements the first phase of graceful.Graceful 215 // and initializes the view cache and does some other housekeeping 216 // before starting the server. 217 func (a *App) Start(ctx context.Context) error { 218 if err := a.Views.Initialize(); err != nil { 219 return err 220 } 221 a.Handler = a.RouteTree 222 return a.Server.Start(ctx) 223 } 224 225 // 226 // internal helpers 227 // 228 229 func (a *App) renderResult(ctx Context, res Result) { 230 if err := res.Render(ctx); err != nil { 231 a.handleError(ctx, err) 232 } 233 } 234 235 func (a *App) handleError(ctx Context, err error) { 236 if err == nil { 237 return 238 } 239 for _, errHandler := range a.onError { 240 errHandler(ctx, err) 241 } 242 }