Skip to content

Commit

Permalink
Remove HandlerError
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Mar 25, 2024
1 parent 5e9396f commit 2f5d02c
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 112 deletions.
16 changes: 12 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# TODO

- [ ] Dot Provider system
- [ ] Accept configuration from JSON
- [ ] Accept configuration from Caddyfile
- [ ] Env?
- [ ] Update documentation
- [ ] Readme docs
- [ ] Go API docs
- [ ] CLI docs
- [ ] Downgrade to go 1.21
- [ ] Use go-arg library for arg parsing
- [ ] Fix go-arg embedded structs or don't use them https://github.com/alexflint/go-arg/issues/242
- [ ] Add ServeTemplate that delays template rendering until requested by
http.ServeContent to optimize cache behavior.
http.ServeContent to optimize cache behavior. Something like
https://github.com/spatialcurrent/go-lazy ?
- [ ] Add NATS module:
- [ ] Subscribe to subject, loop on receive to send via open SSE connection
- [ ] Publish message to subject
Expand Down Expand Up @@ -56,9 +59,14 @@

## v0.5 beta - Mar 2024

- [x] Create system for customizing template dot value. Potential modules: Config/DB/FS/NATS/mail
- Dot Provider system
- [x] Create system for customizing template dot value
- [x] Convert existing modules
- [x] Accept configuration from cli
- [x] Get rid of Server/Instance interfaces, expose structs directly
- [x] Catch servemux addhandler panics and return an error instead
- [x] Use go-arg library for arg parsing
- [x] Fix go-arg embedded structs or don't use them https://github.com/alexflint/go-arg/issues/242

## v0.4 - Mar 2024

Expand Down
25 changes: 0 additions & 25 deletions dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,6 @@ import (
"sync"
)

/*
Name DotProvider
- Context providers should be able to add methods and fields directly to the
root context {{.}}, by having an anonymous field
- Context providers should have customizable name
- Should have a default
- Users should be able to override name
Needs to get configuration from:
- Args
- Parse args with https://pkg.go.dev/github.com/alexflint/go-arg#Parse
- https://github.com/alexflint/go-arg/issues/220
- Format: -c "Tx:sql:sqlite3:file"
- Json
- Caddyfile
- Manual go configuration
- env? defaults?
*/

var registrations map[string]RegisteredDotProvider = make(map[string]RegisteredDotProvider)

func RegisterDot(r RegisteredDotProvider) {
Expand Down
46 changes: 19 additions & 27 deletions dot_resp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package xtemplate

import (
"context"
"errors"
"log/slog"
"maps"
"net/http"
Expand All @@ -14,80 +13,73 @@ import (

type responseDotProvider struct{}

func (responseDotProvider) Type() reflect.Type { return reflect.TypeOf(ResponseDot{}) }
func (responseDotProvider) Type() reflect.Type { return reflect.TypeOf(responseDot{}) }

func (responseDotProvider) Value(log *slog.Logger, _ context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(ResponseDot{Header: make(http.Header), status: http.StatusOK, w: w, r: r, log: log}), nil
return reflect.ValueOf(responseDot{Header: make(http.Header), status: http.StatusOK, w: w, r: r, log: log}), nil
}

func (responseDotProvider) Cleanup(v reflect.Value, err error) error {
d := v.Interface().(ResponseDot)
var handlerErr HandlerError
d := v.Interface().(responseDot)
if err == nil {
maps.Copy(d.w.Header(), d.Header)
d.w.WriteHeader(d.status)
return nil
} else if errors.As(err, &handlerErr) {
d.log.Debug("forwarding response handling", slog.Any("handler", handlerErr))
handlerErr.ServeHTTP(d.w, d.r)
return nil
} else {
return err
}
return err
}

var _ CleanupDotProvider = responseDotProvider{}

type ResponseDot struct {
type responseDot struct {
http.Header
status int
w http.ResponseWriter
r *http.Request
log *slog.Logger
}

// ServeContent aborts execution of the template and instead responds to the request with content
func (d *ResponseDot) ServeContent(path_ string, modtime time.Time, content string) (string, error) {
return "", NewHandlerError("ServeContent", func(w http.ResponseWriter, r *http.Request) {
path_ = path.Clean(path_)

d.log.Debug("serving content response", slog.String("path", path_))

http.ServeContent(w, r, path_, modtime, strings.NewReader(content))
})
// ServeContent aborts execution of the template and instead responds to the
// request with content with any headers set by AddHeader and SetHeader so far
// but ignoring SetStatus.
func (d *responseDot) ServeContent(path_ string, modtime time.Time, content string) (string, error) {
path_ = path.Clean(path_)
d.log.Debug("serving content response", slog.String("path", path_))
maps.Copy(d.w.Header(), d.Header)
http.ServeContent(d.w, d.r, path_, modtime, strings.NewReader(content))
return "", ReturnError{}
}

// AddHeader adds a header field value, appending val to
// existing values for that field. It returns an
// empty string.
func (h ResponseDot) AddHeader(field, val string) string {
func (h responseDot) AddHeader(field, val string) string {
h.Header.Add(field, val)
return ""
}

// SetHeader sets a header field value, overwriting any
// other values for that field. It returns an
// empty string.
func (h ResponseDot) SetHeader(field, val string) string {
func (h responseDot) SetHeader(field, val string) string {
h.Header.Set(field, val)
return ""
}

// DelHeader deletes a header field. It returns an empty string.
func (h ResponseDot) DelHeader(field string) string {
func (h responseDot) DelHeader(field string) string {
h.Header.Del(field)
return ""
}

// SetStatus sets the HTTP response status. It returns an empty string.
func (h *ResponseDot) SetStatus(status int) string {
func (h *responseDot) SetStatus(status int) string {
h.status = status
return ""
}

// ReturnStatus sets the HTTP response status and exits template rendering
// immediately.
func (h *ResponseDot) ReturnStatus(status int) (string, error) {
func (h *responseDot) ReturnStatus(status int) (string, error) {
h.status = status
return "", ReturnError{}
}
9 changes: 0 additions & 9 deletions funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ var xtemplateFuncs template.FuncMap = template.FuncMap{
"sanitizeHtml": funcSanitizeHtml,
"markdown": funcMarkdown,
"splitFrontMatter": funcSplitFrontMatter,
"abortWithStatus": funcAbortWithStatus,
"return": funcReturn,
"status": funcStatus,
"humanize": funcHumanize,
Expand Down Expand Up @@ -107,14 +106,6 @@ func funcReturn() (string, error) {
return "", ReturnError{}
}

// funcAbortWithStatus stops rendering the reponse template and immediately returns the status indicated.
// Example usage: `{{if not (fileExists $includeFile)}}{{abortWithStatus 404}}{{end}}`
func funcAbortWithStatus(statusCode int) (struct{}, error) {
return struct{}{}, NewHandlerError("abort", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(statusCode)
})
}

// See status.go
var validStatus = map[string]int{
"Continue": http.StatusContinue,
Expand Down
26 changes: 0 additions & 26 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,29 +222,3 @@ func negiotiateEncoding(acceptHeaders []string, encodings []encodingInfo) (*enco
}
return &encodings[maxqIdx], err
}

// HandlerError is a special error that hijacks xtemplate's normal response
// handling and passes response handling off to the ServeHTTP method on this
// error value instead.
type HandlerError interface {
Error() string
ServeHTTP(w http.ResponseWriter, r *http.Request)
}

var _ error = HandlerError(nil)
var _ http.Handler = HandlerError(nil)

// NewHandlerError returns a new HandlerError based on a string and a function
// that matches the ServeHTTP signature.
func NewHandlerError(name string, fn func(w http.ResponseWriter, r *http.Request)) HandlerError {
return funcHandlerError{name, fn}
}

type funcHandlerError struct {
name string
fn func(w http.ResponseWriter, r *http.Request)
}

func (fhe funcHandlerError) Error() string { return fhe.name }

func (fhe funcHandlerError) ServeHTTP(w http.ResponseWriter, r *http.Request) { fhe.fn(w, r) }
44 changes: 23 additions & 21 deletions providers/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ var _ encoding.TextUnmarshaler = &FSDot{}
var _ encoding.TextMarshaler = &FSDot{}

func (fs FSDot) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(fsDot{fs, log}), nil
return reflect.ValueOf(fsDot{fs, log, w, r}), nil
}

type fsDot struct {
fs fs.FS
log *slog.Logger
w http.ResponseWriter
r *http.Request
}

var bufPool = sync.Pool{
Expand Down Expand Up @@ -145,24 +147,24 @@ func (c *fsDot) FileExists(filename string) (bool, error) {
// ServeFile aborts execution of the template and instead responds to the
// request with the content of the file at path_
func (c *fsDot) ServeFile(path_ string) (string, error) {
return "", xtemplate.NewHandlerError("ServeFile", func(w http.ResponseWriter, r *http.Request) {
path_ = path.Clean(path_)

c.log.Debug("serving file response", slog.String("path", path_))

file, err := c.fs.Open(path_)
if err != nil {
c.log.Debug("failed to open file", slog.Any("error", err), slog.String("path", path_))
http.Error(w, "internal server error", 500)
return
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
c.log.Debug("error getting stat of file", slog.Any("error", err), slog.String("path", path_))
}

http.ServeContent(w, r, path_, stat.ModTime(), file.(io.ReadSeeker))
})
path_ = path.Clean(path_)

c.log.Debug("serving file response", slog.String("path", path_))

file, err := c.fs.Open(path_)
if err != nil {
return "", fmt.Errorf("failed to open file at path '%s': %w", path_, err)
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
c.log.Debug("error getting stat of file", slog.Any("error", err), slog.String("path", path_))
}

// TODO: Handle setting headers.

http.ServeContent(c.w, c.r, path_, stat.ModTime(), file.(io.ReadSeeker))

return "", xtemplate.ReturnError{}
}

0 comments on commit 2f5d02c

Please sign in to comment.