Skip to content

Commit

Permalink
Filters are an engine configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
osteele committed Jun 30, 2017
1 parent 41da3f9 commit 2e9903f
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 131 deletions.
21 changes: 17 additions & 4 deletions chunks/context.go
Expand Up @@ -6,18 +6,31 @@ import (

// Context is the evaluation context for chunk AST rendering.
type Context struct {
vars map[string]interface{}
vars map[string]interface{}
settings Settings
}

type Settings struct {
ExpressionSettings expressions.Settings
}

func (s Settings) AddFilter(name string, fn interface{}) {
s.ExpressionSettings.AddFilter(name, fn)
}

func NewSettings() Settings {
return Settings{expressions.NewSettings()}
}

// NewContext creates a new evaluation context.
func NewContext(scope map[string]interface{}) Context {
func NewContext(scope map[string]interface{}, s Settings) Context {
// The assign tag modifies the scope, so make a copy first.
// TODO this isn't really the right place for this.
vars := map[string]interface{}{}
for k, v := range scope {
vars[k] = v
}
return Context{vars}
return Context{vars, s}
}

// Evaluate evaluates an expression within the template context.
Expand All @@ -33,5 +46,5 @@ func (c Context) Evaluate(expr expressions.Expression) (out interface{}, err err
}
}
}()
return expr.Evaluate(expressions.NewContext(c.vars))
return expr.Evaluate(expressions.NewContext(c.vars, c.settings.ExpressionSettings))
}
16 changes: 11 additions & 5 deletions chunks/render_context.go
Expand Up @@ -16,8 +16,9 @@ type RenderContext interface {
Evaluate(expr expressions.Expression) (interface{}, error)
EvaluateString(source string) (interface{}, error)
EvaluateStatement(tag, source string) (interface{}, error)
RenderBranch(io.Writer, *ASTControlTag) error
RenderChild(io.Writer, *ASTControlTag) error
RenderChildren(io.Writer) error
// RenderTemplate(io.Writer, filename string) (string, error)
InnerString() (string, error)
}

Expand Down Expand Up @@ -45,7 +46,7 @@ func (c renderContext) EvaluateString(source string) (out interface{}, err error
}
}
}()
return expressions.EvaluateString(source, expressions.NewContext(c.ctx.vars))
return expressions.EvaluateString(source, expressions.NewContext(c.ctx.vars, c.ctx.settings.ExpressionSettings))
}

func (c renderContext) EvaluateStatement(tag, source string) (interface{}, error) {
Expand All @@ -68,6 +69,11 @@ func (c renderContext) Set(name string, value interface{}) {
c.ctx.vars[name] = value
}

// RenderChild renders a node.
func (c renderContext) RenderChild(w io.Writer, b *ASTControlTag) error {
return c.ctx.RenderASTSequence(w, b.Body)
}

// RenderChildren renders the current node's children.
func (c renderContext) RenderChildren(w io.Writer) error {
if c.cn == nil {
Expand All @@ -76,9 +82,9 @@ func (c renderContext) RenderChildren(w io.Writer) error {
return c.ctx.RenderASTSequence(w, c.cn.Body)
}

func (c renderContext) RenderBranch(w io.Writer, b *ASTControlTag) error {
return c.ctx.RenderASTSequence(w, b.Body)
}
// func (c renderContext) RenderTemplate(w io.Writer, filename string) (string, error) {
// // TODO use the tags and filters from the current context
// }

// InnerString renders the children to a string.
func (c renderContext) InnerString() (string, error) {
Expand Down
5 changes: 2 additions & 3 deletions chunks/render_test.go
Expand Up @@ -16,7 +16,7 @@ var renderTests = []struct{ in, expected string }{
{`{{ ar[1] }}`, "second"},
}

var renderTestContext = Context{map[string]interface{}{
var renderTestContext = NewContext(map[string]interface{}{
"x": 123,
"obj": map[string]interface{}{
"a": 1,
Expand All @@ -41,8 +41,7 @@ var renderTestContext = Context{map[string]interface{}{
"page": map[string]interface{}{
"title": "Introduction",
},
},
}
}, NewSettings())

func TestRender(t *testing.T) {
for i, test := range renderTests {
Expand Down
22 changes: 6 additions & 16 deletions engine.go
Expand Up @@ -5,26 +5,16 @@ import (
"io"

"github.com/osteele/liquid/chunks"
"github.com/osteele/liquid/expressions"
"github.com/osteele/liquid/filters"
"github.com/osteele/liquid/tags"
)

// TODO move the filters and tags from globals to the engine
func init() {
tags.DefineStandardTags()
filters.DefineStandardFilters()
}

type engine struct{}

type template struct {
ast chunks.ASTNode
}
type engine struct{ settings chunks.Settings }

// NewEngine returns a new template engine.
func NewEngine() Engine {
return engine{}
e := engine{chunks.NewSettings()}
filters.AddStandardFilters(e.settings.ExpressionSettings)
return e
}

// DefineStartTag is in the Engine interface.
Expand All @@ -40,7 +30,7 @@ func (e engine) DefineStartTag(name string, td TagDefinition) {
// DefineFilter is in the Engine interface.
func (e engine) DefineFilter(name string, fn interface{}) {
// TODO define this on the engine, not globally
expressions.DefineFilter(name, fn)
e.settings.AddFilter(name, fn)
}

// ParseAndRenderString is in the Engine interface.
Expand All @@ -56,7 +46,7 @@ func (e engine) ParseTemplate(text []byte) (Template, error) {
if err != nil {
return nil, err
}
return &template{ast}, nil
return &template{ast, e.settings}, nil
}

// ParseAndRender is in the Engine interface.
Expand Down
6 changes: 6 additions & 0 deletions expressions/builders.go
Expand Up @@ -13,6 +13,12 @@ func makeContainsExpr(e1, e2 func(Context) interface{}) func(Context) interface{
}
}

func makeFilter(fn valueFn, name string, args []valueFn) valueFn {
return func(ctx Context) interface{} {
return ctx.Filters().runFilter(ctx, fn, name, args)
}
}

func makeIndexExpr(obj, index func(Context) interface{}) func(Context) interface{} {
return func(ctx Context) interface{} {
ref := reflect.ValueOf(obj(ctx))
Expand Down
22 changes: 20 additions & 2 deletions expressions/context.go
Expand Up @@ -4,16 +4,34 @@ package expressions
type Context interface {
Get(string) interface{}
Set(string, interface{})
Filters() *FilterDictionary
}

type context struct {
vars map[string]interface{}
copied bool
Settings
}

type Settings struct {
filters *FilterDictionary
}

func NewSettings() Settings {
return Settings{NewFilterDictionary()}
}

func (s Settings) AddFilter(name string, fn interface{}) {
s.filters.AddFilter(name, fn)
}

// NewContext makes a new expression evaluation context.
func NewContext(vars map[string]interface{}) Context {
return &context{vars, false}
func NewContext(vars map[string]interface{}, s Settings) Context {
return &context{vars, false, s}
}

func (c *context) Filters() *FilterDictionary {
return c.filters
}

// Get looks up a variable value in the expression context.
Expand Down
2 changes: 1 addition & 1 deletion expressions/expressions_test.go
Expand Up @@ -103,7 +103,7 @@ var evaluatorTestContext = NewContext(map[string]interface{}{
"b": map[string]interface{}{"c": "d"},
"c": []string{"r", "g", "b"},
},
})
}, NewSettings())

func TestEvaluator(t *testing.T) {
for i, test := range evaluatorTests {
Expand Down
71 changes: 32 additions & 39 deletions expressions/filters.go
Expand Up @@ -23,10 +23,16 @@ func (e UndefinedFilter) Error() string {

type valueFn func(Context) interface{}

var filters = map[string]interface{}{}
type FilterDictionary struct {
filters map[string]interface{}
}

func NewFilterDictionary() *FilterDictionary {
return &FilterDictionary{map[string]interface{}{}}
}

// DefineFilter defines a filter.
func DefineFilter(name string, fn interface{}) {
// AddFilter defines a filter.
func (d *FilterDictionary) AddFilter(name string, fn interface{}) {
rf := reflect.ValueOf(fn)
switch {
case rf.Kind() != reflect.Func:
Expand All @@ -38,7 +44,7 @@ func DefineFilter(name string, fn interface{}) {
// case rf.Type().Out(1).Implements(…):
// panic(fmt.Errorf("a filter's second output must be type error"))
}
filters[name] = fn
d.filters[name] = fn
}

func isClosureInterfaceType(t reflect.Type) bool {
Expand All @@ -47,45 +53,32 @@ func isClosureInterfaceType(t reflect.Type) bool {
return closureType.ConvertibleTo(t) && !interfaceType.ConvertibleTo(t)
}

func makeFilter(f valueFn, name string, params []valueFn) valueFn {
fn, ok := filters[name]
func (d *FilterDictionary) runFilter(ctx Context, f valueFn, name string, params []valueFn) interface{} {
filter, ok := d.filters[name]
if !ok {
panic(UndefinedFilter(name))
}
fr := reflect.ValueOf(fn)
return func(ctx Context) interface{} {
defer func() {
if r := recover(); r != nil {
switch e := r.(type) {
case generics.GenericError:
panic(InterpreterError(e.Error()))
default:
// fmt.Println(string(debug.Stack()))
panic(e)
}
}
}()
args := []interface{}{f(ctx)}
for i, param := range params {
if i+1 < fr.Type().NumIn() && isClosureInterfaceType(fr.Type().In(i+1)) {
expr, err := Parse(param(ctx).(string))
if err != nil {
panic(err)
}
args = append(args, closure{expr, ctx})
} else {
args = append(args, param(ctx))
fr := reflect.ValueOf(filter)
args := []interface{}{f(ctx)}
for i, param := range params {
if i+1 < fr.Type().NumIn() && isClosureInterfaceType(fr.Type().In(i+1)) {
expr, err := Parse(param(ctx).(string))
if err != nil {
panic(err)
}
args = append(args, closure{expr, ctx})
} else {
args = append(args, param(ctx))
}
out, err := generics.Call(fr, args)
if err != nil {
panic(err)
}
switch out := out.(type) {
case []byte:
return string(out)
default:
return out
}
}
out, err := generics.Call(fr, args)
if err != nil {
panic(err)
}
switch out := out.(type) {
case []byte:
return string(out)
default:
return out
}
}
4 changes: 2 additions & 2 deletions expressions/parser_test.go
Expand Up @@ -8,12 +8,12 @@ import (
)

var parseErrorTests = []struct{ in, expected string }{
{"a | unknown_filter", "undefined filter: unknown_filter"},
// {"a | unknown_filter", "undefined filter: unknown_filter"},
}

func TestParseErrors(t *testing.T) {
for i, test := range parseErrorTests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
expr, err := Parse(test.in)
require.Nilf(t, expr, test.in)
require.Errorf(t, err, test.in, test.in)
Expand Down
14 changes: 7 additions & 7 deletions filters/filter_test.go
Expand Up @@ -9,10 +9,6 @@ import (
"github.com/stretchr/testify/require"
)

func init() {
DefineStandardFilters()
}

var filterTests = []struct {
in string
expected interface{}
Expand Down Expand Up @@ -130,7 +126,7 @@ func timeMustParse(s string) time.Time {
return t
}

var filterTestContext = expressions.NewContext(map[string]interface{}{
var filterTestBindings = map[string]interface{}{
"x": 123,
"animals": []string{"zebra", "octopus", "giraffe", "Sally Snake"},
"article": map[string]interface{}{
Expand Down Expand Up @@ -160,12 +156,16 @@ var filterTestContext = expressions.NewContext(map[string]interface{}{
"page": map[string]interface{}{
"title": "Introduction",
},
})
}

func TestFilters(t *testing.T) {
settings := expressions.NewSettings()
AddStandardFilters(settings)
context := expressions.NewContext(filterTestBindings, settings)

for i, test := range filterTests {
t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) {
value, err := expressions.EvaluateString(test.in, filterTestContext)
value, err := expressions.EvaluateString(test.in, context)
require.NoErrorf(t, err, test.in)
expected := test.expected
switch v := value.(type) {
Expand Down

0 comments on commit 2e9903f

Please sign in to comment.