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  }