Skip to content

feat: if "this" is in pongo env, then treat all its fields as part of env, implicitly. #26

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

Open
wants to merge 3 commits into
base: main
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/flosch/pongo2/v6
module github.com/rudderlabs/pongo2/v6

go 1.18
2 changes: 1 addition & 1 deletion pongo2_issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import (
"testing"

"github.com/flosch/pongo2/v6"
"github.com/rudderlabs/pongo2/v6"

Check failure on line 6 in pongo2_issues_test.go

View workflow job for this annotation

GitHub Actions / lint

could not import github.com/rudderlabs/pongo2/v6 (-: # github.com/rudderlabs/pongo2/v6 [github.com/rudderlabs/pongo2/v6.test]
)

func TestIssue151(t *testing.T) {
Expand Down
107 changes: 106 additions & 1 deletion pongo2_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"testing"
"time"

"github.com/flosch/pongo2/v6"
"github.com/rudderlabs/pongo2/v6"
)

type stringerValueType int
Expand Down Expand Up @@ -58,7 +58,7 @@
return false
}

func (u *user) Is_admin() *pongo2.Value {

Check failure on line 61 in pongo2_template_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: pongo2 (typecheck)
return pongo2.AsValue(isAdmin(u))
}

Expand All @@ -76,14 +76,14 @@

type tagSandboxDemoTag struct{}

func (node *tagSandboxDemoTag) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) *pongo2.Error {

Check failure on line 79 in pongo2_template_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: pongo2 (typecheck)
writer.WriteString("hello")
return nil
}

func tagSandboxDemoTagParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) {
return &tagSandboxDemoTag{}, nil
}

Check failure on line 86 in pongo2_template_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: pongo2 (typecheck)

func BannedFilterFn(in *pongo2.Value, params *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
return in, nil
Expand Down Expand Up @@ -524,6 +524,111 @@
}
}

func TestTemplate_ThisFallback(t *testing.T) {
type structWithField struct {
Field1 string
}

tests := []struct {
name string
template string
context pongo2.Context
want string
errorMessage string
wantErr bool
}{
{
name: "Map fallback",
template: "{{ missing_key }}",
context: pongo2.Context{
// 'missing_key' does not exist in root context => fallback to `this`.
"this": map[string]any{
"missing_key": "Hello from map fallback",
},
},
want: "Hello from map fallback",
wantErr: false,
},
{
name: "Struct fallback",
template: "{{ Field1 }}",
context: pongo2.Context{
// 'Field1' is not in root => fallback to `this.Field1`
"this": &structWithField{
Field1: "Hello from struct fallback",
},
},
want: "Hello from struct fallback",
wantErr: false,
},
{
name: "GetAttr fallback",
template: "{{ attr_name }}",
context: pongo2.Context{
// 'attr_name' is not in root => fallback to `this.GetAttr("attr_name")`
"this": &ObjWithoutAttrs{
the_attr_field: "val_from_getAttr",
},
},
want: "val_from_getAttr",
wantErr: false,
},
{
name: "No fallback if variable found in root",
template: "{{ myvar }}",
context: pongo2.Context{
// 'myvar' *is* defined in the root context, so we won't use the fallback.
"myvar": "root value",
"this": map[string]any{
"myvar": "fallback value",
},
},
want: "root value",
wantErr: false,
},
{
name: "Error if not in root nor in 'this'",
template: "{{ notfound }}",
context: pongo2.Context{},
errorMessage: "[Error (where: execution) in <string> | Line 1 Col 4 near 'notfound'] No value found for notfound",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tpl, err := pongo2.FromString(tt.template)
if err != nil {
t.Errorf("Error compiling template %q: %v", tt.template, err)
return
}
got, err := tpl.Execute(tt.context)

if err != nil {
// We expect an error
if !tt.wantErr {
t.Errorf("Unexpected error: %v", err)
return
}
// If we *do* expect an error, check message
if tt.errorMessage != "" && err.Error() != tt.errorMessage {
t.Errorf("Got error = %v, want %v", err.Error(), tt.errorMessage)
}
return
}

if tt.wantErr {
t.Errorf("Expected an error but got none")
return
}

if got != tt.want {
t.Errorf("Template.Execute() = %q; want %q", got, tt.want)
}
})
}
}

func TestTemplates(t *testing.T) {
// Add a global to the default set
pongo2.Globals["this_is_a_global_variable"] = "this is a global text"
Expand Down
2 changes: 1 addition & 1 deletion pongo2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"regexp"
"testing"

"github.com/flosch/pongo2/v6"
"github.com/rudderlabs/pongo2/v6"
)

var testSuite2 = pongo2.NewSet("test suite 2", pongo2.MustNewLocalFileSystemLoader(""))
Expand Down
118 changes: 94 additions & 24 deletions variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,50 @@
// context (e. g. information provided by tags, like the forloop)
val, inPrivate := ctx.Private[vr.parts[0].s]

if !inPrivate {
// Nothing found? Then have a final lookup in the public context
if inPrivate {
// We found it in private
currentPresent = true
} else {
// Not found in private? Then have a final lookup in the public context
val, currentPresent = ctx.Public[vr.parts[0].s]
}

current = reflect.ValueOf(val) // Get the initial value
// If not found in either private or public, try fallback to "this"
resolvedUsingThis := false
if !currentPresent {
part := vr.parts[0]
if part.typ == varTypeIdent {
valThis, inPrivateThis := ctx.Private["this"]
inPublicThis := false
if !inPrivateThis {
valThis, inPublicThis = ctx.Public["this"]
}
if inPrivateThis || inPublicThis {
fv := reflect.ValueOf(valThis)
if valuePtr, ok := valThis.(*Value); ok {
fv = valuePtr.val
}

resolved, found, usedAttr := tryResolveFieldMapOrAttr(fv, part.s)
if found {
current = resolved
currentPresent = true
if usedAttr {
assumeAttr = true
}
resolvedUsingThis = true
}
}
}
}

if !resolvedUsingThis {
if value, ok := val.(*Value); ok {
current = value.val
} else {
current = reflect.ValueOf(val) // Get the initial value
}
}

} else {
// Next parts, resolve it from current
Expand Down Expand Up @@ -345,29 +383,16 @@
current.Kind().String(), vr.String())
}
case varTypeIdent:
var tryField reflect.Value
// Calling a field or key
switch current.Kind() {
case reflect.Struct:
tryField = current.FieldByName(part.s)
case reflect.Map:
tryField = 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())
}
if tryField.IsValid() {
current = tryField
} else {
getAttr := current.MethodByName(getAttrMethodName)
if !getAttr.IsValid() && current.CanAddr() {
getAttr = current.Addr().MethodByName(getAttrMethodName)
}
if getAttr.IsValid() {
current = getAttr
currentPresent = true
nextVal, found, usedAttr := tryResolveFieldMapOrAttr(current, part.s)
if found {
current = nextVal
currentPresent = true
if usedAttr {
assumeAttr = true
}
} else {
return nil, fmt.Errorf("can't access a field/map key by name on type %s (variable %s)",
current.Kind().String(), vr.String())
}
case varTypeSubscript:
// Calling an index is only possible for:
Expand All @@ -379,7 +404,7 @@
return nil, err
}
si := sv.Integer()
if si >= 0 && current.Len() > si {

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / lint

undefined: tryField (typecheck)

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / lint

undefined: tryField) (typecheck)

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / test

undefined: tryField

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / test

undefined: tryField

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / test

undefined: tryField

Check failure on line 407 in variable.go

View workflow job for this annotation

GitHub Actions / test

undefined: tryField
current = current.Index(si)
} else {
// In Django, exceeding the length of a list is just empty.
Expand Down Expand Up @@ -647,7 +672,52 @@
return &Value{val: current, safe: isSafe}, nil
}

// tryResolveFieldMapOrAttr tries to resolve `name` from `val` by:
// 1. Struct field
// 2. Map key
// 3. getAttr method (if present)
// Returns (resolvedVal, found, usedAttr).
func tryResolveFieldMapOrAttr(val reflect.Value, name string) (reflect.Value, bool, bool) {
if !val.IsValid() {
return reflect.Value{}, false, false
}

// If val is a pointer, deref it
if val.Kind() == reflect.Ptr {
val = val.Elem()
if !val.IsValid() {
return reflect.Value{}, false, false
}
}

// 1. Struct field
if val.Kind() == reflect.Struct {
f := val.FieldByName(name)
if f.IsValid() {
return f, true, false
}
}

// 2. Map key
if val.Kind() == reflect.Map {
m := val.MapIndex(reflect.ValueOf(name))
if m.IsValid() {
return m, true, false
}
}

// 3. Attempt getAttr fallback
getAttr := val.MethodByName(getAttrMethodName)
if !getAttr.IsValid() && val.CanAddr() {
getAttr = val.Addr().MethodByName(getAttrMethodName)
}
return getAttr, getAttr.IsValid(), getAttr.IsValid()
}

func (vr *variableResolver) Evaluate(ctx *ExecutionContext) (*Value, *Error) {
if (vr.locationToken.Val == "inptu_material") {
fmt.Println("Hoho")
}
value, err := vr.resolve(ctx)
if err != nil {
return AsValue(nil), ctx.Error(err, vr.locationToken)
Expand Down
Loading