From fa8ac87d5edfe3435e21579d19e86bba3be2089c Mon Sep 17 00:00:00 2001 From: Tatsushi Demachi Date: Mon, 29 Dec 2014 11:33:12 +0900 Subject: [PATCH] Make 'where' template function accepts dot chaining key argument 'where' template function used to accept only each element's struct field name, method name and map key name as its second argument. This extends it to accept dot chaining key like 'Params.foo.bar' as the argument. It evaluates sub elements of each array elements and checks it matches the third argument value. Typical use case would be for filtering Pages by user defined front matter value. For example, to filter pages which have 'Params.foo.bar' and its value is 'baz', it is used like {{ range where .Data.Pages "Params.foo.bar" "baz" }} {{ .Content }} {{ end }} It ignores all leading and trailing dots so it can also be used with ".Params.foo.bar" --- docs/content/templates/functions.md | 13 +++ tpl/template.go | 151 +++++++++++++++++++--------- tpl/template_test.go | 119 ++++++++++++++++++++-- 3 files changed, 227 insertions(+), 56 deletions(-) diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index 819a5c7a9..4138ffde9 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -66,6 +66,19 @@ e.g. {{ .Content}} {{ end }} +It can be used with dot chaining second argument to refer a nested element of a value. + +e.g. + + // Front matter on some pages + +++ + series: golang + +++ + + {{ range where .Site.Recent "Params.series" "golang" }} + {{ .Content}} + {{ end }} + *where and first can be stacked* e.g. diff --git a/tpl/template.go b/tpl/template.go index aef6c3ba6..1b8107f37 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -289,6 +289,19 @@ func In(l interface{}, v interface{}) bool { return false } +// indirect is taken from 'text/template/exec.go' +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} + // First is exposed to templates, to iterate over the first N items in a // rangeable list. func First(limit interface{}, seq interface{}) (interface{}, error) { @@ -326,76 +339,122 @@ func First(limit interface{}, seq interface{}) (interface{}, error) { return seqv.Slice(0, limitv).Interface(), nil } -func Where(seq, key, match interface{}) (interface{}, error) { +var ( + zero reflect.Value + errorType = reflect.TypeOf((*error)(nil)).Elem() +) + +func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) { + if !obj.IsValid() { + return zero, errors.New("can't evaluate an invalid value") + } + typ := obj.Type() + obj, isNil := indirect(obj) + + // first, check whether obj has a method. In this case, obj is + // an interface, a struct or its pointer. If obj is a struct, + // to check all T and *T method, use obj pointer type Value + objPtr := obj + if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { + objPtr = objPtr.Addr() + } + mt, ok := objPtr.Type().MethodByName(elemName) + if ok { + if mt.PkgPath != "" { + return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ) + } + // struct pointer has one receiver argument and interface doesn't have an argument + if mt.Type.NumIn() > 1 || mt.Type.NumOut() == 0 || mt.Type.NumOut() > 2 { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 1 && mt.Type.Out(0).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + if mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType) { + return zero, fmt.Errorf("%s is a method of type %s but doesn't satisfy requirements", elemName, typ) + } + res := objPtr.Method(mt.Index).Call([]reflect.Value{}) + if len(res) == 2 && !res[1].IsNil() { + return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error)) + } + return res[0], nil + } + + // elemName isn't a method so next start to check whether it is + // a struct field or a map value. In both cases, it mustn't be + // a nil value + if isNil { + return zero, fmt.Errorf("can't evaluate a nil pointer of type %s by a struct field or map key name %s", typ, elemName) + } + switch obj.Kind() { + case reflect.Struct: + ft, ok := obj.Type().FieldByName(elemName) + if ok { + if ft.PkgPath != "" { + return zero, fmt.Errorf("%s is an unexported field of struct type %s", elemName, typ) + } + return obj.FieldByIndex(ft.Index), nil + } + return zero, fmt.Errorf("%s isn't a field of struct type %s", elemName, typ) + case reflect.Map: + kv := reflect.ValueOf(elemName) + if kv.Type().AssignableTo(obj.Type().Key()) { + return obj.MapIndex(kv), nil + } + return zero, fmt.Errorf("%s isn't a key of map type %s", elemName, typ) + } + return zero, fmt.Errorf("%s is neither a struct field, a method nor a map element of type %s", elemName, typ) +} + +func Where(seq, key, match interface{}) (r interface{}, err error) { seqv := reflect.ValueOf(seq) kv := reflect.ValueOf(key) mv := reflect.ValueOf(match) - // this is better than my first pass; ripped from text/template/exec.go indirect(): - for ; seqv.Kind() == reflect.Ptr || seqv.Kind() == reflect.Interface; seqv = seqv.Elem() { - if seqv.IsNil() { - return nil, errors.New("can't iterate over a nil value") - } - if seqv.Kind() == reflect.Interface && seqv.NumMethod() > 0 { - break - } + seqv, isNil := indirect(seqv) + if isNil { + return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(seq).Type().String()) + } + + var path []string + if kv.Kind() == reflect.String { + path = strings.Split(strings.Trim(kv.String(), "."), ".") } switch seqv.Kind() { case reflect.Array, reflect.Slice: - r := reflect.MakeSlice(seqv.Type(), 0, 0) + rv := reflect.MakeSlice(seqv.Type(), 0, 0) for i := 0; i < seqv.Len(); i++ { var vvv reflect.Value - vv := seqv.Index(i) - switch vv.Kind() { - case reflect.Map: - if kv.Type() == vv.Type().Key() && vv.MapIndex(kv).IsValid() { + rvv := seqv.Index(i) + if kv.Kind() == reflect.String { + vvv = rvv + for _, elemName := range path { + vvv, err = evaluateSubElem(vvv, elemName) + if err != nil { + return nil, err + } + } + } else { + vv, _ := indirect(rvv) + if vv.Kind() == reflect.Map && kv.Type().AssignableTo(vv.Type().Key()) { vvv = vv.MapIndex(kv) } - case reflect.Struct: - if kv.Kind() == reflect.String { - method := vv.MethodByName(kv.String()) - if method.IsValid() && method.Type().NumIn() == 0 && method.Type().NumOut() > 0 { - vvv = method.Call(nil)[0] - } else if vv.FieldByName(kv.String()).IsValid() { - vvv = vv.FieldByName(kv.String()) - } - } - case reflect.Ptr: - if !vv.IsNil() { - ev := vv.Elem() - switch ev.Kind() { - case reflect.Map: - if kv.Type() == ev.Type().Key() && ev.MapIndex(kv).IsValid() { - vvv = ev.MapIndex(kv) - } - case reflect.Struct: - if kv.Kind() == reflect.String { - method := vv.MethodByName(kv.String()) - if method.IsValid() && method.Type().NumIn() == 0 && method.Type().NumOut() > 0 { - vvv = method.Call(nil)[0] - } else if ev.FieldByName(kv.String()).IsValid() { - vvv = ev.FieldByName(kv.String()) - } - } - } - } } - if vvv.IsValid() && mv.Type() == vvv.Type() { switch mv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if mv.Int() == vvv.Int() { - r = reflect.Append(r, vv) + rv = reflect.Append(rv, rvv) } case reflect.String: if mv.String() == vvv.String() { - r = reflect.Append(r, vv) + rv = reflect.Append(rv, rvv) } } } } - return r.Interface(), nil + return rv.Interface(), nil default: return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String()) } diff --git a/tpl/template_test.go b/tpl/template_test.go index 30d721b6c..00327ef76 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -1,6 +1,8 @@ package tpl import ( + "errors" + "fmt" "html/template" "reflect" "testing" @@ -305,8 +307,85 @@ func (x TstX) TstRv() string { return "r" + x.B } +func (x TstX) unexportedMethod() string { + return x.unexported +} + +func (x TstX) MethodWithArg(s string) string { + return s +} + +func (x TstX) MethodReturnNothing() {} + +func (x TstX) MethodReturnErrorOnly() error { + return errors.New("something error occured") +} + +func (x TstX) MethodReturnTwoValues() (string, string) { + return "foo", "bar" +} + +func (x TstX) MethodReturnValueWithError() (string, error) { + return "", errors.New("something error occured") +} + +func (x TstX) String() string { + return fmt.Sprintf("A: %s, B: %s", x.A, x.B) +} + type TstX struct { A, B string + unexported string +} + +func TestEvaluateSubElem(t *testing.T) { + tstx := TstX{A: "foo", B: "bar"} + var inner struct { + S fmt.Stringer + } + inner.S = tstx + interfaceValue := reflect.ValueOf(&inner).Elem().Field(0) + + for i, this := range []struct { + value reflect.Value + key string + expect interface{} + }{ + {reflect.ValueOf(tstx), "A", "foo"}, + {reflect.ValueOf(&tstx), "TstRp", "rfoo"}, + {reflect.ValueOf(tstx), "TstRv", "rbar"}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1, "foo"}, + {reflect.ValueOf(map[string]string{"key1": "foo", "key2": "bar"}), "key1", "foo"}, + {interfaceValue, "String", "A: foo, B: bar"}, + {reflect.Value{}, "foo", false}, + //{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), 1.2, false}, + {reflect.ValueOf(tstx), "unexported", false}, + {reflect.ValueOf(tstx), "unexportedMethod", false}, + {reflect.ValueOf(tstx), "MethodWithArg", false}, + {reflect.ValueOf(tstx), "MethodReturnNothing", false}, + {reflect.ValueOf(tstx), "MethodReturnErrorOnly", false}, + {reflect.ValueOf(tstx), "MethodReturnTwoValues", false}, + {reflect.ValueOf(tstx), "MethodReturnValueWithError", false}, + {reflect.ValueOf((*TstX)(nil)), "A", false}, + {reflect.ValueOf(tstx), "C", false}, + {reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false}, + {reflect.ValueOf([]string{"foo", "bar"}), "1", false}, + } { + result, err := evaluateSubElem(this.value, this.key) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] evaluateSubElem didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result.Kind() != reflect.String || result.String() != this.expect { + t.Errorf("[%d] evaluateSubElem with %v got %v but expected %v", i, this.key, result, this.expect) + } + } + } } func TestWhere(t *testing.T) { @@ -314,6 +393,10 @@ func TestWhere(t *testing.T) { //page1 := &Page{contentType: "v", Source: Source{File: *source.NewFile("/x/y/z/source.md")}} //page2 := &Page{contentType: "w", Source: Source{File: *source.NewFile("/y/z/a/source.md")}} + type Mid struct { + Tst TstX + } + for i, this := range []struct { sequence interface{} key interface{} @@ -322,21 +405,37 @@ func TestWhere(t *testing.T) { }{ {[]map[int]string{{1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}}, 2, "m", []map[int]string{{1: "a", 2: "m"}}}, {[]map[string]int{{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}}, "b", 4, []map[string]int{{"a": 3, "b": 4}}}, - {[]TstX{{"a", "b"}, {"c", "d"}, {"e", "f"}}, "B", "f", []TstX{{"e", "f"}}}, + {[]TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}}, "B", "f", []TstX{{A: "e", B: "f"}}}, {[]*map[int]string{&map[int]string{1: "a", 2: "m"}, &map[int]string{1: "c", 2: "d"}, &map[int]string{1: "e", 3: "m"}}, 2, "m", []*map[int]string{&map[int]string{1: "a", 2: "m"}}}, - {[]*TstX{&TstX{"a", "b"}, &TstX{"c", "d"}, &TstX{"e", "f"}}, "B", "f", []*TstX{&TstX{"e", "f"}}}, - {[]*TstX{&TstX{"a", "b"}, &TstX{"c", "d"}, &TstX{"e", "c"}}, "TstRp", "rc", []*TstX{&TstX{"c", "d"}}}, - {[]TstX{TstX{"a", "b"}, TstX{"c", "d"}, TstX{"e", "c"}}, "TstRv", "rc", []TstX{TstX{"e", "c"}}}, + {[]*TstX{&TstX{A: "a", B: "b"}, &TstX{A: "c", B: "d"}, &TstX{A: "e", B: "f"}}, "B", "f", []*TstX{&TstX{A: "e", B: "f"}}}, + {[]*TstX{&TstX{A: "a", B: "b"}, &TstX{A: "c", B: "d"}, &TstX{A: "e", B: "c"}}, "TstRp", "rc", []*TstX{&TstX{A: "c", B: "d"}}}, + {[]TstX{TstX{A: "a", B: "b"}, TstX{A: "c", B: "d"}, TstX{A: "e", B: "c"}}, "TstRv", "rc", []TstX{TstX{A: "e", B: "c"}}}, + {[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, "foo.B", "d", []map[string]TstX{{"foo": TstX{A: "c", B: "d"}}}}, + {[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, ".foo.B", "d", []map[string]TstX{{"foo": TstX{A: "c", B: "d"}}}}, + {[]map[string]TstX{{"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}}, "foo.TstRv", "rd", []map[string]TstX{{"foo": TstX{A: "c", B: "d"}}}}, + {[]map[string]*TstX{{"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}}, "foo.TstRp", "rc", []map[string]*TstX{{"foo": &TstX{A: "c", B: "d"}}}}, + {[]map[string]Mid{{"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}}, "foo.Tst.B", "d", []map[string]Mid{{"foo": Mid{Tst: TstX{A: "c", B: "d"}}}}}, + {[]map[string]Mid{{"foo": Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": Mid{Tst: TstX{A: "e", B: "f"}}}}, "foo.Tst.TstRv", "rd", []map[string]Mid{{"foo": Mid{Tst: TstX{A: "c", B: "d"}}}}}, + {[]map[string]*Mid{{"foo": &Mid{Tst: TstX{A: "a", B: "b"}}}, {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, {"foo": &Mid{Tst: TstX{A: "e", B: "f"}}}}, "foo.Tst.TstRp", "rc", []map[string]*Mid{{"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}}}, + {(*[]TstX)(nil), "A", "a", false}, + {TstX{A: "a", B: "b"}, "A", "a", false}, + {[]map[string]*TstX{{"foo": nil}}, "foo.B", "d", false}, //{[]*Page{page1, page2}, "Type", "v", []*Page{page1}}, //{[]*Page{page1, page2}, "Section", "y", []*Page{page2}}, } { results, err := Where(this.sequence, this.key, this.match) - if err != nil { - t.Errorf("[%d] failed: %s", i, err) - continue - } - if !reflect.DeepEqual(results, this.expect) { - t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, this.key, this.match, results, this.expect) + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Where didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if !reflect.DeepEqual(results, this.expect) { + t.Errorf("[%d] Where clause matching %v with %v, got %v but expected %v", i, this.key, this.match, results, this.expect) + } } } }