Skip to content

Commit e3df71f

Browse files
committed
exp/template: support field syntax on maps
While using exp/template in practice, the syntax for indexing values using the "index" action was found to be very inconvenient for frequent use when handling dynamic data from maps such as the ones used with json and yaml, that use a type like map[string]interface{}. For these kinds of maps, the default handling of fields as {{.Field}} makes the task of handling the several references significantly more pleasant and elegant, and is equivalent to what's currently done in the "template" package and in other external packages (e.g. mustache). Even with this change, the index action is still relevant as it allows indexing maps in other scenarios where keys wouldn't be valid field names. R=golang-dev, r, gustavo CC=golang-dev https://golang.org/cl/4898043
1 parent e3f3a54 commit e3df71f

File tree

3 files changed

+37
-3
lines changed

3 files changed

+37
-3
lines changed

src/pkg/exp/template/doc.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ An argument is a simple value, denoted by one of the following.
101101
.Field1.Field2
102102
Fields can also be evaluated on variables, including chaining:
103103
$x.Field1.Field2
104+
- The name of a key of the data, which must be a map, preceded
105+
by a period, such as
106+
.Key
107+
The result is the map element value indexed by the key.
108+
Key invocations may be chained and combined with fields to any
109+
depth:
110+
.Field1.Key1.Field2.Key2
111+
Although the key must be an alphanumeric identifier, unlike with
112+
field names they do not need to start with an upper case letter.
113+
Keys can also be evaluated on variables, including chaining:
114+
$x.key1.key2
104115
- The name of a niladic method of the data, preceded by a period,
105116
such as
106117
.Method
@@ -109,9 +120,9 @@ An argument is a simple value, denoted by one of the following.
109120
any type) or two return values, the second of which is an os.Error.
110121
If it has two and the returned error is non-nil, execution terminates
111122
and an error is returned to the caller as the value of Execute.
112-
Method invocations may be chained and combined with fields
123+
Method invocations may be chained and combined with fields and keys
113124
to any depth:
114-
.Field1.Method1.Field2.Method2
125+
.Field1.Key1.Method1.Field2.Key2.Method2
115126
Methods can also be evaluated on variables, including chaining:
116127
$x.Method1.Field
117128
- The name of a niladic function, such as

src/pkg/exp/template/exec.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,20 +394,31 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
394394
if method, ok := methodByName(ptr, fieldName); ok {
395395
return s.evalCall(dot, method, fieldName, args, final)
396396
}
397+
hasArgs := len(args) > 1 || final.IsValid()
397398
// It's not a method; is it a field of a struct?
398399
receiver, isNil := indirect(receiver)
399400
if receiver.Kind() == reflect.Struct {
400401
tField, ok := receiver.Type().FieldByName(fieldName)
401402
if ok {
402403
field := receiver.FieldByIndex(tField.Index)
403-
if len(args) > 1 || final.IsValid() {
404+
if hasArgs {
404405
s.errorf("%s is not a method but has arguments", fieldName)
405406
}
406407
if tField.PkgPath == "" { // field is exported
407408
return field
408409
}
409410
}
410411
}
412+
// If it's a map, attempt to use the field name as a key.
413+
if receiver.Kind() == reflect.Map {
414+
nameVal := reflect.ValueOf(fieldName)
415+
if nameVal.Type().AssignableTo(receiver.Type().Key()) {
416+
if hasArgs {
417+
s.errorf("%s is not a method but has arguments", fieldName)
418+
}
419+
return receiver.MapIndex(nameVal)
420+
}
421+
}
411422
if isNil {
412423
s.errorf("nil pointer evaluating %s.%s", typ, fieldName)
413424
}

src/pkg/exp/template/exec_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type T struct {
3939
MSI map[string]int
4040
MSIone map[string]int // one element, for deterministic output
4141
MSIEmpty map[string]int
42+
MXI map[interface{}]int
43+
MII map[int]int
4244
SMSI []map[string]int
4345
// Empty interfaces; used to see if we can dig inside one.
4446
Empty0 interface{} // nil
@@ -85,6 +87,8 @@ var tVal = &T{
8587
SB: []bool{true, false},
8688
MSI: map[string]int{"one": 1, "two": 2, "three": 3},
8789
MSIone: map[string]int{"one": 1},
90+
MXI: map[interface{}]int{"one": 1},
91+
MII: map[int]int{1: 1},
8892
SMSI: []map[string]int{
8993
{"one": 1, "two": 2},
9094
{"eleven": 11, "twelve": 12},
@@ -211,6 +215,14 @@ var execTests = []execTest{
211215
{".X", "-{{.X}}-", "-x-", tVal, true},
212216
{".U.V", "-{{.U.V}}-", "-v-", tVal, true},
213217

218+
// Fields on maps.
219+
{"map .one", "{{.MSI.one}}", "1", tVal, true},
220+
{"map .two", "{{.MSI.two}}", "2", tVal, true},
221+
{"map .NO", "{{.MSI.NO}}", "<no value>", tVal, true},
222+
{"map .one interface", "{{.MXI.one}}", "1", tVal, true},
223+
{"map .WRONG args", "{{.MSI.one 1}}", "", tVal, false},
224+
{"map .WRONG type", "{{.MII.one}}", "", tVal, false},
225+
214226
// Dots of all kinds to test basic evaluation.
215227
{"dot int", "<{{.}}>", "<13>", 13, true},
216228
{"dot uint", "<{{.}}>", "<14>", uint(14), true},

0 commit comments

Comments
 (0)