Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to customize the resolution of context contained variables. #287

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Please use the [issue tracker](https://github.com/flosch/pongo2/issues) if you'r
- [Advanced C-like expressions](https://github.com/flosch/pongo2/blob/master/template_tests/expressions.tpl).
- [Complex function calls within expressions](https://github.com/flosch/pongo2/blob/master/template_tests/function_calls_wrapper.tpl).
- [Easy API to create new filters and tags](http://godoc.org/github.com/flosch/pongo2#RegisterFilter) ([including parsing arguments](http://godoc.org/github.com/flosch/pongo2#Parser))
- [Customizing variable resolution](http://godoc.org/github.com/flosch/pongo2#hdr-Variable_resolution)
- Additional features:
- Macros including importing macros from other files (see [template_tests/macro.tpl](https://github.com/flosch/pongo2/blob/master/template_tests/macro.tpl))
- [Template sandboxing](https://godoc.org/github.com/flosch/pongo2#TemplateSet) ([directory patterns](http://golang.org/pkg/path/filepath/#Match), banned tags/filters)
Expand Down
48 changes: 48 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,54 @@ func SetAutoescape(newValue bool) {
// {{ myfunc("test", 42) }}
// {{ user.name }}
// {{ pongo2.version }}
//
// Variable resolution
//
// By the default sub variables are resolved by
// 1. Field names inside structs
// 2. Keys of values inside maps
// 3. Indexes of values inside slices
//
// This behavior can be customized using either struct tag "pongo2" like:
// type MyStruct struct {
// FieldA string `pongo2:"uh"`
// FieldB string `pongo2:"yeah"`
// }
//
// my.tmpl:
// {{ myStruct.uh }} {{ myStruct.yeah }}
//
// ...or by implementing NamedFieldResolver or IndexedFieldResolver.
//
// type MyStruct struct {
// fieldA string
// }
//
// // GetNamedField implements NamedFieldResolver
// func (s MyStruct) GetNamedField(s string) (interface{}, error) {
// switch s {
// case "uh":
// return s.fieldA, nil
// case "yeah":
// return "YEAH!", nil
// default:
// return nil, pongo2.ErrNoSuchField
// }
//
// // GetNamedField implements IndexedFieldResolver
// func (s MyStruct) GetIndexedField(s int) (interface{}, error) {
// switch s {
// case 0:
// return s.fieldA, nil
// case 1:
// return "YEAH!", nil
// default:
// return nil, pongo2.ErrNoSuchField
// }
//
// my.tmpl:
// {{ myStruct.uh }} {{ myStruct.yeah }}
// {{ myStruct.0 }} {{ myStruct.1 }}
type Context map[string]interface{}

func (c Context) checkForValidIdentifiers() *Error {
Expand Down
149 changes: 125 additions & 24 deletions variable.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
package pongo2

import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)

var (
ErrNoSuchField = errors.New("no such field")
)

// NamedFieldResolver can be implemented by every value inside a Context.
// By default, reflection is used to resolve fields of a struct or a map.
type NamedFieldResolver interface {
// GetNamedField will be called with the requested field name.
// Any error will lead to the immediate interruption of the evaluation;
// except in ErrNoSuchField: In this case the default evaluation process will
// continue (reflection).
GetNamedField(string) (interface{}, error)
}

// IndexedFieldResolver can be implemented by every value inside a Context.
// By default, reflection is used to resolve index of a slice.
type IndexedFieldResolver interface {
// GetIndexedField will be called with the requested field index.
// Any error will lead to the immediate interruption of the evaluation;
// except in ErrNoSuchField: In this case the default evaluation process will
// continue (reflection).
GetIndexedField(int) (interface{}, error)
}

const (
varTypeInt = iota
varTypeIdent
Expand Down Expand Up @@ -266,39 +291,26 @@ func (vr *variableResolver) resolve(ctx *ExecutionContext) (*Value, error) {
}

// Look up which part must be called now
var resolver func(reflect.Value, *variablePart) (_ reflect.Value, done bool, _ error)
switch part.typ {
case varTypeInt:
// Calling an index is only possible for:
// * slices/arrays/strings
switch current.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
if part.i >= 0 && current.Len() > part.i {
current = current.Index(part.i)
} else {
// In Django, exceeding the length of a list is just empty.
return AsValue(nil), nil
}
default:
return nil, fmt.Errorf("can't access an index on type %s (variable %s)",
current.Kind().String(), vr.String())
}
resolver = vr.resolveIndexedField
case varTypeIdent:
// debugging:
// fmt.Printf("now = %s (kind: %s)\n", part.s, current.Kind().String())

// Calling a field or key
switch current.Kind() {
case reflect.Struct:
current = current.FieldByName(part.s)
case reflect.Map:
current = current.MapIndex(reflect.ValueOf(part.s))
default:
return nil, fmt.Errorf("can't access a field by name on type %s (variable %s)",
current.Kind().String(), vr.String())
}
resolver = vr.resolveNamedField
default:
panic("unimplemented")
}

if v, done, err := resolver(current, part); err != nil {
return nil, err
} else if done {
return &Value{val: v}, nil
} else {
current = v
}
}
}

Expand Down Expand Up @@ -445,6 +457,95 @@ func (vr *variableResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
return value, nil
}

func (vr *variableResolver) resolveNamedField(of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) {
// If current does implement NamedFieldResolver, call it with the actual field name.
if fr, ok := of.Interface().(NamedFieldResolver); ok {
if val, err := fr.GetNamedField(by.s); err == ErrNoSuchField {
// Continue with reflection, below...
} else if err != nil {
return reflect.Value{}, false, fmt.Errorf("can't access field %s on type %s (variable %s): %w",
by.s, of.Kind().String(), vr.String(), err)
} else {
return reflect.ValueOf(val), false, nil
}
}

// Calling a field or key
switch of.Kind() {
case reflect.Struct:
return vr.resolveStructField(of, by.s), false, nil
case reflect.Map:
return of.MapIndex(reflect.ValueOf(by.s)), false, nil
default:
return reflect.Value{}, false, fmt.Errorf("can't access a field by name on type %s (variable %s)",
of.Kind().String(), vr.String())
}
}

func (vr *variableResolver) resolveStructField(of reflect.Value, name string) reflect.Value {
t := of.Type()
nf := t.NumField()
var byAliasIndex, byNameIndex []int
for i := 0; i < nf; i++ {
f := t.Field(i)
// Only respect exported field to prevent security issues in templates.
if f.IsExported() {
// We remember this field if its name matches.
if f.Name == name {
byNameIndex = f.Index
}
alias := vr.resolveStructFieldTag(f)
// We remember this field if its alias via tag matches.
if alias == name {
byAliasIndex = f.Index
}
}
}

if byAliasIndex != nil {
return of.FieldByIndex(byAliasIndex)
}
if byNameIndex != nil {
return of.FieldByIndex(byNameIndex)
}
return reflect.Value{}
}

func (vr *variableResolver) resolveStructFieldTag(of reflect.StructField) (alias string) {
plain := of.Tag.Get("pongo2")
plainParts := strings.SplitN(plain, ",", 2)
return strings.TrimSpace(plainParts[0])
}

func (vr *variableResolver) resolveIndexedField(of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) {
// If current does implement IndexedFieldResolver, call it with the actual index.
if fr, ok := of.Interface().(IndexedFieldResolver); ok {
if val, err := fr.GetIndexedField(by.i); err == ErrNoSuchField {
// Continue with reflection, below...
} else if err != nil {
return reflect.Value{}, false, fmt.Errorf("can't access index %d on type %s (variable %s): %w",
by.i, of.Kind().String(), vr.String(), err)
} else {
return reflect.ValueOf(val), false, nil
}
}

// Calling an index is only possible for:
// * slices/arrays/strings
switch of.Kind() {
case reflect.String, reflect.Array, reflect.Slice:
if by.i >= 0 && of.Len() > by.i {
return of.Index(by.i), false, nil
} else {
// In Django, exceeding the length of a list is just empty.
return reflect.ValueOf(nil), true, nil
}
default:
return reflect.Value{}, false, fmt.Errorf("can't access an index on type %s (variable %s)",
of.Kind().String(), vr.String())
}
}

func (v *nodeFilteredVariable) FilterApplied(name string) bool {
for _, filter := range v.filterChain {
if filter.name == name {
Expand Down
Loading