Skip to content

Commit

Permalink
fix: various fixes and improvements to support Speakeasy's SDK Generator
Browse files Browse the repository at this point in the history
  • Loading branch information
TristanSpeakEasy committed Jan 27, 2023
1 parent e7691c8 commit fc64268
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 42 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,14 @@ The `sjs` snippet can be used anywhere within your template (including multiple
Context data that is available to the templates is also available to JavasScript. Snippets and Files imported with a template file will have access to the same context data as that template file. For example
```gotemplate
```sj
```sjs
console.log(context.Global); // The context data provided by the initial call to the templating engine.
console.log(context.Local); // The context data provided to the template file.
sjs```
```
The `Local` context provided can be modified from within JavaScript and will be available to the template file when it is rendered.
The context object also contains `LocalComputed` and `GlobalComputed` objects that allow you to store computed values that can be later.
`LocalComputed` is only available to the current template file and `GlobalComputed` is available to all templates and scripts, from the point it was set.
### Using the `render` function
Expand Down Expand Up @@ -247,6 +248,18 @@ The [underscore.js](http://underscorejs.org/) library is included by default and
_.each([1, 2, 3], console.log);
```

### Importing External Javascript Libraries

Using `WithJSFiles` you can import external JavaScript libraries into the global scope. For example:

```go
engine := easytemplate.New(
easytemplate.WithJSFiles("faker.min.js", "<CONTENT OF FILE HERE>"),
)
```

The imported code will be available in the global scope.

### Available Engine functions to JS

The following functions are available to JavaScript from the templating engine:
Expand Down
37 changes: 32 additions & 5 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ func WithJSFuncs(funcs map[string]func(call CallContext) goja.Value) Opt {
}
}

// WithJSFiles allows for providing additional javascript files to be loaded into the engine.
func WithJSFiles(files map[string]string) Opt {
return func(e *Engine) {
e.jsFiles = files
}
}

// Engine provides the templating engine.
type Engine struct {
baseDir string
Expand All @@ -93,6 +100,7 @@ type Engine struct {

ran bool
jsFuncs map[string]func(call CallContext) goja.Value
jsFiles map[string]string
}

type jsVM struct {
Expand Down Expand Up @@ -125,6 +133,7 @@ func New(opts ...Opt) *Engine {
e := &Engine{
templator: t,
jsFuncs: map[string]func(call CallContext) goja.Value{},
jsFiles: map[string]string{},
}

t.ReadFunc = e.readFile
Expand Down Expand Up @@ -155,7 +164,7 @@ func (e *Engine) RunScript(scriptFile string, data any) error {
return fmt.Errorf("failed to read script file: %w", err)
}

s, err := vm.Compile(scriptFile, string(script), false)
s, err := vm.Compile(scriptFile, string(script), true)
if err != nil {
return fmt.Errorf("failed to compile script: %w", err)
}
Expand Down Expand Up @@ -200,6 +209,13 @@ func (e *Engine) init(data any) (*jsVM, error) {
return nil, fmt.Errorf("failed to init underscore: %w", err)
}

for name, content := range e.jsFiles {
_, err := g.RunString(content)
if err != nil {
return nil, fmt.Errorf("failed to init %s: %w", name, err)
}
}

new(require.Registry).Enable(g)
console.Enable(g)

Expand Down Expand Up @@ -242,10 +258,21 @@ func (e *Engine) init(data any) (*jsVM, error) {
}
}(vm)

e.templator.ContextData = data
if _, err := vm.RunString(`function createComputedContextObject() { return {}; }`); err != nil {
return nil, fmt.Errorf("failed to init createComputedContextObject: %w", err)
}

globalComputed, err := vm.RunString(`createComputedContextObject();`)
if err != nil {
return nil, fmt.Errorf("failed to init globalComputed: %w", err)
}

e.templator.SetContextData(data, globalComputed)
if err := vm.Set("context", &template.Context{
Global: data,
Local: data,
Global: data,
GlobalComputed: globalComputed,
Local: data,
LocalComputed: globalComputed,
}); err != nil {
return nil, fmt.Errorf("failed to set context: %w", err)
}
Expand All @@ -263,7 +290,7 @@ func (e *Engine) require(call CallContext) goja.Value {
panic(vm.NewGoError(err))
}

s, err := vm.Compile(scriptPath, string(script), false)
s, err := vm.Compile(scriptPath, string(script), true)
if err != nil {
panic(vm.NewGoError(err))
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ go 1.19

require (
github.com/dop251/goja v0.0.0-20230119130012-17fd568758fe
github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e
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/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.5.0 // indirect
Expand Down
15 changes: 15 additions & 0 deletions internal/template/mocks/template_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 85 additions & 23 deletions internal/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"text/template"

Expand All @@ -21,29 +22,45 @@ type (
ReadFunc func(string) ([]byte, error)
)

var sjsRegex = regexp.MustCompile("(?ms)(^```sjs\\s+(.*?)^sjs```)")
var sjsRegex = regexp.MustCompile("(?ms)(```sjs\\s*\\n*(.*?)sjs```)")

// Context is the context that is passed templates or js.
type Context struct {
Global any
Local any
Global any
Local any
GlobalComputed goja.Value
LocalComputed goja.Value
}

type tmplContext struct {
Global any
Local any
GlobalComputed any
LocalComputed any
}

// VM represents a virtual machine that can be used to run js.
type VM interface {
Get(name string) goja.Value
Set(name string, value interface{}) error
Set(name string, value any) error
Compile(name string, src string, strict bool) (*goja.Program, error)
RunProgram(p *goja.Program) (result goja.Value, err error)
RunString(script string) (result goja.Value, err error)
GetObject(val goja.Value) *goja.Object
}

// Templator extends the go text/template package to allow for sjs snippets.
type Templator struct {
WriteFunc WriteFunc
ReadFunc ReadFunc
ContextData interface{}
TmplFuncs map[string]any
WriteFunc WriteFunc
ReadFunc ReadFunc
TmplFuncs map[string]any
contextData any
globalComputed goja.Value
}

func (t *Templator) SetContextData(contextData any, globalComputed goja.Value) {
t.contextData = contextData
t.globalComputed = globalComputed
}

// TemplateFile will template a file and write the output to outFile.
Expand Down Expand Up @@ -84,9 +101,16 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o
}
}()

localComputed, err := vm.RunString(`createComputedContextObject();`)
if err != nil {
return "", fmt.Errorf("failed to create local computed context: %w", err)
}

context := &Context{
Global: t.ContextData,
Local: inputData,
Global: t.contextData,
GlobalComputed: t.globalComputed,
Local: inputData,
LocalComputed: localComputed,
}

currentContext := vm.Get("context")
Expand All @@ -100,22 +124,38 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o
return "", fmt.Errorf("failed to read template file: %w", err)
}

replacedLines := 0

evaluated, err := utils.ReplaceAllStringSubmatchFunc(sjsRegex, string(data), func(match []string) (string, error) {
const expectedMatchLen = 3
if len(match) != expectedMatchLen {
return match[0], nil
}

return t.execSJSBlock(vm, match[2])
output, err := t.execSJSBlock(vm, match[2], templatePath)
if err != nil {
return "", err
}

replacedLines += strings.Count(match[1], "\n") - strings.Count(output, "\n")

return output, nil
})
if err != nil {
return "", err
}

// Use the local context from the inline script
context.Local = getLocalContext(vm)
// Get the computed context back as it might have been modified by the inline script
localComputed = getComputedContext(vm)

tmplCtx := &tmplContext{
Global: context.Global,
Local: context.Local,
GlobalComputed: context.GlobalComputed.Export(),
LocalComputed: localComputed.Export(),
}

out, err = t.execTemplate(templatePath, evaluated, context)
out, err = t.execTemplate(templatePath, evaluated, tmplCtx, replacedLines)
if err != nil {
return "", err
}
Expand All @@ -128,8 +168,8 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o
return out, nil
}

func (t *Templator) execSJSBlock(vm VM, js string) (string, error) {
s, err := vm.Compile("inline", js, false)
func (t *Templator) execSJSBlock(vm VM, js, templatePath string) (string, error) {
s, err := vm.Compile("inline", js, true)
if err != nil {
return "", fmt.Errorf("failed to compile inline script: %w", err)
}
Expand All @@ -142,7 +182,7 @@ func (t *Templator) execSJSBlock(vm VM, js string) (string, error) {
}

if _, err := vm.RunProgram(s); err != nil {
return "", fmt.Errorf("failed to run inline script: %w", err)
return "", fmt.Errorf("failed to run inline script in %s:\n%s\n%w", templatePath, js, err)
}

if err := vm.Set("render", currentRender); err != nil {
Expand All @@ -152,18 +192,16 @@ func (t *Templator) execSJSBlock(vm VM, js string) (string, error) {
return strings.Join(c.renderedContent, "\n"), nil
}

func getLocalContext(vm VM) any {
func getComputedContext(vm VM) goja.Value {
// Get the local context back as it might have been modified by the inline script
contextVal := vm.Get("context")

localContextVal := vm.GetObject(contextVal).Get("Local")
computedVal := vm.GetObject(contextVal).Get("LocalComputed")

localContext := localContextVal.Export()

return localContext
return computedVal
}

func (t *Templator) execTemplate(name string, tmplContent string, data any) (string, error) {
func (t *Templator) execTemplate(name string, tmplContent string, data any, replacedLines int) (string, error) {
tmp, err := template.New(name).Funcs(t.TmplFuncs).Parse(tmplContent)
if err != nil {
return "", fmt.Errorf("failed to parse template: %w", err)
Expand All @@ -172,8 +210,32 @@ func (t *Templator) execTemplate(name string, tmplContent string, data any) (str
var buf bytes.Buffer

if err := tmp.Execute(&buf, data); err != nil {
err = adjustLineNumber(name, err, replacedLines)
return "", fmt.Errorf("failed to execute template: %w", err)
}

return buf.String(), nil
}

func adjustLineNumber(name string, err error, replacedLines int) error {
lineNumRegex, rErr := regexp.Compile(fmt.Sprintf(`template: %s:(\d+)`, regexp.QuoteMeta(name)))
if rErr == nil {
errMsg, rErr := utils.ReplaceAllStringSubmatchFunc(lineNumRegex, err.Error(), func(matches []string) (string, error) {
if len(matches) != 2 { //nolint:gomnd
return matches[0], nil
}

currentLineNumber, err := strconv.Atoi(matches[1])
if err != nil {
return matches[0], nil //nolint:nilerr
}

return strings.Replace(matches[0], matches[1], fmt.Sprintf("%d", currentLineNumber+replacedLines), 1), nil
})
if rErr == nil {
err = fmt.Errorf(errMsg)
}
}

return err
}
Loading

0 comments on commit fc64268

Please sign in to comment.