Skip to content

Commit

Permalink
Add support for strict variables
Browse files Browse the repository at this point in the history
The Ruby implementation has support for erroring when a template has an
undefined variable. This is implemented by passing an option to the
render() method.

As this version doesn't expose render.Config in the engine, a
StrictVariables() method is provided on the engine to enable it. This
differs from the Ruby version in that it's on for the entire engine,
rather than enabled for each call to Render(). This is more similar to
the Ruby version's render!() method as it immediately errors, rather
than storing a stack of errors which can be accessed later.

Refs. #8
  • Loading branch information
jamesog committed Oct 29, 2022
1 parent f491f5e commit decc5e5
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 9 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ These features of Shopify Liquid aren't implemented:
}}`. [[Issue #42](https://github.com/osteele/liquid/issues/42)]
- Warn and lax [error modes](https://github.com/shopify/liquid#error-modes).
- Non-strict filters. An undefined filter is currently an error.
- Strict variables. An undefined variable is not an error.

### Drops

Expand Down
20 changes: 13 additions & 7 deletions cmd/liquid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import (

// for testing
var (
stderr io.Writer = os.Stderr
stdout io.Writer = os.Stdout
stdin io.Reader = os.Stdin
exit func(int) = os.Exit
env func() []string = os.Environ
bindings map[string]interface{} = map[string]interface{}{}
stderr io.Writer = os.Stderr
stdout io.Writer = os.Stdout
stdin io.Reader = os.Stdin
exit func(int) = os.Exit
env func() []string = os.Environ
bindings map[string]interface{} = map[string]interface{}{}
strictVars bool
)

func main() {
Expand All @@ -41,6 +42,7 @@ func main() {

var bindEnvs bool
cmdLine.BoolVar(&bindEnvs, "env", false, "bind environment variables")
cmdLine.BoolVar(&strictVars, "strict", false, "enable strict variable mode in templates")

err = cmdLine.Parse(os.Args[1:])
if err != nil {
Expand Down Expand Up @@ -86,7 +88,11 @@ func render() error {
return err
}

tpl, err := liquid.NewEngine().ParseTemplate(buf)
e := liquid.NewEngine()
if strictVars {
e.StrictVariables()
}
tpl, err := e.ParseTemplate(buf)
if err != nil {
return err
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/liquid/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestMain(t *testing.T) {
main()
require.True(t, envCalled)
require.Equal(t, "Hello, World!", buf.String())
bindings = make(map[string]interface{})

// filename
stdin = os.Stdin
Expand All @@ -72,6 +73,17 @@ func TestMain(t *testing.T) {
exitCode := 0
exit = func(n int) { exitCalled = true; exitCode = n }

// strict variables
stdin = bytes.NewBufferString(src)
buf = &bytes.Buffer{}
stderr = buf
os.Args = []string{"liquid", "--strict"}
main()
require.True(t, exitCalled)
require.Equal(t, 1, exitCode)
require.Equal(t, "Liquid error: undefined variable in {{ TARGET }}\n", buf.String())

exitCode = 0
os.Args = []string{"liquid", "testdata/source.liquid"}
main()
require.Equal(t, 0, exitCode)
Expand Down
5 changes: 5 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func (e *Engine) RegisterTag(name string, td Renderer) {
})
}

// StrictVariables causes the renderer to error when the template contains an undefined variable.
func (e *Engine) StrictVariables() {
e.cfg.StrictVariables = true
}

// ParseTemplate creates a new Template using the engine configuration.
func (e *Engine) ParseTemplate(source []byte) (*Template, SourceError) {
return newTemplate(&e.cfg, source, "", 0)
Expand Down
3 changes: 2 additions & 1 deletion render/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
type Config struct {
parser.Config
grammar
Cache map[string][]byte
Cache map[string][]byte
StrictVariables bool
}

type grammar struct {
Expand Down
4 changes: 4 additions & 0 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package render

import (
"errors"
"fmt"
"io"
"reflect"
Expand Down Expand Up @@ -66,6 +67,9 @@ func (n *ObjectNode) render(w *trimWriter, ctx nodeContext) Error {
if err != nil {
return wrapRenderError(err, n)
}
if value == nil && ctx.config.StrictVariables {
return wrapRenderError(errors.New("undefined variable"), n)
}
if err := wrapRenderError(writeObject(w, value), n); err != nil {
return err
}
Expand Down
37 changes: 37 additions & 0 deletions render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ var renderTests = []struct{ in, out string }{
{`x {%- y -%} z`, "xyz"},
}

var renderStrictTests = []struct{ in, out string }{
// literal representations
{`{{ true }}`, "true"},
{`{{ false }}`, "false"},
{`{{ 12 }}`, "12"},
{`{{ 12.3 }}`, "12.3"},
{`{{ date }}`, "2015-07-17 15:04:05 +0000"},
{`{{ "string" }}`, "string"},
{`{{ array }}`, "firstsecondthird"},

// variables and properties
{`{{ int }}`, "123"},
{`{{ page.title }}`, "Introduction"},
{`{{ array[1] }}`, "second"},
{`{{ invalid }}`, ""},
}

var renderErrorTests = []struct{ in, out string }{
{`{% errblock %}{% enderrblock %}`, "errblock error"},
}
Expand Down Expand Up @@ -111,6 +128,26 @@ func TestRenderErrors(t *testing.T) {
}
}

func TestRenderStrictVariables(t *testing.T) {
cfg := NewConfig()
cfg.StrictVariables = true
addRenderTestTags(cfg)
for i, test := range renderStrictTests {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
root, err := cfg.Compile(test.in, parser.SourceLoc{})
require.NoErrorf(t, err, test.in)
buf := new(bytes.Buffer)
err = Render(root, buf, renderTestBindings, cfg)
if test.in == `{{ invalid }}` {
require.Errorf(t, err, test.in)
} else {
require.NoErrorf(t, err, test.in)
}
require.Equalf(t, test.out, buf.String(), test.in)
})
}
}

func addRenderTestTags(cfg Config) {
cfg.AddTag("y", func(string) (func(io.Writer, Context) error, error) {
return func(w io.Writer, _ Context) error {
Expand Down

0 comments on commit decc5e5

Please sign in to comment.