From 16ab435b1e10166858b8ef6195fd27c9b585621a Mon Sep 17 00:00:00 2001 From: gnoczinski Date: Fri, 4 Feb 2022 11:09:34 +0100 Subject: [PATCH 1/4] Added ability to customize the resolution of context contained files by: 1. `NamedFieldResolver.GetNamedField(name)` and `IndexedFieldResolver.GetIndexedField(index)` interfaces 2. Struct field `pongo2:"..."` 3. Added security enforcement to never use NOT exported fields. --- variable.go | 149 ++++++++++++++++++++++++----- variable_test.go | 237 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 362 insertions(+), 24 deletions(-) create mode 100644 variable_test.go diff --git a/variable.go b/variable.go index 5e12105..f60d394 100644 --- a/variable.go +++ b/variable.go @@ -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 @@ -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 + } } } @@ -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 { diff --git a/variable_test.go b/variable_test.go new file mode 100644 index 0000000..b2c65ef --- /dev/null +++ b/variable_test.go @@ -0,0 +1,237 @@ +package pongo2_test + +import ( + "errors" + "github.com/flosch/pongo2/v5" + "testing" +) + +func TestVariables_Named(t *testing.T) { + tests := map[string]struct { + template string + contextObject interface{} + want string + wantErr string + }{ + "ByReflection": { + template: "[{{ obj.Foo }}]", + contextObject: testVariablesStructSimple{Foo: "someFoo"}, + want: "[someFoo]", + }, + "ByReflectionMethod": { + template: "[{{ obj.GetBar }}]", + contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, + want: "[someBar]", + }, + "ByReflectionFunc": { + template: "[{{ obj.SomeFuncVar }}]", + contextObject: testVariablesStructSimple{SomeFuncVar: func() string { return "fromFunc" }}, + want: "[fromFunc]", + }, + "ByReflectionNotExported": { + template: "[{{ obj.hiddenBar }}]", + contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, + want: "[]", + }, + "ByReflectionMethodNotExported": { + template: "[{{ obj.hiddenGetBar }}]", + contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, + want: "[]", + }, + "ByNamed": { + template: "[{{ obj.foo }}]", + contextObject: testVariablesStructNamed{hiddenFoo: "someFoo"}, + want: "[someFoo]", + }, + "ByNamedFallback": { + template: "[{{ obj.Bar }}]", + contextObject: testVariablesStructNamed{Bar: "someBar"}, + want: "[someBar]", + }, + "ByNamedFailing": { + template: "[{{ obj.explode }}]", + contextObject: testVariablesStructNamed{}, + wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access field explode on type struct (variable obj.explode): expected", + }, + "ByNamedFunc": { + template: "[{{ obj.func }}]", + contextObject: testVariablesStructNamed{}, + want: "[fromFunc]", + }, + "ByNamedAliased": { + template: "[{{ obj.aliased }}]", + contextObject: testVariablesStructNamed{Aliased1: "expected"}, + want: "[expected]", + }, + "ByNamedAliasedConflicting": { + template: "[{{ obj.AliasedConflicting }}]", + contextObject: testVariablesStructNamed{Aliased2: "expected", AliasedConflicting: "not expected, because overwritten by Aliased2"}, + want: "[expected]", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tpl, _ := pongo2.FromString(tt.template) + got, err := tpl.Execute(map[string]interface{}{ + "obj": tt.contextObject, + }) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Template.Execute() error = %v, expected error: %v", err, tt.wantErr) + return + } + } + if got != tt.want { + t.Errorf("Template.Execute() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVariables_Indexed(t *testing.T) { + tests := map[string]struct { + template string + contextObject interface{} + want string + wantErr string + }{ + "ByReflection": { + template: "[{{ obj.1 }}]", + contextObject: []string{"a", "b", "c"}, + want: "[b]", + }, + "ByReflectionFunc": { + template: "[{{ obj.0 }}]", + contextObject: []interface{}{func() string { return "fromFunc" }}, + want: "[fromFunc]", + }, + "ByIndexedOnSlice": { + template: "[{{ obj.1 }}]", + contextObject: testVariablesSliceIndexed{"a", "b", "c"}, + want: "[theField]", + }, + "ByIndexedFallbackOnSlice": { + template: "[{{ obj.0 }}]", + contextObject: testVariablesSliceIndexed{"a", "b", "c"}, + want: "[a]", + }, + "ByIndexedFailingOnSlice": { + template: "[{{ obj.2 }}]", + contextObject: testVariablesSliceIndexed{"a", "b", "c"}, + wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access index 2 on type slice (variable obj.2): expected", + }, + "ByIndexedOnStruct": { + template: "[{{ obj.1 }}]", + contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, + want: "[someFoo]", + }, + "ByIndexedFallbackDoesNotWorkOnStruct": { + template: "[{{ obj.0 }}]", + contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, + wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access an index on type struct (variable obj.0)", + }, + "ByIndexedFailingOnStruct": { + template: "[{{ obj.2 }}]", + contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, + wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access index 2 on type struct (variable obj.2): expected", + }, + "ByIndexedFunc": { + template: "[{{ obj.3 }}]", + contextObject: testVariablesStructIndexed{}, + want: "[fromFunc]", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tpl, _ := pongo2.FromString(tt.template) + got, err := tpl.Execute(map[string]interface{}{ + "obj": tt.contextObject, + }) + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("Template.Execute() error = %v, expected error: %v", err, tt.wantErr) + return + } + } + if got != tt.want { + t.Errorf("Template.Execute() = %v, want %v", got, tt.want) + } + }) + } +} + +type testVariablesStructSimple struct { + Foo string + hiddenBar string + SomeFuncVar func() string +} + +func (tv testVariablesStructSimple) GetBar() string { + return tv.hiddenBar +} + +func (tv testVariablesStructSimple) hiddenGetBar() string { + return tv.hiddenBar +} + +type testVariablesStructNamed struct { + hiddenFoo string + Bar string + + Aliased1 string `pongo2:"aliased"` + Aliased2 string `pongo2:"AliasedConflicting"` + AliasedConflicting string +} + +func (tv testVariablesStructNamed) GetNamedField(s string) (interface{}, error) { + switch s { + case "foo": + return tv.hiddenFoo, nil + case "explode": + return nil, errTestExpected + case "func": + return func() string { + return "fromFunc" + }, nil + default: + return nil, pongo2.ErrNoSuchField + } +} + +type testVariablesStructIndexed struct { + hiddenFoo string +} + +func (tv testVariablesStructIndexed) GetIndexedField(s int) (interface{}, error) { + switch s { + case 1: + return tv.hiddenFoo, nil + case 2: + return nil, errTestExpected + case 3: + return func() string { + return "fromFunc" + }, nil + default: + return nil, pongo2.ErrNoSuchField + } +} + +type testVariablesSliceIndexed []string + +func (tv testVariablesSliceIndexed) GetIndexedField(s int) (interface{}, error) { + switch s { + case 1: + return "theField", nil + case 2: + return nil, errTestExpected + default: + return nil, pongo2.ErrNoSuchField + } +} + +var ( + errTestExpected = errors.New("expected") +) From 0d6befe0d0a95e5ff88972f646d4453eef446b92 Mon Sep 17 00:00:00 2001 From: gnoczinski Date: Fri, 4 Feb 2022 12:25:07 +0100 Subject: [PATCH 2/4] Added documentation --- README.md | 1 + context.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/README.md b/README.md index 54c95a6..1816b7c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/context.go b/context.go index dbc5e3e..e388253 100644 --- a/context.go +++ b/context.go @@ -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 { From 6ac2780c86d131255e60cac5ad482005906e20ac Mon Sep 17 00:00:00 2001 From: gnoczinski Date: Fri, 4 Feb 2022 12:37:16 +0100 Subject: [PATCH 3/4] Reduced code duplication? --- variable_test.go | 68 ++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 48 deletions(-) diff --git a/variable_test.go b/variable_test.go index b2c65ef..8869ee7 100644 --- a/variable_test.go +++ b/variable_test.go @@ -13,130 +13,102 @@ func TestVariables_Named(t *testing.T) { want string wantErr string }{ - "ByReflection": { + "Named_ByReflection": { template: "[{{ obj.Foo }}]", contextObject: testVariablesStructSimple{Foo: "someFoo"}, want: "[someFoo]", }, - "ByReflectionMethod": { + "Named_ByReflectionMethod": { template: "[{{ obj.GetBar }}]", contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, want: "[someBar]", }, - "ByReflectionFunc": { + "Named_ByReflectionFunc": { template: "[{{ obj.SomeFuncVar }}]", contextObject: testVariablesStructSimple{SomeFuncVar: func() string { return "fromFunc" }}, want: "[fromFunc]", }, - "ByReflectionNotExported": { + "Named_ByReflectionNotExported": { template: "[{{ obj.hiddenBar }}]", contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, want: "[]", }, - "ByReflectionMethodNotExported": { + "Named_ByReflectionMethodNotExported": { template: "[{{ obj.hiddenGetBar }}]", contextObject: testVariablesStructSimple{hiddenBar: "someBar"}, want: "[]", }, - "ByNamed": { + "Named_ByNamed": { template: "[{{ obj.foo }}]", contextObject: testVariablesStructNamed{hiddenFoo: "someFoo"}, want: "[someFoo]", }, - "ByNamedFallback": { + "Named_ByNamedFallback": { template: "[{{ obj.Bar }}]", contextObject: testVariablesStructNamed{Bar: "someBar"}, want: "[someBar]", }, - "ByNamedFailing": { + "Named_ByNamedFailing": { template: "[{{ obj.explode }}]", contextObject: testVariablesStructNamed{}, wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access field explode on type struct (variable obj.explode): expected", }, - "ByNamedFunc": { + "Named_ByNamedFunc": { template: "[{{ obj.func }}]", contextObject: testVariablesStructNamed{}, want: "[fromFunc]", }, - "ByNamedAliased": { + "Named_ByNamedAliased": { template: "[{{ obj.aliased }}]", contextObject: testVariablesStructNamed{Aliased1: "expected"}, want: "[expected]", }, - "ByNamedAliasedConflicting": { + "Named_ByNamedAliasedConflicting": { template: "[{{ obj.AliasedConflicting }}]", contextObject: testVariablesStructNamed{Aliased2: "expected", AliasedConflicting: "not expected, because overwritten by Aliased2"}, want: "[expected]", }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - tpl, _ := pongo2.FromString(tt.template) - got, err := tpl.Execute(map[string]interface{}{ - "obj": tt.contextObject, - }) - if err != nil { - if err.Error() != tt.wantErr { - t.Errorf("Template.Execute() error = %v, expected error: %v", err, tt.wantErr) - return - } - } - if got != tt.want { - t.Errorf("Template.Execute() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestVariables_Indexed(t *testing.T) { - tests := map[string]struct { - template string - contextObject interface{} - want string - wantErr string - }{ - "ByReflection": { + "Indexed_ByReflection": { template: "[{{ obj.1 }}]", contextObject: []string{"a", "b", "c"}, want: "[b]", }, - "ByReflectionFunc": { + "Indexed_ByReflectionFunc": { template: "[{{ obj.0 }}]", contextObject: []interface{}{func() string { return "fromFunc" }}, want: "[fromFunc]", }, - "ByIndexedOnSlice": { + "Indexed_ByIndexedOnSlice": { template: "[{{ obj.1 }}]", contextObject: testVariablesSliceIndexed{"a", "b", "c"}, want: "[theField]", }, - "ByIndexedFallbackOnSlice": { + "Indexed_ByIndexedFallbackOnSlice": { template: "[{{ obj.0 }}]", contextObject: testVariablesSliceIndexed{"a", "b", "c"}, want: "[a]", }, - "ByIndexedFailingOnSlice": { + "Indexed_ByIndexedFailingOnSlice": { template: "[{{ obj.2 }}]", contextObject: testVariablesSliceIndexed{"a", "b", "c"}, wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access index 2 on type slice (variable obj.2): expected", }, - "ByIndexedOnStruct": { + "Indexed_ByIndexedOnStruct": { template: "[{{ obj.1 }}]", contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, want: "[someFoo]", }, - "ByIndexedFallbackDoesNotWorkOnStruct": { + "Indexed_ByIndexedFallbackDoesNotWorkOnStruct": { template: "[{{ obj.0 }}]", contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access an index on type struct (variable obj.0)", }, - "ByIndexedFailingOnStruct": { + "Indexed_ByIndexedFailingOnStruct": { template: "[{{ obj.2 }}]", contextObject: testVariablesStructIndexed{hiddenFoo: "someFoo"}, wantErr: "[Error (where: execution) in | Line 1 Col 5 near 'obj'] can't access index 2 on type struct (variable obj.2): expected", }, - "ByIndexedFunc": { + "Indexed_ByIndexedFunc": { template: "[{{ obj.3 }}]", contextObject: testVariablesStructIndexed{}, want: "[fromFunc]", From 813e9dc7895d126538a6d52681926184f9f67574 Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Tue, 17 Dec 2024 14:44:46 +0100 Subject: [PATCH 4/4] Applied changes required by changes from upstream main. --- .semaphore/semaphore.yml | 36 +++++++++++++------------- variable.go | 55 +++++++++++++++++++++++++++++++++++----- variable_test.go | 3 ++- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 4a6cf64..1cb906b 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -1,18 +1,18 @@ -version: v1.0 -name: Go -agent: - machine: - type: e1-standard-2 - os_image: ubuntu2004 -blocks: - - name: Test - task: - jobs: - - name: go test - commands: - - sem-version go 1.18 - - export GOPATH=~/go - - 'export PATH=/home/semaphore/go/bin:$PATH' - - checkout - - go get ./... - - go test ./... +version: v1.0 +name: Go +agent: + machine: + type: e1-standard-2 + os_image: ubuntu2004 +blocks: + - name: Test + task: + jobs: + - name: go test + commands: + - sem-version go 1.18 + - export GOPATH=~/go + - 'export PATH=/home/semaphore/go/bin:$PATH' + - checkout + - go get ./... + - go test ./... diff --git a/variable.go b/variable.go index 5002b37..07522cb 100644 --- a/variable.go +++ b/variable.go @@ -328,20 +328,19 @@ 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) + var resolver func(*ExecutionContext, reflect.Value, *variablePart) (_ reflect.Value, done bool, _ error) switch part.typ { case varTypeInt: resolver = vr.resolveIndexedField case varTypeIdent: - // debugging: - // fmt.Printf("now = %s (kind: %s)\n", part.s, current.Kind().String()) - resolver = vr.resolveNamedField + case varTypeSubscript: + resolver = vr.resolveSubscriptField default: panic("unimplemented") } - if v, done, err := resolver(current, part); err != nil { + if v, done, err := resolver(ctx, current, part); err != nil { return nil, err } else if done { return &Value{val: v}, nil @@ -498,7 +497,7 @@ 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) { +func (vr *variableResolver) resolveNamedField(_ *ExecutionContext, 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 { @@ -558,7 +557,7 @@ func (vr *variableResolver) resolveStructFieldTag(of reflect.StructField) (alias return strings.TrimSpace(plainParts[0]) } -func (vr *variableResolver) resolveIndexedField(of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) { +func (vr *variableResolver) resolveIndexedField(_ *ExecutionContext, 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 { @@ -587,6 +586,48 @@ func (vr *variableResolver) resolveIndexedField(of reflect.Value, by *variablePa } } +func (vr *variableResolver) resolveSubscriptField(ctx *ExecutionContext, of reflect.Value, by *variablePart) (_ reflect.Value, done bool, _ error) { + // Calling an index is only possible for: + // * slices/arrays/strings + switch of.Kind() { + case reflect.String, reflect.Array, reflect.Slice: + sv, err := by.subscript.Evaluate(ctx) + if err != nil { + return reflect.Value{}, false, err + } + si := sv.Integer() + if si >= 0 && of.Len() > si { + return of.Index(si), false, nil + } else { + // In Django, exceeding the length of a list is just empty. + return reflect.ValueOf(nil), true, nil + } + // Calling a field or key + case reflect.Struct: + sv, err := by.subscript.Evaluate(ctx) + if err != nil { + return reflect.Value{}, false, err + } + return of.FieldByName(sv.String()), false, nil + case reflect.Map: + sv, err := by.subscript.Evaluate(ctx) + if err != nil { + return reflect.Value{}, false, err + } + if sv.IsNil() { + return reflect.ValueOf(nil), true, nil + } + if sv.val.Type().AssignableTo(of.Type().Key()) { + return of.MapIndex(sv.val), false, nil + } else { + 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 { diff --git a/variable_test.go b/variable_test.go index 8869ee7..ab6b3b6 100644 --- a/variable_test.go +++ b/variable_test.go @@ -2,8 +2,9 @@ package pongo2_test import ( "errors" - "github.com/flosch/pongo2/v5" "testing" + + "github.com/flosch/pongo2/v6" ) func TestVariables_Named(t *testing.T) {