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  }