github.com/blend/go-sdk@v1.20220411.3/web/view_cache.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package web 9 10 import ( 11 "html/template" 12 "net/http" 13 "sync" 14 15 "github.com/blend/go-sdk/bufferutil" 16 "github.com/blend/go-sdk/ex" 17 templatehelpers "github.com/blend/go-sdk/template" 18 ) 19 20 const ( 21 // DefaultTemplateNameBadRequest is the default template name for bad request view results. 22 DefaultTemplateNameBadRequest = "bad_request" 23 // DefaultTemplateNameInternalError is the default template name for internal server error view results. 24 DefaultTemplateNameInternalError = "error" 25 // DefaultTemplateNameNotFound is the default template name for not found error view results. 26 DefaultTemplateNameNotFound = "not_found" 27 // DefaultTemplateNameNotAuthorized is the default template name for not authorized error view results. 28 DefaultTemplateNameNotAuthorized = "not_authorized" 29 // DefaultTemplateNameStatus is the default template name for status view results. 30 DefaultTemplateNameStatus = "status" 31 32 // DefaultTemplateBadRequest is a basic view. 33 DefaultTemplateBadRequest = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body><h4>Bad Request</h4></body><pre>{{ .ViewModel }}</pre></html>` 34 // DefaultTemplateInternalError is a basic view. 35 DefaultTemplateInternalError = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body><h4>Internal Error</h4><pre>{{ .ViewModel }}</body></html>` 36 // DefaultTemplateNotAuthorized is a basic view. 37 DefaultTemplateNotAuthorized = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body><h4>Not Authorized</h4></body></html>` 38 // DefaultTemplateNotFound is a basic view. 39 DefaultTemplateNotFound = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body><h4>Not Found</h4></body></html>` 40 // DefaultTemplateStatus is a basic view. 41 DefaultTemplateStatus = `<html><head><style>body { font-family: sans-serif; text-align: center; }</style></head><body><h4>{{ .ViewModel.StatusCode }}</h4></body><pre>{{ .ViewModel.Response }}</pre></html>` 42 ) 43 44 // Assert the view cache is a result provider. 45 var ( 46 _ ResultProvider = (*ViewCache)(nil) 47 ) 48 49 // MustNewViewCache returns a new view cache and panics on eror. 50 func MustNewViewCache(opts ...ViewCacheOption) *ViewCache { 51 vc, err := NewViewCache(opts...) 52 if err != nil { 53 panic(err) 54 } 55 return vc 56 } 57 58 // NewViewCache returns a new view cache. 59 func NewViewCache(options ...ViewCacheOption) (*ViewCache, error) { 60 vc := &ViewCache{ 61 FuncMap: template.FuncMap(templatehelpers.ViewFuncs{}.FuncMap()), 62 BufferPool: bufferutil.NewPool(1024), 63 InternalErrorTemplateName: DefaultTemplateNameInternalError, 64 BadRequestTemplateName: DefaultTemplateNameBadRequest, 65 NotFoundTemplateName: DefaultTemplateNameNotFound, 66 NotAuthorizedTemplateName: DefaultTemplateNameNotAuthorized, 67 StatusTemplateName: DefaultTemplateNameStatus, 68 } 69 var err error 70 for _, option := range options { 71 if err = option(vc); err != nil { 72 return nil, err 73 } 74 } 75 return vc, nil 76 } 77 78 // ViewCache is the cached views used in view results. 79 type ViewCache struct { 80 sync.Mutex 81 LiveReload bool 82 FuncMap template.FuncMap 83 Paths []string 84 Literals []string 85 Templates *template.Template 86 BufferPool *bufferutil.Pool 87 88 BadRequestTemplateName string 89 InternalErrorTemplateName string 90 NotFoundTemplateName string 91 NotAuthorizedTemplateName string 92 StatusTemplateName string 93 } 94 95 // Initialize caches templates by path. 96 func (vc *ViewCache) Initialize() error { 97 vc.Lock() 98 defer vc.Unlock() 99 if vc.Templates == nil && !vc.LiveReload { 100 return vc.initialize() 101 } 102 return nil 103 } 104 105 // Parse parses the view tree. 106 func (vc *ViewCache) Parse() (views *template.Template, err error) { 107 views = template.New("").Funcs(vc.FuncMap) 108 if len(vc.Paths) > 0 { 109 views, err = views.ParseFiles(vc.Paths...) 110 if err != nil { 111 err = ex.New(err) 112 return 113 } 114 } 115 116 if len(vc.Literals) > 0 { 117 for _, viewLiteral := range vc.Literals { 118 views, err = views.Parse(viewLiteral) 119 if err != nil { 120 err = ex.New(err) 121 return 122 } 123 } 124 } 125 return 126 } 127 128 // Lookup looks up a view. 129 func (vc *ViewCache) Lookup(name string) (*template.Template, error) { 130 if vc.Templates == nil { 131 templates, err := vc.Parse() 132 if err != nil { 133 return nil, err 134 } 135 return templates.Lookup(name), nil 136 } 137 return vc.Templates.Lookup(name), nil 138 } 139 140 // ---------------------------------------------------------------------- 141 // results 142 // ---------------------------------------------------------------------- 143 144 // BadRequest returns a view result. 145 func (vc *ViewCache) BadRequest(err error) Result { 146 t, viewErr := vc.Lookup(vc.BadRequestTemplateName) 147 if viewErr != nil { 148 return vc.viewError(viewErr) 149 } 150 if t == nil { 151 t, _ = template.New("default").Parse(DefaultTemplateBadRequest) 152 } 153 154 return &ViewResult{ 155 ViewName: vc.BadRequestTemplateName, 156 StatusCode: http.StatusBadRequest, 157 ViewModel: err, 158 Template: t, 159 Views: vc, 160 } 161 } 162 163 // InternalError returns a view result. 164 func (vc *ViewCache) InternalError(err error) Result { 165 t, viewErr := vc.Lookup(vc.InternalErrorTemplateName) 166 if viewErr != nil { 167 return vc.viewError(viewErr) 168 } 169 if t == nil { 170 t, _ = template.New("").Parse(DefaultTemplateInternalError) 171 } 172 return ResultWithLoggedError(&ViewResult{ 173 ViewName: vc.InternalErrorTemplateName, 174 StatusCode: http.StatusInternalServerError, 175 ViewModel: err, 176 Template: t, 177 Views: vc, 178 }, err) 179 } 180 181 // NotFound returns a view result. 182 func (vc *ViewCache) NotFound() Result { 183 t, viewErr := vc.Lookup(vc.NotFoundTemplateName) 184 if viewErr != nil { 185 return vc.viewError(viewErr) 186 } 187 if t == nil { 188 t, _ = template.New("").Parse(DefaultTemplateNotFound) 189 } 190 return &ViewResult{ 191 ViewName: vc.NotFoundTemplateName, 192 StatusCode: http.StatusNotFound, 193 Template: t, 194 Views: vc, 195 } 196 } 197 198 // NotAuthorized returns a view result. 199 func (vc *ViewCache) NotAuthorized() Result { 200 t, err := vc.Lookup(vc.NotAuthorizedTemplateName) 201 if err != nil { 202 return vc.viewError(err) 203 } 204 if t == nil { 205 t, _ = template.New("").Parse(DefaultTemplateNotAuthorized) 206 } 207 208 return &ViewResult{ 209 ViewName: vc.NotAuthorizedTemplateName, 210 StatusCode: http.StatusUnauthorized, 211 Template: t, 212 Views: vc, 213 } 214 } 215 216 // Status returns a status view result. 217 func (vc *ViewCache) Status(statusCode int, response interface{}) Result { 218 t, viewErr := vc.Lookup(vc.StatusTemplateName) 219 if viewErr != nil { 220 return vc.viewError(viewErr) 221 } 222 if t == nil { 223 t, _ = template.New("").Parse(DefaultTemplateStatus) 224 } 225 226 return &ViewResult{ 227 Views: vc, 228 ViewName: vc.StatusTemplateName, 229 StatusCode: statusCode, 230 Template: t, 231 ViewModel: StatusViewModel{ 232 StatusCode: statusCode, 233 Response: ResultOrDefault(response, http.StatusText(statusCode))}, 234 } 235 } 236 237 // View returns a view result. 238 func (vc *ViewCache) View(viewName string, viewModel interface{}) Result { 239 return vc.ViewStatus(http.StatusOK, viewName, viewModel) 240 } 241 242 // ViewStatus returns a view result with a given status code.. 243 func (vc *ViewCache) ViewStatus(statusCode int, viewName string, viewModel interface{}) Result { 244 t, err := vc.Lookup(viewName) 245 if err != nil { 246 return vc.viewError(err) 247 } 248 if t == nil { 249 return vc.InternalError(ex.New(ErrUnsetViewTemplate, ex.OptMessagef("viewname: %s", viewName))) 250 } 251 252 return &ViewResult{ 253 ViewName: viewName, 254 StatusCode: statusCode, 255 ViewModel: viewModel, 256 Template: t, 257 Views: vc, 258 } 259 } 260 261 // ---------------------------------------------------------------------- 262 // properties 263 // ---------------------------------------------------------------------- 264 265 // AddPaths adds paths to the view collection. 266 func (vc *ViewCache) AddPaths(paths ...string) { 267 vc.Paths = append(vc.Paths, paths...) 268 } 269 270 // AddLiterals adds view literal strings to the view collection. 271 func (vc *ViewCache) AddLiterals(views ...string) { 272 vc.Literals = append(vc.Literals, views...) 273 } 274 275 // ---------------------------------------------------------------------- 276 // helpers 277 // ---------------------------------------------------------------------- 278 279 func (vc *ViewCache) viewError(err error) Result { 280 t, _ := template.New("").Parse(DefaultTemplateInternalError) 281 return &ViewResult{ 282 ViewName: DefaultTemplateNameInternalError, 283 StatusCode: http.StatusInternalServerError, 284 ViewModel: err, 285 Template: t, 286 Views: vc, 287 } 288 } 289 290 func (vc *ViewCache) initialize() error { 291 if len(vc.Paths) == 0 && len(vc.Literals) == 0 { 292 return nil 293 } 294 views, err := vc.Parse() 295 if err != nil { 296 return err 297 } 298 vc.Templates = views 299 return nil 300 }