Skip to content

Commit

Permalink
feat: added methods that allow a template to be rendered multiple tim…
Browse files Browse the repository at this point in the history
…es with accumulated data from each run
  • Loading branch information
TristanSpeakEasy committed Nov 10, 2023
1 parent 7f9b227 commit 4ec21c3
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 67 deletions.
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ linters:
- golint
- maligned
- gci
- depguard
# deprecated/archived
- interfacer
- scopelint
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ A number of methods are available to start the engine, including:
* `templateFile` (string) - The path to the template file to start the engine from.
* `data` (any) - Context data to provide to templates and scripts. Available as `{{.Global}}` in templates and `context.Global` in scripts.

Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument),
each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be
found in the `testdata/templates/testMultiple.stmpl` file.

### Controlling the flow of templating

The engine allows you to control the flow of templating from within templates and scripts themselves. This means from a single entry point you can start multiple templates and scripts.
Expand All @@ -145,12 +149,21 @@ This is done by calling the following functions from within templates and script
* `templateString(templateFile string, data any) (string, error)` - Start a template file and return the rendered template as a string.
* `templateFile` (string) - The path to the template file to start the engine from.
* `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts.
* `templateStringInput(templateName string, templateString string, data any) (string, error)` - Template the input string and return the rendered template as a string.
* `templateName` (string) - The name of the template to render.
* `templateString` (string) - An input template string to template.
* `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts.

Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument),
each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be
found in the `testdata/templates/testMultiple.stmpl` file.

This allows for example:

```gotemplate
{{ templateFile "tmpl.stmpl" "out.txt" .Local }}{{/* Template another file */}}
{{ templateString "tmpl.stmpl" .Local }}{{/* Template another file and include the rendered output in this templates rendered output */}}
{{ templateStringInput "Hello {{ .Local.name }}" .Local }}{{/* Template a string and include the rendered output in this templates rendered output */}}
```

### Registering templating functions
Expand Down Expand Up @@ -184,8 +197,9 @@ sjs```
The `sjs` snippet can be used anywhere within your template (including multiple snippets) and will be replaced with any "rendered" output returned when using the `render` function.

Naive transformation of typescript code is supported through [esbuild](https://esbuild.github.io/api/#transformation). This means that you can directly import typescript code and use type annotations in place of any JavaScript. However, be aware:
* EasyTemplate will not perform type checking itself. Type annotations are transformed into commented out code.
* Scripts/Snippets are not bundled, but executed as a single module on the global scope. This means no `import` statements are possible. [Instead, the global `require` function](#importing-javascript) is available to directly execute JS/TS code.

* EasyTemplate will not perform type checking itself. Type annotations are transformed into commented out code.
* Scripts/Snippets are not bundled, but executed as a single module on the global scope. This means no `import` statements are possible. [Instead, the global `require` function](#importing-javascript) is available to directly execute JS/TS code.

### Context data

Expand Down Expand Up @@ -305,10 +319,18 @@ The following functions are available to JavaScript from the templating engine:
* `templateString(templateString, data)` - Render a template and return the rendered output.
* `templateString` (string) - The template string to render.
* `data` (object) - Data available to the template as `Local` context ie `{name: "John"}` is available as `{{ .Local.name }}`.
* `templateStringInput(templateName, templateString, data)` - Render a template and return the rendered output.
* `templateName` (string) - The name of the template to render.
* `templateString` (string) - The template string to render.
* `data` (object) - Data available to the template as `Local` context ie `{name: "John"}` is available as `{{ .Local.name }}`.
* `render(output)` - Render the output to the template file, if called multiples times the output will be appended to the previous output as a new line. The cumulative output will replace the current `sjs` block in the template file.
* `output` (string) - The output to render.
* `require(filePath)` - Import a JavaScript file into the global scope.
* `filePath` (string) - The path to the JavaScript file to import.
* `registerTemplateFunc(name, func)` - Register a template function to be used in the template files.
* `name` (string) - The name of the function to register.
* `func` (function) - The function to register.

Note: there are also versions of the above template methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument),
each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be
found in the `testdata/templates/testMultiple.stmpl` file.
82 changes: 74 additions & 8 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ func New(opts ...Opt) *Engine {
t.ReadFunc = e.readFile

e.jsFuncs = map[string]func(call CallContext) goja.Value{
"require": e.require,
"templateFile": e.templateFileJS,
"templateString": e.templateStringJS,
"templateStringInput": e.templateStringInputJS,
"registerTemplateFunc": e.registerTemplateFunc,
"unregisterTemplateFunc": e.unregisterTemplateFunc,
"require": e.require,
"templateFile": e.templateFileJS,
"templateFileMultiple": e.templateFileMultipleJS,
"templateString": e.templateStringJS,
"templateStringMultiple": e.templateStringMultipleJS,
"templateStringInput": e.templateStringInputJS,
"templateStringInputMultiple": e.templateStringInputMultipleJS,
"registerTemplateFunc": e.registerTemplateFunc,
"unregisterTemplateFunc": e.unregisterTemplateFunc,
}

for _, opt := range opts {
Expand Down Expand Up @@ -205,6 +208,17 @@ func (e *Engine) RunTemplate(templateFile string, outFile string, data any) erro
return e.templator.TemplateFile(vm, templateFile, outFile, data)
}

// RunTemplateMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template to a file.
// The same LocalComputed context is available to each run.
func (e *Engine) RunTemplateMultiple(templateFile string, outFile string, data any, numTimes int) error {
vm, err := e.init(data)
if err != nil {
return err
}

return e.templator.TemplateFileMultiple(vm, templateFile, outFile, data, numTimes)
}

// RunTemplateString runs the provided template file, with the provided data, starting the template engine and templating the provided template, returning the rendered result.
func (e *Engine) RunTemplateString(templateFile string, data any) (string, error) {
vm, err := e.init(data)
Expand All @@ -215,14 +229,36 @@ func (e *Engine) RunTemplateString(templateFile string, data any) (string, error
return e.templator.TemplateString(vm, templateFile, data)
}

// RunTemplateStringMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result.
// The same LocalComputed context is available to each run.
func (e *Engine) RunTemplateStringMultiple(templateFile string, data any, numTimes int) (string, error) {
vm, err := e.init(data)
if err != nil {
return "", err
}

return e.templator.TemplateStringMultiple(vm, templateFile, data, numTimes)
}

// RunTemplateStringInput runs the provided input template string, with the provided data, starting the template engine and templating the provided template, returning the rendered result.
func (e *Engine) RunTemplateStringInput(name, template string, data any) (string, error) {
vm, err := e.init(data)
if err != nil {
return "", err
}

return e.templator.TemplateStringInput(vm, name, template, data)
return e.templator.TemplateStringInput(vm, name, template, data, 1)
}

// RunTemplateStringInputMultiple runs the provided input template string numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result.
// The same LocalComputed context is available to each run.
func (e *Engine) RunTemplateStringInputMultiple(name, template string, data any, numTimes int) (string, error) {
vm, err := e.init(data)
if err != nil {
return "", err
}

return e.templator.TemplateStringInput(vm, name, template, data, numTimes)
}

//nolint:funlen
Expand Down Expand Up @@ -270,6 +306,16 @@ func (e *Engine) init(data any) (*vm.VM, error) {
return "", nil
}
}(v)
e.templator.TmplFuncs["templateFileMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) {
return func(templateFile, outFile string, data any, numTimes int) (string, error) {
err := e.templator.TemplateFileMultiple(v, templateFile, outFile, data, numTimes)
if err != nil {
return "", err
}

return "", nil
}
}(v)
e.templator.TmplFuncs["templateString"] = func(v *vm.VM) func(string, any) (string, error) {
return func(templateFile string, data any) (string, error) {
templated, err := e.templator.TemplateString(v, templateFile, data)
Expand All @@ -280,9 +326,29 @@ func (e *Engine) init(data any) (*vm.VM, error) {
return templated, nil
}
}(v)
e.templator.TmplFuncs["templateStringMultiple"] = func(v *vm.VM) func(string, any, int) (string, error) {
return func(templateFile string, data any, numTimes int) (string, error) {
templated, err := e.templator.TemplateStringMultiple(v, templateFile, data, numTimes)
if err != nil {
return "", err
}

return templated, nil
}
}(v)
e.templator.TmplFuncs["templateStringInput"] = func(v *vm.VM) func(string, string, any) (string, error) {
return func(name, template string, data any) (string, error) {
templated, err := e.templator.TemplateStringInput(v, name, template, data)
templated, err := e.templator.TemplateStringInput(v, name, template, data, 1)
if err != nil {
return "", err
}

return templated, nil
}
}(v)
e.templator.TmplFuncs["templateStringInputMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) {
return func(name, template string, data any, numTimes int) (string, error) {
templated, err := e.templator.TemplateStringInput(v, name, template, data, numTimes)
if err != nil {
return "", err
}
Expand Down
14 changes: 7 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ module github.com/speakeasy-api/easytemplate
go 1.19

require (
github.com/dop251/goja v0.0.0-20231024180952-594410467bc6
github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e
github.com/evanw/esbuild v0.17.8
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab
github.com/evanw/esbuild v0.19.5
github.com/go-sourcemap/sourcemap v2.1.3+incompatible
github.com/golang/mock v1.6.0
github.com/stretchr/testify v1.8.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
29 changes: 14 additions & 15 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
github.com/dop251/goja v0.0.0-20231024180952-594410467bc6 h1:U9bRrSlYCu0P8hMulhIdYpr5HUao66tKPdNgD88Zi5M=
github.com/dop251/goja v0.0.0-20231024180952-594410467bc6/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e h1:DJ5cKH4HUYevCd09vMIbwc8U02eBKLFR2q1O1hSAJcY=
github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0=
github.com/evanw/esbuild v0.17.8 h1:QzE7cRRq7y3qH7ZKGN0/nUGbdZPyikfNaMaCMNUufnU=
github.com/evanw/esbuild v0.17.8/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab h1:LrVf0AFnp5WiGKJ0a6cFf4RwNIN327uNUeVGJtmAFEE=
github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
github.com/evanw/esbuild v0.19.5 h1:9ildZqajUJzDAwNf9MyQsLh2RdDRKTq3kcyyzhE39us=
github.com/evanw/esbuild v0.19.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand Down Expand Up @@ -56,7 +57,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -70,18 +70,17 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
Expand Down
Loading

0 comments on commit 4ec21c3

Please sign in to comment.