Skip to content

Commit

Permalink
Add public docs; factor builder to simplify instance; add to readme
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Mar 26, 2024
1 parent 2f5d02c commit abad36d
Show file tree
Hide file tree
Showing 15 changed files with 381 additions and 488 deletions.
336 changes: 160 additions & 176 deletions README.md

Large diffs are not rendered by default.

81 changes: 51 additions & 30 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ import (
"github.com/tdewolff/minify/v2"
)

type builder struct {
*Instance
InstanceStats
m *minify.M
routes []InstanceRoute
}

type InstanceStats struct {
Routes int
TemplateFiles int
TemplateDefinitions int
TemplateInitializers int
StaticFiles int
StaticFilesAlternateEncodings int
}

type InstanceRoute struct {
Pattern string
Handler http.Handler
}

type fileInfo struct {
identityPath, hash, contentType string
encodings []encodingInfo
Expand All @@ -41,9 +62,9 @@ var extensionContentTypes = map[string]string{
".csv": "text/csv",
}

func (x *Instance) addStaticFileHandler(path_ string) error {
func (b *builder) addStaticFileHandler(path_ string) error {
// Open and stat the file
fsfile, err := x.config.FS.Open(path_)
fsfile, err := b.config.TemplatesFS.Open(path_)
if err != nil {
return fmt.Errorf("failed to open static file '%s': %w", path_, err)
}
Expand All @@ -65,7 +86,7 @@ func (x *Instance) addStaticFileHandler(path_ string) error {
var reader io.Reader = fsfile
encoding = "identity"
var exists bool
file, exists = x.files[identityPath]
file, exists = b.files[identityPath]
if exists {
switch ext {
case ".gz":
Expand Down Expand Up @@ -115,24 +136,24 @@ func (x *Instance) addStaticFileHandler(path_ string) error {
file.encodings = []encodingInfo{{encoding: encoding, path: path_, size: size, modtime: stat.ModTime()}}

pattern := "GET " + identityPath
handler := staticFileHandler(x.config.FS, file)
if err = catch("add handler to servemux", func() { x.router.HandleFunc(pattern, handler) }); err != nil {
handler := staticFileHandler(b.config.TemplatesFS, file)
if err = catch("add handler to servemux", func() { b.router.HandleFunc(pattern, handler) }); err != nil {
return err
}
x.stats.StaticFiles += 1
x.stats.Routes += 1
x.files[identityPath] = file
x.routes = append(x.routes, InstanceRoute{pattern, handler})
b.StaticFiles += 1
b.Routes += 1
b.files[identityPath] = file
b.routes = append(b.routes, InstanceRoute{pattern, handler})

x.config.Logger.Debug("added static file handler", slog.String("path", identityPath), slog.String("filepath", path_), slog.String("contenttype", file.contentType), slog.Int64("size", size), slog.Time("modtime", stat.ModTime()), slog.String("hash", sri))
b.config.Logger.Debug("added static file handler", slog.String("path", identityPath), slog.String("filepath", path_), slog.String("contenttype", file.contentType), slog.Int64("size", size), slog.Time("modtime", stat.ModTime()), slog.String("hash", sri))
} else {
if file.hash != sri {
return fmt.Errorf("encoded file contents did not match original file '%s': expected %s, got %s", path_, file.hash, sri)
}
file.encodings = append(file.encodings, encodingInfo{encoding: encoding, path: path_, size: size, modtime: stat.ModTime()})
sort.Slice(file.encodings, func(i, j int) bool { return file.encodings[i].size < file.encodings[j].size })
x.stats.StaticFilesAlternateEncodings += 1
x.config.Logger.Debug("added static file encoding", slog.String("path", identityPath), slog.String("filepath", path_), slog.String("encoding", encoding), slog.Int64("size", size), slog.Time("modtime", stat.ModTime()))
b.StaticFilesAlternateEncodings += 1
b.config.Logger.Debug("added static file encoding", slog.String("path", identityPath), slog.String("filepath", path_), slog.String("encoding", encoding), slog.Int64("size", size), slog.Time("modtime", stat.ModTime()))
}
return nil
}
Expand All @@ -149,36 +170,36 @@ func catch(description string, fn func()) (err error) {

var routeMatcher *regexp.Regexp = regexp.MustCompile("^(GET|POST|PUT|PATCH|DELETE|SSE) (.*)$")

func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error {
content, err := fs.ReadFile(x.config.FS, path_)
func (b *builder) addTemplateHandler(path_ string) error {
content, err := fs.ReadFile(b.config.TemplatesFS, path_)
if err != nil {
return fmt.Errorf("could not read template file '%s': %v", path_, err)
}
if minify != nil {
content, err = minify.Bytes("text/html", content)
if b.m != nil {
content, err = b.m.Bytes("text/html", content)
if err != nil {
return fmt.Errorf("could not minify template file '%s': %v", path_, err)
}
}
path_ = path.Clean("/" + path_)
// parse each template file manually to have more control over its final
// names in the template namespace.
newtemplates, err := parse.Parse(path_, string(content), x.config.LDelim, x.config.RDelim, x.funcs, buliltinsSkeleton)
newtemplates, err := parse.Parse(path_, string(content), b.config.LDelim, b.config.RDelim, b.funcs, buliltinsSkeleton)
if err != nil {
return fmt.Errorf("could not parse template file '%s': %v", path_, err)
}
x.stats.TemplateFiles += 1
b.TemplateFiles += 1

// add parsed templates, register handlers
for name, tree := range newtemplates {
if x.templates.Lookup(name) != nil {
x.config.Logger.Debug("overriding named template '%s' with definition from file: %s", name, path_)
if b.templates.Lookup(name) != nil {
b.config.Logger.Debug("overriding named template '%s' with definition from file: %s", name, path_)
}
tmpl, err := x.templates.AddParseTree(name, tree)
tmpl, err := b.templates.AddParseTree(name, tree)
if err != nil {
return fmt.Errorf("could not add template '%s' from '%s': %v", name, path_, err)
}
x.stats.TemplateDefinitions += 1
b.TemplateDefinitions += 1

var pattern string
var handler http.HandlerFunc
Expand All @@ -189,7 +210,7 @@ func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error {
continue
}
// strip the extension from the handled path
routePath := strings.TrimSuffix(path_, x.config.TemplateExtension)
routePath := strings.TrimSuffix(path_, b.config.TemplateExtension)
// files named 'index' handle requests to the directory
if path.Base(routePath) == "index" {
routePath = path.Dir(routePath)
Expand All @@ -198,24 +219,24 @@ func (x *Instance) addTemplateHandler(path_ string, minify *minify.M) error {
routePath += "{$}"
}
pattern = "GET " + routePath
handler = bufferingTemplateHandler(x, tmpl)
handler = bufferingTemplateHandler(b.Instance, tmpl)
} else if matches := routeMatcher.FindStringSubmatch(name); len(matches) == 3 {
method, path_ := matches[1], matches[2]
if method == "SSE" {
pattern = "GET " + path_
handler = flushingTemplateHandler(x, tmpl)
handler = flushingTemplateHandler(b.Instance, tmpl)
} else {
pattern = method + " " + path_
handler = bufferingTemplateHandler(x, tmpl)
handler = bufferingTemplateHandler(b.Instance, tmpl)
}
}

if err = catch("add handler to servemux", func() { x.router.HandleFunc(pattern, handler) }); err != nil {
if err = catch("add handler to servemux", func() { b.router.HandleFunc(pattern, handler) }); err != nil {
return err
}
x.routes = append(x.routes, InstanceRoute{pattern, handler})
x.stats.Routes += 1
x.config.Logger.Debug("added template handler", "method", "GET", "pattern", pattern, "template_path", path_)
b.routes = append(b.routes, InstanceRoute{pattern, handler})
b.Routes += 1
b.config.Logger.Debug("added template handler", "method", "GET", "pattern", pattern, "template_path", path_)
}
return nil
}
11 changes: 6 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ func main() {
// Provide configs to override the defaults like: `xtemplate.Main(xtemplate.WithFooConfig())`
func Main(overrides ...xtemplate.ConfigOverride) {
var args struct {
xtemplate.Config
Watch []string
WatchTemplates bool `default:"true"`
Listen string `arg:"-l" default:"0.0.0.0:8080"`
xtemplate.Config `arg:"-w"`
Watch []string
WatchTemplates bool `default:"true"`
Listen string `arg:"-l" default:"0.0.0.0:8080"`
LogLevel int `default:"-2"`
}
arg.MustParse(&args)
args.Defaults()

log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.Level(args.Config.LogLevel)}))
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.Level(args.LogLevel)}))
args.Config.Logger = log

for _, o := range overrides {
Expand Down
37 changes: 26 additions & 11 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,37 @@ func New() (c *Config) {
}

type Config struct {
// The FS to load templates from. Overrides Path if not nil.
FS fs.FS `json:"-" arg:"-"`

// The path to the templates directory.
// The path to the templates directory. Default `templates`.
TemplatesDir string `json:"templates_dir,omitempty" arg:"-t,--template-dir" default:"templates"`

// The FS to load templates from. Overrides Path if not nil.
TemplatesFS fs.FS `json:"-" arg:"-"`

// File extension to search for to find template files. Default `.html`.
TemplateExtension string `json:"template_extension,omitempty" arg:"--template-ext" default:".html"`

// The template action delimiters, default "{{" and "}}".
// Left template action delimiter. Default `{{`.
LDelim string `json:"left,omitempty" arg:"--ldelim" default:"{{"`

// Right template action delimiter. Default `}}`.
RDelim string `json:"right,omitempty" arg:"--rdelim" default:"}}"`

// Minify html templates at load time.
// Whether html templates are minified at load time. Default `true`.
Minify bool `json:"minify,omitempty" arg:"-m,--minify" default:"true"`

Dot []DotConfig `json:"dot_config" arg:"-c,--dot-config,separate"`
// A list of additional custom fields to add to the template dot value
// `{{.}}`.
Dot []DotConfig `json:"dot_config" arg:"-d,--dot,separate"`

// Additional functions to add to the template execution context.
FuncMaps []template.FuncMap `json:"-" arg:"-"`
Ctx context.Context `json:"-" arg:"-"`

Logger *slog.Logger `json:"-" arg:"-"`
LogLevel int `json:"log_level,omitempty"`
// The instance context that is threaded through dot providers and can
// cancel the server. Defaults to `context.Background()`.
Ctx context.Context `json:"-" arg:"-"`

// The default logger. Defaults to `slog.Default()`.
Logger *slog.Logger `json:"-" arg:"-"`
}

// FillDefaults sets default values for unset fields
Expand All @@ -60,14 +67,22 @@ func (config *Config) Defaults() *Config {
config.RDelim = "}}"
}

if config.Logger == nil {
config.Logger = slog.Default()
}

if config.Ctx == nil {
config.Ctx = context.Background()
}

return config
}

type ConfigOverride func(*Config)

func WithTemplateFS(fs fs.FS) ConfigOverride {
return func(c *Config) {
c.FS = fs
c.TemplatesFS = fs
}
}

Expand Down
24 changes: 12 additions & 12 deletions dot_flush.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,37 @@ import (
"time"
)

type flushDotProvider struct{}
type dotFlushProvider struct{}

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

func (flushDotProvider) Value(_ *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
func (dotFlushProvider) Value(_ *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
f, ok := w.(http.Flusher)
if !ok {
return reflect.Value{}, fmt.Errorf("response writer could not cast to http.Flusher")
}
return reflect.ValueOf(FlushDot{flusher: f, serverCtx: sctx, requestCtx: r.Context()}), nil
return reflect.ValueOf(DotFlush{flusher: f, serverCtx: sctx, requestCtx: r.Context()}), nil
}

func (flushDotProvider) Cleanup(v reflect.Value, err error) {
func (dotFlushProvider) Cleanup(v reflect.Value, err error) {
if err == nil {
v.Interface().(FlushDot).flusher.Flush()
v.Interface().(DotFlush).flusher.Flush()
}
}

var _ DotProvider = flushDotProvider{}
var _ DotProvider = dotFlushProvider{}

type FlushDot struct {
type DotFlush struct {
flusher http.Flusher
serverCtx, requestCtx context.Context
}

func (f FlushDot) Flush() string {
func (f DotFlush) Flush() string {
f.flusher.Flush()
return ""
}

func (f FlushDot) Repeat(max_ ...int) <-chan int {
func (f DotFlush) Repeat(max_ ...int) <-chan int {
max := math.MaxInt64 // sorry you can only loop for 2^63-1 iterations max
if len(max_) > 0 {
max = max_[0]
Expand Down Expand Up @@ -68,7 +68,7 @@ func (f FlushDot) Repeat(max_ ...int) <-chan int {
}

// Sleep sleeps for ms millisecionds.
func (f FlushDot) Sleep(ms int) (string, error) {
func (f DotFlush) Sleep(ms int) (string, error) {
select {
case <-time.After(time.Duration(ms) * time.Millisecond):
case <-f.requestCtx.Done():
Expand All @@ -81,7 +81,7 @@ func (f FlushDot) Sleep(ms int) (string, error) {

// Block blocks execution until the request is canceled by the client or until
// the server closes.
func (f FlushDot) WaitForServerStop() (string, error) {
func (f DotFlush) WaitForServerStop() (string, error) {
select {
case <-f.requestCtx.Done():
return "", ReturnError{}
Expand Down
Loading

0 comments on commit abad36d

Please sign in to comment.