]> Cypherpunks repositories - gostls13.git/commitdiff
add the ability to invoke niladic single-valued methods in templates.
authorRob Pike <r@golang.org>
Wed, 16 Dec 2009 11:10:50 +0000 (03:10 -0800)
committerRob Pike <r@golang.org>
Wed, 16 Dec 2009 11:10:50 +0000 (03:10 -0800)
Fixes #389.

R=rsc
CC=golang-dev
https://golang.org/cl/180061

src/pkg/template/template.go
src/pkg/template/template_test.go

index 8e39d802eec909d06fa6dda439fdbd1095d28a82..b46d28613c6fba05ca6ab724d1389e289ec6871c 100644 (file)
        Data items may be values or pointers; the interface hides the
        indirection.
 
+       In the following, 'field' is one of several things, according to the data.
+       - the name of a field of a struct (result = data.field)
+       - the value stored in a map under that key (result = data[field])
+       - the result of invoking a niladic single-valued method with that name
+          (result = data.field())
+
        Major constructs ({} are metacharacters; [] marks optional elements):
 
                {# comment }
@@ -604,8 +610,8 @@ func (st *state) findVar(s string) reflect.Value {
                return st.data
        }
        data := st.data
-       elems := strings.Split(s, ".", 0)
-       for i := 0; i < len(elems); i++ {
+       for _, elem := range strings.Split(s, ".", 0) {
+               origData := data // for method lookup need value before indirection.
                // Look up field; data must be a struct or map.
                data = reflect.Indirect(data)
                if data == nil {
@@ -614,20 +620,73 @@ func (st *state) findVar(s string) reflect.Value {
 
                switch typ := data.Type().(type) {
                case *reflect.StructType:
-                       field, ok := typ.FieldByName(elems[i])
-                       if !ok {
-                               return nil
+                       if field, ok := typ.FieldByName(elem); ok {
+                               data = data.(*reflect.StructValue).FieldByIndex(field.Index)
+                               continue
                        }
-                       data = data.(*reflect.StructValue).FieldByIndex(field.Index)
                case *reflect.MapType:
-                       data = data.(*reflect.MapValue).Elem(reflect.NewValue(elems[i]))
-               default:
-                       return nil
+                       data = data.(*reflect.MapValue).Elem(reflect.NewValue(elem))
+                       continue
+               }
+
+               // No luck with that name; is it a method?
+               if result, found := callMethod(origData, elem); found {
+                       data = result
+                       continue
                }
+               return nil
        }
        return data
 }
 
+// See if name is a method of the value at some level of indirection.
+// The return values are the result of the call (which may be nil if
+// there's trouble) and whether a method of the right name exists with
+// any signature.
+func callMethod(data reflect.Value, name string) (result reflect.Value, found bool) {
+       found = false
+       // Method set depends on pointerness, and the value may be arbitrarily
+       // indirect.  Simplest approach is to walk down the pointer chain and
+       // see if we can find the method at each step.
+       // Most steps will see NumMethod() == 0.
+       for {
+               typ := data.Type()
+               if nMethod := data.Type().NumMethod(); nMethod > 0 {
+                       for i := 0; i < nMethod; i++ {
+                               method := typ.Method(i)
+                               if method.Name == name {
+                                       found = true // we found the name regardless
+                                       // does receiver type match? (pointerness might be off)
+                                       if typ == method.Type.In(0) {
+                                               return call(data, method), found
+                                       }
+                               }
+                       }
+               }
+               if nd, ok := data.(*reflect.PtrValue); ok {
+                       data = nd.Elem()
+               } else {
+                       break
+               }
+       }
+       return
+}
+
+// Invoke the method. If its signature is wrong, return nil.
+func call(v reflect.Value, method reflect.Method) reflect.Value {
+       funcType := method.Type
+       // Method must take no arguments, meaning as a func it has one argument (the receiver)
+       if funcType.NumIn() != 1 {
+               return nil
+       }
+       // Method must return a single value.
+       if funcType.NumOut() != 1 {
+               return nil
+       }
+       // Result will be the zeroth element of the returned slice.
+       return method.Func.Call([]reflect.Value{v})[0]
+}
+
 // Is there no data to look at?
 func empty(v reflect.Value) bool {
        v = reflect.Indirect(v)
@@ -649,7 +708,7 @@ func empty(v reflect.Value) bool {
        return true
 }
 
-// Look up a variable, up through the parent if necessary.
+// Look up a variable or method, up through the parent if necessary.
 func (t *Template) varValue(name string, st *state) reflect.Value {
        field := st.findVar(name)
        if field == nil {
index aa156d2f8f7e4fb37480a1f0dbf39935ee2f27d2..c2bc5125fa142bad59fc624ed1a2faa64c0aa2ef 100644 (file)
@@ -45,6 +45,10 @@ type S struct {
        bytes         []byte
 }
 
+func (s *S) pointerMethod() string { return "ptrmethod!" }
+
+func (s S) valueMethod() string { return "valmethod!" }
+
 var t1 = T{"ItemNumber1", "ValueNumber1"}
 var t2 = T{"ItemNumber2", "ValueNumber2"}
 
@@ -95,6 +99,19 @@ var tests = []*Test{
                out: "Header=77\n",
        },
 
+       // Method at top level
+       &Test{
+               in: "ptrmethod={pointerMethod}\n",
+
+               out: "ptrmethod=ptrmethod!\n",
+       },
+
+       &Test{
+               in: "valmethod={valueMethod}\n",
+
+               out: "valmethod=valmethod!\n",
+       },
+
        // Section
        &Test{
                in: "{.section data }\n" +