Skip to content

Commit

Permalink
Passes tests
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Mar 24, 2024
1 parent a49d88d commit 1f1f3f6
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 115 deletions.
8 changes: 6 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// import dbs and provide config overrides.
package main

import "github.com/infogulch/xtemplate"
import (
"os"

"github.com/infogulch/xtemplate"
)

func main() {
xtemplate.Main( /* Main accepts some configuration overrides, see ../config.go */ )
xtemplate.Main(xtemplate.WithContextFS(os.DirFS("./context")))
}
178 changes: 112 additions & 66 deletions dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Needs to get configuration from:
*/

type DotProvider interface {
Type() reflect.Type
Value(request_scoped_logger *slog.Logger, server_ctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error)
}

Expand All @@ -74,89 +75,90 @@ type DotConfig struct {
Name string
}

func NewStructDotProvider(dcs []DotConfig) CleanupDotProvider {
cdcs := []DotConfig{}
for _, dc := range dcs {
if _, ok := dc.DotProvider.(CleanupDotProvider); ok {
cdcs = append(cdcs, dc)
func newDot(dcs []DotConfig) *dot {
fields := make([]reflect.StructField, 0, len(dcs))
cleanups := []cleanup{}
for i, dc := range dcs {
f := reflect.StructField{
Name: dc.Name,
Type: dc.DotProvider.Type(),
Anonymous: false, // alas
}
if f.Name == "" {
f.Name = f.Type.Name()
}
fields = append(fields, f)
if cdp, ok := dc.DotProvider.(CleanupDotProvider); ok {
cleanups = append(cleanups, cleanup{i, cdp})
}
}
n := len(dcs)
return &structDotProvider{dcs, cdcs, &sync.Pool{New: func() any { return make(map[string]reflect.Value, n) }}}
typ := reflect.StructOf(fields)
return &dot{dcs, cleanups, &sync.Pool{New: func() any { v := reflect.New(typ).Elem(); return &v }}}
}

type structDotProvider struct {
dcs []DotConfig
cdcs []DotConfig
pool *sync.Pool
type dot struct {
dcs []DotConfig
cleanups []cleanup
pool *sync.Pool
}

func (dp *structDotProvider) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
val := dp.pool.Get().(map[string]reflect.Value)
for _, dc := range dp.dcs {
a, err := dc.Value(log, sctx, w, r)
type cleanup struct {
idx int
CleanupDotProvider
}

func (d *dot) value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (val *reflect.Value, err error) {
val = d.pool.Get().(*reflect.Value)
val.SetZero()
for i, dc := range d.dcs {
var v reflect.Value
v, err = dc.Value(log, sctx, w, r)
if err != nil {
return reflect.Value{}, fmt.Errorf("failed to construct dot value for %s (%v): %w", dc.Name, dc.DotProvider, err)
}
if a == (reflect.Value{}) {
log.Debug("dot provider returned nil value", slog.String("provider_name", dc.Name))
err = fmt.Errorf("failed to construct dot value for %s (%v): %w", dc.Name, dc.DotProvider, err)
v.SetZero()
d.pool.Put(val)
val = nil
return
}
val[dc.Name] = a
val.Field(i).Set(v)
}
return reflect.ValueOf(val), nil
return
}

func (dp *structDotProvider) Cleanup(v reflect.Value, err error) error {
val := v.Interface().(map[string]reflect.Value)
for _, cdc := range dp.cdcs {
err = errors.Join(cdc.DotProvider.(CleanupDotProvider).Cleanup(val[cdc.Name], err))
func (d *dot) cleanup(v *reflect.Value, err error) error {
for _, cleanup := range d.cleanups {
err = cleanup.Cleanup(v.Field(cleanup.idx), err)
}
clear(val)
dp.pool.Put(val)
v.SetZero()
d.pool.Put(v)
return err
}

var _ CleanupDotProvider = &structDotProvider{}

type requestDotProvider struct{}

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

var _ DotProvider = requestDotProvider{}

type requestDot struct {
*http.Request
}

type instanceDotProvider struct {
instance *Instance
}

func (instanceDotProvider) Type() reflect.Type { return reflect.TypeOf(InstanceDot{}) }

func (p instanceDotProvider) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(InstanceDot{p.instance, log}), nil
return reflect.ValueOf(InstanceDot{instance: p.instance}), nil
}

var _ DotProvider = instanceDotProvider{}

type InstanceDot struct {
instance *Instance
log *slog.Logger
func (instanceDotProvider) Cleanup(_ reflect.Value, err error) error {
if errors.As(err, &ReturnError{}) {
return nil
}
return err
}

// ServeContent aborts execution of the template and instead responds to the request with content
func (d *InstanceDot) 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_))
var _ CleanupDotProvider = instanceDotProvider{}

http.ServeContent(w, r, path_, modtime, strings.NewReader(content))
})
type InstanceDot struct {
instance *Instance
_ int // required ???
}

func (d *InstanceDot) StaticFileHash(urlpath string) (string, error) {
func (d InstanceDot) StaticFileHash(urlpath string) (string, error) {
urlpath = path.Clean("/" + urlpath)
fileinfo, ok := d.instance.files[urlpath]
if !ok {
Expand All @@ -165,7 +167,7 @@ func (d *InstanceDot) StaticFileHash(urlpath string) (string, error) {
return fileinfo.hash, nil
}

func (c *InstanceDot) Template(name string, context any) (string, error) {
func (c InstanceDot) Template(name string, context any) (string, error) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
Expand All @@ -180,23 +182,46 @@ func (c *InstanceDot) Template(name string, context any) (string, error) {
return buf.String(), nil
}

func (c *InstanceDot) Func(name string) any {
func (c InstanceDot) Func(name string) any {
return c.instance.funcs[name]
}

type requestDotProvider struct{}

func (requestDotProvider) Type() reflect.Type { return reflect.TypeOf(requestDot{}) }

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

var _ DotProvider = requestDotProvider{}

type requestDot struct {
*http.Request
}

type responseDotProvider struct{}

func (responseDotProvider) Value(_ *slog.Logger, _ context.Context, w http.ResponseWriter, _ *http.Request) (reflect.Value, error) {
return reflect.ValueOf(ResponseDot{Header: make(http.Header), status: http.StatusOK, w: w}), nil
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
}

func (responseDotProvider) Cleanup(v reflect.Value, err error) error {
val := v.Interface().(ResponseDot)
if err != nil {
maps.Copy(val.w.Header(), val.Header)
val.w.WriteHeader(val.status)
d := v.Interface().(ResponseDot)
var handlerErr HandlerError
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{}
Expand All @@ -205,6 +230,19 @@ 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))
})
}

// AddHeader adds a header field value, appending val to
Expand Down Expand Up @@ -244,6 +282,8 @@ func (h *ResponseDot) ReturnStatus(status int) (string, error) {

type flushDotProvider struct{}

func (flushDotProvider) Type() reflect.Type { return reflect.TypeOf(FlushDot{}) }

func (flushDotProvider) Value(_ *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
f, ok := w.(http.Flusher)
if !ok {
Expand Down Expand Up @@ -324,6 +364,8 @@ type fsDotProvider struct {
fs fs.FS
}

func (fsDotProvider) Type() reflect.Type { return reflect.TypeOf(FsDot{}) }

func (fs fsDotProvider) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(FsDot{fs: fs.fs, log: log}), nil
}
Expand Down Expand Up @@ -438,6 +480,8 @@ type ConfigDotProvider struct {
config map[string]string
}

func (ConfigDotProvider) Type() reflect.Type { return reflect.TypeOf(ConfigDotProvider{}) }

func (c ConfigDotProvider) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(c), nil
}
Expand All @@ -453,12 +497,14 @@ type sqlDotProvider struct {
opt *sql.TxOptions
}

func (sqlDotProvider) Type() reflect.Type { return reflect.TypeOf(SqlDot{}) }

func (d *sqlDotProvider) Value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(SqlDot{d.db, log, r.Context(), d.opt, nil}), nil
}

func (dp *sqlDotProvider) Cleanup(v reflect.Value, err error) error {
d := v.Interface().(SqlDot)
d := v.Interface().(*SqlDot)
if err != nil {
return errors.Join(err, d.rollback())
} else {
Expand Down
3 changes: 3 additions & 0 deletions funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ var xtemplateFuncs template.FuncMap = template.FuncMap{
"ksuid": funcKsuid,
"idx": funcIdx,
"try": funcTry,
"debug": func() string {
return ""
},
}

var blueMondayPolicies map[string]*bluemonday.Policy = map[string]*bluemonday.Policy{
Expand Down
37 changes: 7 additions & 30 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package xtemplate

import (
"bytes"
"errors"
"fmt"
"html/template"
"io"
Expand All @@ -28,7 +27,7 @@ func bufferingTemplateHandler(server *Instance, tmpl *template.Template) http.Ha
return func(w http.ResponseWriter, r *http.Request) {
log := getCtxLogger(r)

dot, err := server.bufferDot.Value(log, server.config.Ctx, w, r)
dot, err := server.bufferDot.value(log, server.config.Ctx, w, r)
if err != nil {
log.Error("failed to initialize dot value: %w", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
Expand All @@ -39,30 +38,14 @@ func bufferingTemplateHandler(server *Instance, tmpl *template.Template) http.Ha
buf.Reset()
defer bufPool.Put(buf)

err = tmpl.Execute(buf, dot)
err = tmpl.Execute(buf, *dot)

// Handle sentinel errors
if errors.As(err, &ReturnError{}) {
err = nil
}
var handlerErr HandlerError
if errors.As(err, &handlerErr) {
err = nil
}

// Run cleanup routines
if err = server.bufferDot.Cleanup(dot, err); err != nil {
log.Info("error executing template", slog.Any("error", err))
if err = server.bufferDot.cleanup(dot, err); err != nil {
log.Warn("error executing template", slog.Any("error", err))
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}

if handlerErr != nil {
log.Debug("forwarding response handling", slog.Any("handler", handlerErr))
handlerErr.ServeHTTP(w, r)
return
}

w.Write(buf.Bytes())
}
}
Expand All @@ -80,22 +63,16 @@ func flushingTemplateHandler(server *Instance, tmpl *template.Template) http.Han
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

dot, err := server.flusherDot.Value(log, server.config.Ctx, w, r)
dot, err := server.flusherDot.value(log, server.config.Ctx, w, r)
if err != nil {
log.Error("failed to initialize dot value: %w", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}

err = tmpl.Execute(w, dot)

// Handle sentinel error
if errors.As(err, &ReturnError{}) {
err = nil
}
err = tmpl.Execute(w, *dot)

// Run cleanup routines
if err = server.bufferDot.Cleanup(dot, err); err != nil {
if err = server.flusherDot.cleanup(dot, err); err != nil {
log.Info("error executing template", slog.Any("error", err))
http.Error(w, "internal server error", http.StatusInternalServerError)
return
Expand Down
Loading

0 comments on commit 1f1f3f6

Please sign in to comment.