go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/web/views.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 "errors" 12 "fmt" 13 "io/fs" 14 "net/http" 15 "text/template" 16 17 "go.charczuk.com/sdk/errutil" 18 ) 19 20 const ( 21 // TemplateBadRequest is the default template name for bad request view results. 22 TemplateBadRequest = "bad_request" 23 // TemplateInternalError is the default template name for internal server error view results. 24 TemplateInternalError = "error" 25 // TemplateNotFound is the default template name for not found error view results. 26 TemplateNotFound = "not_found" 27 // TemplateNotAuthorized is the default template name for not authorized error view results. 28 TemplateNotAuthorized = "not_authorized" 29 // TemplateResult is the default template name for the result catchall endpoint. 30 TemplateResult = "result" 31 ) 32 33 var ( 34 // ErrUnsetViewTemplate is an error that is thrown if a given secure session id is invalid. 35 ErrUnsetViewTemplate = errors.New("view result template is unset") 36 ) 37 38 const ( 39 templateLiteralHeader = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body>` 40 templateLiteralFooter = `</body></html>` 41 templateLiteralBadRequest = templateLiteralHeader + `<h4>Bad Request</h4></body><pre>{{ . }}</pre>` + templateLiteralFooter 42 templateLiteralInternalError = templateLiteralHeader + `<h4>Internal Error</h4><pre>{{ . }}</pre>` + templateLiteralFooter 43 templateLiteralNotAuthorized = templateLiteralHeader + `<h4>Not Authorized</h4>` + templateLiteralFooter 44 templateLiteralNotFound = templateLiteralHeader + `<h4>Not Found</h4>` + templateLiteralFooter 45 templateLiteralResult = templateLiteralHeader + `<h4>{{ . }}</h4>` + templateLiteralFooter 46 ) 47 48 var ( 49 _ ResultProvider = (*Views)(nil) 50 ) 51 52 // Views is the cached views used in view results. 53 type Views struct { 54 FuncMap template.FuncMap 55 56 ViewPaths []string 57 ViewLiterals []string 58 ViewFS []ViewFS 59 60 bp *BufferPool 61 t *template.Template 62 } 63 64 // ViewFS is a fs reference for views. 65 type ViewFS struct { 66 // FS is the virtual filesystem reference. 67 FS fs.FS 68 // Patterns denotes glob patterns to match 69 // within the filesystem itself (and can be empty!) 70 Patterns []string 71 } 72 73 // AddPaths adds paths to the view collection. 74 func (vc *Views) AddPaths(paths ...string) { 75 vc.ViewPaths = append(vc.ViewPaths, paths...) 76 } 77 78 // AddLiterals adds view literal strings to the view collection. 79 func (vc *Views) AddLiterals(views ...string) { 80 vc.ViewLiterals = append(vc.ViewLiterals, views...) 81 } 82 83 // AddFS adds view fs instances to the view collection. 84 func (vc *Views) AddFS(fs ...ViewFS) { 85 vc.ViewFS = append(vc.ViewFS, fs...) 86 } 87 88 // Initialize caches templates by path. 89 func (vc *Views) Initialize() (err error) { 90 if vc.t == nil { 91 if len(vc.ViewPaths) > 0 || len(vc.ViewLiterals) > 0 || len(vc.ViewFS) > 0 { 92 vc.t, err = vc.Parse() 93 if err != nil { 94 err = fmt.Errorf("view initialize; %w", err) 95 return 96 } 97 } else { 98 vc.t = template.New("") 99 } 100 } 101 if vc.bp == nil { 102 vc.bp = NewBufferPool(256) 103 } 104 return 105 } 106 107 // Parse parses the view tree. 108 func (vc Views) Parse() (views *template.Template, err error) { 109 views = template.New("views").Funcs(vc.FuncMap).Funcs(RequestFuncStubs()) 110 if len(vc.ViewPaths) > 0 { 111 views, err = views.ParseFiles(vc.ViewPaths...) 112 if err != nil { 113 err = fmt.Errorf("cannot parse view files: %w", err) 114 return 115 } 116 } 117 for _, viewLiteral := range vc.ViewLiterals { 118 views, err = views.Parse(viewLiteral) 119 if err != nil { 120 err = fmt.Errorf("cannot parse view literals: %w", err) 121 return 122 } 123 } 124 for _, viewFS := range vc.ViewFS { 125 views, err = views.ParseFS(viewFS.FS, viewFS.Patterns...) 126 if err != nil { 127 err = fmt.Errorf("cannot parse view filesystems: %w", err) 128 return 129 } 130 } 131 return 132 } 133 134 // Lookup looks up a view. 135 func (vc Views) Lookup(name string) *template.Template { 136 // we do the nil check here because there are usage patterns 137 // where we may not have initialized the template cache 138 // but still want to use Lookup. 139 if vc.t == nil { 140 return nil 141 } 142 return vc.t.Lookup(name) 143 } 144 145 // BadRequest returns a view result. 146 func (vc Views) BadRequest(err error) Result { 147 t := vc.Lookup(TemplateBadRequest) 148 if t == nil { 149 t, _ = template.New("").Parse(templateLiteralBadRequest) 150 } 151 return &ViewResult{ 152 ViewName: TemplateBadRequest, 153 StatusCode: http.StatusBadRequest, 154 ViewModel: err, 155 Template: t, 156 Views: vc, 157 } 158 } 159 160 // InternalError returns a view result. 161 func (vc Views) InternalError(err error) Result { 162 t := vc.Lookup(TemplateInternalError) 163 if t == nil { 164 t, _ = template.New("").Parse(templateLiteralInternalError) 165 } 166 return &ViewResult{ 167 ViewName: TemplateInternalError, 168 StatusCode: http.StatusInternalServerError, 169 ViewModel: err, 170 Template: t, 171 Views: vc, 172 Err: err, 173 } 174 } 175 176 // NotFound returns a view result. 177 func (vc Views) NotFound() Result { 178 t := vc.Lookup(TemplateNotFound) 179 if t == nil { 180 t, _ = template.New("").Parse(templateLiteralNotFound) 181 } 182 return &ViewResult{ 183 ViewName: TemplateNotFound, 184 StatusCode: http.StatusNotFound, 185 Template: t, 186 Views: vc, 187 } 188 } 189 190 // NotAuthorized returns a view result. 191 func (vc Views) NotAuthorized() Result { 192 t := vc.Lookup(TemplateNotAuthorized) 193 if t == nil { 194 t, _ = template.New("").Parse(templateLiteralNotAuthorized) 195 } 196 return &ViewResult{ 197 ViewName: TemplateNotAuthorized, 198 StatusCode: http.StatusUnauthorized, 199 Template: t, 200 Views: vc, 201 } 202 } 203 204 // Result returns a status view result. 205 func (vc Views) Result(statusCode int, response any) Result { 206 t := vc.Lookup(TemplateResult) 207 if t == nil { 208 t, _ = template.New("").Parse(templateLiteralResult) 209 } 210 return &ViewResult{ 211 Views: vc, 212 ViewName: TemplateResult, 213 StatusCode: statusCode, 214 Template: t, 215 ViewModel: response, 216 } 217 } 218 219 // View returns a view result with an OK status code. 220 func (vc Views) View(viewName string, viewModel any) Result { 221 t := vc.Lookup(viewName) 222 if t == nil { 223 return vc.InternalError(ErrUnsetViewTemplate) 224 } 225 return &ViewResult{ 226 ViewName: viewName, 227 StatusCode: http.StatusOK, 228 ViewModel: viewModel, 229 Template: t, 230 Views: vc, 231 } 232 } 233 234 // ViewStatus returns a view result with a given status code. 235 func (vc Views) ViewStatus(statusCode int, viewName string, viewModel any) Result { 236 t := vc.Lookup(viewName) 237 if t == nil { 238 return vc.InternalError(ErrUnsetViewTemplate) 239 } 240 return &ViewResult{ 241 ViewName: viewName, 242 StatusCode: statusCode, 243 ViewModel: viewModel, 244 Template: t, 245 Views: vc, 246 } 247 } 248 249 // ViewResult is a result that renders a view. 250 type ViewResult struct { 251 ViewName string 252 StatusCode int 253 ViewModel any 254 Views Views 255 Template *template.Template 256 Err error 257 } 258 259 // Render renders the result to the given response writer. 260 func (vr *ViewResult) Render(ctx Context) (err error) { 261 if vr.Template == nil { 262 err = errutil.Append(ErrUnsetViewTemplate, vr.Err) 263 return 264 } 265 ctx.Response().Header().Set(HeaderContentType, ContentTypeHTML) 266 267 buffer := vr.Views.bp.Get() 268 defer vr.Views.bp.Put(buffer) 269 270 executeErr := vr.Template.Funcs(vr.RequestFuncs(ctx)).Execute(buffer, vr.ViewModel) 271 if executeErr != nil { 272 ctx.Response().WriteHeader(http.StatusInternalServerError) 273 _, writeErr := ctx.Response().Write([]byte(fmt.Sprintf("%+v\n", executeErr))) 274 err = errutil.Append(executeErr, writeErr, vr.Err) 275 return 276 } 277 278 ctx.Response().WriteHeader(vr.StatusCode) 279 _, writeErr := ctx.Response().Write(buffer.Bytes()) 280 err = errutil.Append(writeErr, vr.Err) 281 return 282 } 283 284 // RequestFuncStubs are "stub" versions of the request bound funcs. 285 func RequestFuncStubs() template.FuncMap { 286 return template.FuncMap{ 287 "request_context": func() Context { return nil }, 288 "localize": func(str string) string { return str }, 289 } 290 } 291 292 // RequestFuncs returns the view funcs that are bound to the request specifically. 293 func (vr *ViewResult) RequestFuncs(ctx Context) template.FuncMap { 294 return template.FuncMap{ 295 "request_context": func() Context { return ctx }, 296 "localize": func(str string) (string, error) { 297 return ctx.App().Localization.Printer(ctx.Locale()).Print(str), nil 298 }, 299 } 300 }