Skip to content

Commit

Permalink
Simplify dotprovider interface; streamline funcs section in readme
Browse files Browse the repository at this point in the history
  • Loading branch information
infogulch committed Mar 27, 2024
1 parent fbf7841 commit a0dd211
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 244 deletions.
139 changes: 20 additions & 119 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ everything would just get out of the way of the fundamentals:
🎇 **The idea of `xtemplate` is that *templates* can be the nexus of these
fundamentals.**

## 🚫 Anti-goals
<details><summary>🚫 Anti-goals</summary>

`xtemplate` needs to implement some of the things that are required to make a
good web server in a way that avoids common issues with existing web server
Expand All @@ -45,6 +45,8 @@ designs, otherwise they'll be in the way of the fundamentals:
hash of asset files, depriving clients of enough information to optimize
caching behavior and check resource integrity.

</details>

## ✨ Features

*Click a feature to expand and show details:*
Expand Down Expand Up @@ -411,7 +413,8 @@ configured multiple times with different configurations.
#### ✏️ Custom dot fields
You can create custom dot fields by
You can create custom dot fields that expose arbitrary Go functionality to your
templates. See [👩‍⚕️ Writing a custom `DotProvider`](#-writing-a-custom-dotprovider).
### 📐 Functions
Expand All @@ -420,123 +423,21 @@ depend on request context or mutate state. There are three sets by default:
functions that come by default in the go template library, functions from the
sprig library, and custom functions added by xtemplate.
You can custom FuncMaps by setting `config.FuncMaps = myFuncMap` or calling
`xtemplate.Main(xtemplate.WithFuncMaps(myFuncMap))`.
<details><summary><strong>xtemplate functions</strong></summary>
See [funcs.go](/funcs.go) for details.
- `markdown` Renders the given Markdown text as HTML and returns it. This uses the [Goldmark](https://github.com/yuin/goldmark) library, which is CommonMark compliant. It also has these extensions enabled: Github Flavored Markdown, Footnote, and syntax highlighting provided by [Chroma](https://github.com/alecthomas/chroma).
- `splitFrontMatter` Splits front matter out from the body. Front matter is metadata that appears at the very beginning of a file or string. Front matter can be in YAML, TOML, or JSON formats.
- `.Meta` to access the metadata fields, for example: `{{$parsed.Meta.title}}`
- `.Body` to access the body after the front matter, for example: `{{markdown $parsed.Body}}`
- `sanitizeHtml` Uses [bluemonday](https://github.com/microcosm-cc/bluemonday/) to sanitize strings with html content. `{{sanitizeHtml "strict" "Shows <b>only</b> text content"}}`
- First parameter is the name of the chosen sanitization policy. `"strict"` = [`StrictPolicy()`](https://github.com/microcosm-cc/bluemonday/blob/main/policies.go#L38C6-L38C20), `"ugc"` = [`UGCPolicy()`](https://github.com/microcosm-cc/bluemonday/blob/main/policies.go#L54C6-L54C17) for 'user generated content', `"externalugc"` = `UGCPolicy()` + disallow relative urls + add target=_blank to urls.
- Second parameter is the content to sanitize.
- Returns the string as a `template.HTML` type which can be output directly into the document without `trustHtml`.
- `humanize` Transforms size and time inputs to a human readable format using the [go-humanize](https://github.com/dustin/go-humanize) library. Call with two parameters, the format type and the value to format. Format types are:
- **size** which turns an integer amount of bytes into a string like `2.3 MB`, for example: `{{humanize "size" "2048000"}}`
- **time** which turns a time string into a relative time string like `2 weeks ago`, for example: `{{humanize "time" "Fri, 05 May 2022 15:04:05 +0200"}}`
- `ksuid` returns a 'K-Sortable Globally Unique ID' using [segmentio/ksuid](https://github.com/segmentio/ksuid)
- `idx` gets an item from a list, similar to the built-in `index`, but with reversed args: index first, then array. This is useful to use index in a pipeline, for example: `{{generate-list | idx 5}}`
- `try` takes a function that returns an error in the first argument and calls it with the values from the remaining arguments, and returns the result including any error as struct fields. This enables template authors to handle funcs that return errors within the template definition. Example: `{{ $result := try .QueryVal "SELECT 'oops' WHERE 1=0" }}{{if $result.OK}}{{$result.Value}}{{else}}QueryVal requires exactly one row. Error: {{$result.Error}}{{end}}`
</details>
<details><summary><strong>Go stdlib template functions</strong></summary>
See [text/template#Functions](https://pkg.go.dev/text/template#hdr-Functions).
- `and`
Returns the boolean AND of its arguments by returning the
first empty argument or the last argument. That is,
"and x y" behaves as "if x then y else x."
Evaluation proceeds through the arguments left to right
and returns when the result is determined.
- `call`
Returns the result of calling the first argument, which
must be a function, with the remaining arguments as parameters.
Thus "call .X.Y 1 2" is, in Go notation, dot.X.Y(1, 2) where
Y is a func-valued field, map entry, or the like.
The first argument must be the result of an evaluation
that yields a value of function type (as distinct from
a predefined function such as print). The function must
return either one or two result values, the second of which
is of type error. If the arguments don't match the function
or the returned error value is non-nil, execution stops.
- `html`
Returns the escaped HTML equivalent of the textual
representation of its arguments. This function is unavailable
in html/template, with a few exceptions.
- `index`
Returns the result of indexing its first argument by the
following arguments. Thus "index x 1 2 3" is, in Go syntax,
x[1][2][3]. Each indexed item must be a map, slice, or array.
- `slice`
slice returns the result of slicing its first argument by the
remaining arguments. Thus "slice x 1 2" is, in Go syntax, x[1:2],
while "slice x" is x[:], "slice x 1" is x[1:], and "slice x 1 2 3"
is x[1:2:3]. The first argument must be a string, slice, or array.
- `js`
Returns the escaped JavaScript equivalent of the textual
representation of its arguments.
- `len`
Returns the integer length of its argument.
- `not`
Returns the boolean negation of its single argument.
- `or`
Returns the boolean OR of its arguments by returning the
first non-empty argument or the last argument, that is,
"or x y" behaves as "if x then x else y".
Evaluation proceeds through the arguments left to right
and returns when the result is determined.
- `print`
An alias for fmt.Sprint
- `printf`
An alias for fmt.Sprintf
- `println`
An alias for fmt.Sprintln
- `urlquery`
Returns the escaped value of the textual representation of
its arguments in a form suitable for embedding in a URL query.
This function is unavailable in html/template, with a few
exceptions.
</details>
<details><summary><strong>Sprig library template functions</strong></summary>
See the Sprig documentation for details: [Sprig Function Documentation](https://masterminds.github.io/sprig/).
- [String Functions](https://masterminds.github.io/sprig/strings.html):
- `trim`, `trimAll`, `trimSuffix`, `trimPrefix`, `repeat`, `substr`, `replace`, `shuffle`, `nospace`, `trunc`, `abbrev`, `abbrevboth`, `wrap`, `wrapWith`, `quote`, `squote`, `cat`, `indent`, `nindent`
- `upper`, `lower`, `title`, `untitle`, `camelcase`, `kebabcase`, `swapcase`, `snakecase`, `initials`, `plural`
- `contains`, `hasPrefix`, `hasSuffix`
- `randAlphaNum`, `randAlpha`, `randNumeric`, `randAscii`
- `regexMatch`, `mustRegexMatch`, `regexFindAll`, `mustRegexFindAll`, `regexFind`, `mustRegexFind`, `regexReplaceAll`, `mustRegexReplaceAll`, `regexReplaceAllLiteral`, `mustRegexReplaceAllLiteral`, `regexSplit`, `mustRegexSplit`, `regexQuoteMeta`
* [String List Functions](https://masterminds.github.io/sprig/strings.html): `splitList`, `sortAlpha`, etc.
- [Integer Math Functions](https://masterminds.github.io/sprig/math.html): `add`, `max`, `mul`, etc.
- [Integer Slice Functions](https://masterminds.github.io/sprig/integer_slice.html): `until`, `untilStep`
- [Float Math Functions](https://masterminds.github.io/sprig/mathf.html): `addf`, `maxf`, `mulf`, etc.
- [Date Functions](https://masterminds.github.io/sprig/date.html): `now`, `date`, etc.
- [Defaults Functions](https://masterminds.github.io/sprig/defaults.html): `default`, `empty`, `coalesce`, `fromJson`, `toJson`, `toPrettyJson`, `toRawJson`, `ternary`
- [Encoding Functions](https://masterminds.github.io/sprig/encoding.html): `b64enc`, `b64dec`, etc.
- [Lists and List Functions](https://masterminds.github.io/sprig/lists.html): `list`, `first`, `uniq`, etc.
- [Dictionaries and Dict Functions](https://masterminds.github.io/sprig/dicts.html): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, `deepCopy`, etc.
- [Type Conversion Functions](https://masterminds.github.io/sprig/conversion.html): `atoi`, `int64`, `toString`, etc.
- [Path and Filepath Functions](https://masterminds.github.io/sprig/paths.html): `base`, `dir`, `ext`, `clean`, `isAbs`, `osBase`, `osDir`, `osExt`, `osClean`, `osIsAbs`
- [Flow Control Functions](https://masterminds.github.io/sprig/flow_control.html): `fail`
- Advanced Functions
- [UUID Functions](https://masterminds.github.io/sprig/uuid.html): `uuidv4`
- [OS Functions](https://masterminds.github.io/sprig/os.html): `env`, `expandenv`
- [Version Comparison Functions](https://masterminds.github.io/sprig/semver.html): `semver`, `semverCompare`
- [Reflection](https://masterminds.github.io/sprig/reflection.html): `typeOf`, `kindIs`, `typeIsLike`, etc.
- [Cryptographic and Security Functions](https://masterminds.github.io/sprig/crypto.html): `derivePassword`, `sha256sum`, `genPrivateKey`, etc.
- [Network](https://masterminds.github.io/sprig/network.html): `getHostByName`
</details>
You can custom FuncMaps by configuring the `Config.FuncMaps` field.
* 📏 `xtemplate` includes funcs to render markdown, sanitize html, convert
values to human-readable forms, and to try to call a function to handle an
error within the template. See the free functions named `FuncXYZ(...)` in
xtemplate's Go docs for details.
* 📏 Sprig publishes a library of useful template funcs that enable templates to
manipulate strings, integers, floating point numbers, and dates, as well as
perform encoding tasks, manipulate lists and dicts, converting types,
and manipulate file paths See [Sprig Function Documentation](sprig).
* 📏 Go's built in functions add logic and basic printing functionality.
See: [text/template#Functions](gofuncs).
[sprig]: https://masterminds.github.io/sprig/
[gofuncs]: https://pkg.go.dev/text/template#hdr-Functions
## 🏆 Users
Expand Down
27 changes: 15 additions & 12 deletions dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"context"
"encoding"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"reflect"
"slices"
"sync"
Expand All @@ -23,11 +23,9 @@ func RegisterDot(r RegisteredDotProvider) {
}

type DotProvider interface {
Type() reflect.Type

// Value should always return a valid instance of the provided type, even if
// it also returns an error.
Value(request_scoped_logger *slog.Logger, server_ctx context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error)
Value(server_ctx context.Context, w http.ResponseWriter, r *http.Request) (any, error)
}

type RegisteredDotProvider interface {
Expand All @@ -38,7 +36,7 @@ type RegisteredDotProvider interface {

type CleanupDotProvider interface {
DotProvider
Cleanup(reflect.Value, error) error
Cleanup(any, error) error
}

type DotConfig struct {
Expand Down Expand Up @@ -95,9 +93,14 @@ func makeDot(dcs []DotConfig) dot {
fields := make([]reflect.StructField, 0, len(dcs))
cleanups := []cleanup{}
for i, dc := range dcs {
a, _ := dc.DotProvider.Value(context.Background(), nil, httptest.NewRequest("GET", "/", nil))
t := reflect.TypeOf(a)
if t.Kind() == reflect.Interface && t.NumMethod() == 0 {
t = t.Elem()
}
f := reflect.StructField{
Name: dc.Name,
Type: dc.DotProvider.Type(),
Type: t,
Anonymous: false, // alas
}
if f.Name == "" {
Expand All @@ -123,27 +126,27 @@ type cleanup struct {
CleanupDotProvider
}

func (d *dot) value(log *slog.Logger, sctx context.Context, w http.ResponseWriter, r *http.Request) (val *reflect.Value, err error) {
func (d *dot) value(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)
var a any
a, err = dc.Value(sctx, w, r)
if err != nil {
err = fmt.Errorf("failed to construct dot value for %s (%v): %w", dc.Name, dc.DotProvider, err)
v.SetZero()
val.SetZero()
d.pool.Put(val)
val = nil
return
}
val.Field(i).Set(v)
val.Field(i).Set(reflect.ValueOf(a))
}
return
}

func (d *dot) cleanup(v *reflect.Value, err error) error {
for _, cleanup := range d.cleanups {
err = cleanup.Cleanup(v.Field(cleanup.idx), err)
err = cleanup.Cleanup(v.Field(cleanup.idx).Interface(), err)
}
v.SetZero()
d.pool.Put(v)
Expand Down
17 changes: 7 additions & 10 deletions dot_flush.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,29 @@ package xtemplate
import (
"context"
"fmt"
"log/slog"
"math"
"net/http"
"reflect"
"time"
)

type dotFlushProvider struct{}

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

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

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

var _ DotProvider = dotFlushProvider{}
var _ CleanupDotProvider = dotFlushProvider{}

// DotFlush is used as the `.Flush` field for flushing template handlers (SSE).
type DotFlush struct {
Expand Down
10 changes: 3 additions & 7 deletions dot_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@ import (
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"path"
"reflect"
)

type dotXProvider struct {
instance *Instance
}

func (dotXProvider) Type() reflect.Type { return reflect.TypeOf(DotX{}) }

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

func (dotXProvider) Cleanup(_ reflect.Value, err error) error {
func (dotXProvider) Cleanup(_ any, err error) error {
if errors.As(err, &ReturnError{}) {
return nil
}
Expand Down
8 changes: 2 additions & 6 deletions dot_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ package xtemplate

import (
"context"
"log/slog"
"net/http"
"reflect"
)

type dotReqProvider struct{}

func (dotReqProvider) Type() reflect.Type { return reflect.TypeOf(DotReq{}) }

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

var _ DotProvider = dotReqProvider{}
Expand Down
12 changes: 5 additions & 7 deletions dot_resp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@ import (
"maps"
"net/http"
"path"
"reflect"
"strings"
"time"
)

type dotRespProvider struct{}

func (dotRespProvider) Type() reflect.Type { return reflect.TypeOf(DotResp{}) }

func (dotRespProvider) Value(log *slog.Logger, _ context.Context, w http.ResponseWriter, r *http.Request) (reflect.Value, error) {
return reflect.ValueOf(DotResp{Header: make(http.Header), status: http.StatusOK, w: w, r: r, log: log}), nil
func (dotRespProvider) Value(_ context.Context, w http.ResponseWriter, r *http.Request) (any, error) {
log := GetCtxLogger(r)
return DotResp{Header: make(http.Header), status: http.StatusOK, w: w, r: r, log: log}, nil
}

func (dotRespProvider) Cleanup(v reflect.Value, err error) error {
d := v.Interface().(DotResp)
func (dotRespProvider) Cleanup(v any, err error) error {
d := v.(DotResp)
if err == nil {
maps.Copy(d.w.Header(), d.Header)
d.w.WriteHeader(d.status)
Expand Down
Loading

0 comments on commit a0dd211

Please sign in to comment.