From decc5e500a339fbfe3b15afd173c3bebd7711b3f Mon Sep 17 00:00:00 2001 From: James O'Gorman Date: Sat, 29 Oct 2022 21:27:15 +0100 Subject: [PATCH] Add support for strict variables 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 --- README.md | 1 - cmd/liquid/main.go | 20 +++++++++++++------- cmd/liquid/main_test.go | 12 ++++++++++++ engine.go | 5 +++++ render/config.go | 3 ++- render/render.go | 4 ++++ render/render_test.go | 37 +++++++++++++++++++++++++++++++++++++ 7 files changed, 73 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0e58539..7061648 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/liquid/main.go b/cmd/liquid/main.go index 74ec7ae..2151290 100644 --- a/cmd/liquid/main.go +++ b/cmd/liquid/main.go @@ -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() { @@ -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 { @@ -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 } diff --git a/cmd/liquid/main_test.go b/cmd/liquid/main_test.go index aba0753..565a697 100644 --- a/cmd/liquid/main_test.go +++ b/cmd/liquid/main_test.go @@ -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 @@ -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) diff --git a/engine.go b/engine.go index bcc1295..69f38f4 100644 --- a/engine.go +++ b/engine.go @@ -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) diff --git a/render/config.go b/render/config.go index 65ec55b..afc8f51 100644 --- a/render/config.go +++ b/render/config.go @@ -8,7 +8,8 @@ import ( type Config struct { parser.Config grammar - Cache map[string][]byte + Cache map[string][]byte + StrictVariables bool } type grammar struct { diff --git a/render/render.go b/render/render.go index 3e8b812..c072eb4 100644 --- a/render/render.go +++ b/render/render.go @@ -2,6 +2,7 @@ package render import ( + "errors" "fmt" "io" "reflect" @@ -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 } diff --git a/render/render_test.go b/render/render_test.go index db8d334..57b1b62 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -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"}, } @@ -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 {