From 0d17ee7ed4331513adb6e08f5697c4d803155655 Mon Sep 17 00:00:00 2001 From: Tatsushi Demachi Date: Sun, 4 Jan 2015 14:24:58 +0900 Subject: [PATCH] Add operator argument to `where` template function It allows to use `where` template function like SQL `where` clause. For example, {{ range where .Data.Pages "Type" "!=" "post" }} {{ .Content }} {{ end }} Now these operators are implemented: =, ==, eq, !=, <>, ne, >=, ge, >, gt, <=, le, <, lt, in, not in It also fixes `TestWhere` more readable --- docs/content/templates/functions.md | 19 ++ tpl/template.go | 150 +++++++++++++-- tpl/template_test.go | 272 ++++++++++++++++++++++++++-- 3 files changed, 409 insertions(+), 32 deletions(-) diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index 2e9cbd3c1..da794a36d 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -79,6 +79,25 @@ e.g. {{ .Content}} {{ end }} +It can also be used with an operator like `!=`, `>=`, `in` etc. Without an operator (like above), `where` compares a given field with a matching value in a way like `=` is specified. + +e.g. + + {{ range where .Data.Pages "Section" "!=" "post" }} + {{ .Content}} + {{ end }} + +Following operators are now available + +- `=`, `==`, `eq`: True if a given field value equals a matching value +- `!=`, `<>`, `ne`: True if a given field value doesn't equal a matching value +- `>=`, `ge`: True if a given field value is greater than or equal to a matching value +- `>`, `gt`: True if a given field value is greater than a matching value +- `<=`, `le`: True if a given field value is lesser than or equal to a matching value +- `<`, `lt`: True if a given field value is lesser than a matching value +- `in`: True if a given field value is included in a matching value. A matching value must be an array or a slice +- `not in`: True if a given field value isn't included in a matching value. A matching value must be an array or a slice + *where and first can be stacked* e.g. diff --git a/tpl/template.go b/tpl/template.go index a116c2136..e8cdd4050 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -368,10 +368,141 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) 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) { +func checkCondition(v, mv reflect.Value, op string) (bool, error) { + if !v.IsValid() || !mv.IsValid() { + return false, nil + } + + var isNil bool + v, isNil = indirect(v) + if isNil { + return false, nil + } + mv, isNil = indirect(mv) + if isNil { + return false, nil + } + + var ivp, imvp *int64 + var svp, smvp *string + var ima []int64 + var sma []string + if mv.Type() == v.Type() { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + imv := mv.Int() + imvp = &imv + case reflect.String: + sv := v.String() + svp = &sv + smv := mv.String() + smvp = &smv + } + } else { + if mv.Kind() != reflect.Array && mv.Kind() != reflect.Slice { + return false, nil + } + if mv.Type().Elem() != v.Type() { + return false, nil + } + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv := v.Int() + ivp = &iv + for i := 0; i < mv.Len(); i++ { + ima = append(ima, mv.Index(i).Int()) + } + case reflect.String: + sv := v.String() + svp = &sv + for i := 0; i < mv.Len(); i++ { + sma = append(sma, mv.Index(i).String()) + } + } + } + + switch op { + case "", "=", "==", "eq": + if ivp != nil && imvp != nil { + return *ivp == *imvp, nil + } else if svp != nil && smvp != nil { + return *svp == *smvp, nil + } + case "!=", "<>", "ne": + if ivp != nil && imvp != nil { + return *ivp != *imvp, nil + } else if svp != nil && smvp != nil { + return *svp != *smvp, nil + } + case ">=", "ge": + if ivp != nil && imvp != nil { + return *ivp >= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp >= *smvp, nil + } + case ">", "gt": + if ivp != nil && imvp != nil { + return *ivp > *imvp, nil + } else if svp != nil && smvp != nil { + return *svp > *smvp, nil + } + case "<=", "le": + if ivp != nil && imvp != nil { + return *ivp <= *imvp, nil + } else if svp != nil && smvp != nil { + return *svp <= *smvp, nil + } + case "<", "lt": + if ivp != nil && imvp != nil { + return *ivp < *imvp, nil + } else if svp != nil && smvp != nil { + return *svp < *smvp, nil + } + case "in", "not in": + var r bool + if ivp != nil && len(ima) > 0 { + r = In(ima, *ivp) + } else if svp != nil { + if len(sma) > 0 { + r = In(sma, *svp) + } else if smvp != nil { + r = In(*smvp, *svp) + } + } else { + return false, nil + } + if op == "not in" { + return !r, nil + } else { + return r, nil + } + default: + return false, errors.New("no such an operator") + } + return false, nil +} + +func Where(seq, key interface{}, args ...interface{}) (r interface{}, err error) { seqv := reflect.ValueOf(seq) kv := reflect.ValueOf(key) - mv := reflect.ValueOf(match) + + var mv reflect.Value + var op string + switch len(args) { + case 1: + mv = reflect.ValueOf(args[0]) + case 2: + var ok bool + if op, ok = args[0].(string); !ok { + return nil, errors.New("operator argument must be string type") + } + op = strings.TrimSpace(strings.ToLower(op)) + mv = reflect.ValueOf(args[1]) + default: + return nil, errors.New("can't evaluate the array by no match argument or more than or equal to two arguments") + } seqv, isNil := indirect(seqv) if isNil { @@ -403,17 +534,10 @@ func Where(seq, key, match interface{}) (r interface{}, err error) { vvv = vv.MapIndex(kv) } } - 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() { - rv = reflect.Append(rv, rvv) - } - case reflect.String: - if mv.String() == vvv.String() { - rv = reflect.Append(rv, rvv) - } - } + if ok, err := checkCondition(vvv, mv, op); ok { + rv = reflect.Append(rv, rvv) + } else if err != nil { + return nil, err } } return rv.Interface(), nil diff --git a/tpl/template_test.go b/tpl/template_test.go index 123057afd..578d1d884 100644 --- a/tpl/template_test.go +++ b/tpl/template_test.go @@ -334,7 +334,7 @@ func (x TstX) String() string { } type TstX struct { - A, B string + A, B string unexported string } @@ -388,6 +388,62 @@ func TestEvaluateSubElem(t *testing.T) { } } +func TestCheckCondition(t *testing.T) { + type expect struct { + result bool + isError bool + } + + for i, this := range []struct { + value reflect.Value + match reflect.Value + op string + expect + }{ + {reflect.ValueOf(123), reflect.ValueOf(123), "", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("foo"), "", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "!=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), "!=", expect{true, false}}, + {reflect.ValueOf(456), reflect.ValueOf(123), ">=", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">=", expect{true, false}}, + {reflect.ValueOf(456), reflect.ValueOf(123), ">", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar"), ">", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "<=", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<=", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf(456), "<", expect{true, false}}, + {reflect.ValueOf("bar"), reflect.ValueOf("foo"), "<", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{123, 45, 678}), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"foo", "bar", "baz"}), "in", expect{true, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar-foo-baz"), "in", expect{true, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf("bar--baz"), "not in", expect{true, false}}, + {reflect.Value{}, reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.Value{}, "", expect{false, false}}, + {reflect.ValueOf((*TstX)(nil)), reflect.ValueOf("foo"), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf((*TstX)(nil)), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf(map[int]string{}), "", expect{false, false}}, + {reflect.ValueOf("foo"), reflect.ValueOf([]int{1, 2}), "", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf([]int{}), "in", expect{false, false}}, + {reflect.ValueOf(123), reflect.ValueOf(123), "op", expect{false, true}}, + } { + result, err := checkCondition(this.value, this.match, this.op) + if this.expect.isError { + if err == nil { + t.Errorf("[%d] checkCondition didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] failed: %s", i, err) + continue + } + if result != this.expect.result { + t.Errorf("[%d] check condition %v %s %v, got %v but expected %v", i, this.value, this.op, this.match, result, this.expect.result) + } + } + } +} + func TestWhere(t *testing.T) { // TODO(spf): Put these page tests back in //page1 := &Page{contentType: "v", Source: Source{File: *source.NewFile("/x/y/z/source.md")}} @@ -400,30 +456,192 @@ func TestWhere(t *testing.T) { for i, this := range []struct { sequence interface{} key interface{} + op string match interface{} expect interface{} }{ - {[]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: "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: "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}, + { + sequence: []map[int]string{ + {1: "a", 2: "m"}, {1: "c", 2: "d"}, {1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []map[int]string{ + {1: "a", 2: "m"}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "x": 4}, + }, + key: "b", match: 4, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []TstX{ + {A: "e", B: "f"}, + }, + }, + { + sequence: []*map[int]string{ + &map[int]string{1: "a", 2: "m"}, &map[int]string{1: "c", 2: "d"}, &map[int]string{1: "e", 3: "m"}, + }, + key: 2, match: "m", + expect: []*map[int]string{ + &map[int]string{1: "a", 2: "m"}, + }, + }, + { + sequence: []*TstX{ + &TstX{A: "a", B: "b"}, &TstX{A: "c", B: "d"}, &TstX{A: "e", B: "f"}, + }, + key: "B", match: "f", + expect: []*TstX{ + &TstX{A: "e", B: "f"}, + }, + }, + { + sequence: []*TstX{ + &TstX{A: "a", B: "b"}, &TstX{A: "c", B: "d"}, &TstX{A: "e", B: "c"}, + }, + key: "TstRp", match: "rc", + expect: []*TstX{ + &TstX{A: "c", B: "d"}, + }, + }, + { + sequence: []TstX{ + TstX{A: "a", B: "b"}, TstX{A: "c", B: "d"}, TstX{A: "e", B: "c"}, + }, + key: "TstRv", match: "rc", + expect: []TstX{ + TstX{A: "e", B: "c"}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: ".foo.B", match: "d", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]TstX{ + {"foo": TstX{A: "a", B: "b"}}, {"foo": TstX{A: "c", B: "d"}}, {"foo": TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRv", match: "rd", + expect: []map[string]TstX{ + {"foo": TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []map[string]*TstX{ + {"foo": &TstX{A: "a", B: "b"}}, {"foo": &TstX{A: "c", B: "d"}}, {"foo": &TstX{A: "e", B: "f"}}, + }, + key: "foo.TstRp", match: "rc", + expect: []map[string]*TstX{ + {"foo": &TstX{A: "c", B: "d"}}, + }, + }, + { + sequence: []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"}}}, + }, + key: "foo.Tst.B", match: "d", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []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"}}}, + }, + key: "foo.Tst.TstRv", match: "rd", + expect: []map[string]Mid{ + {"foo": Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []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"}}}, + }, + key: "foo.Tst.TstRp", match: "rc", + expect: []map[string]*Mid{ + {"foo": &Mid{Tst: TstX{A: "c", B: "d"}}}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: ">", match: 3, + expect: []map[string]int{ + {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "!=", match: "f", + expect: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, + }, + }, + { + sequence: []map[string]int{ + {"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}, + }, + key: "b", op: "in", match: []int{3, 4, 5}, + expect: []map[string]int{ + {"a": 3, "b": 4}, + }, + }, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "not in", match: []string{"c", "d", "e"}, + expect: []TstX{ + {A: "a", B: "b"}, {A: "e", B: "f"}, + }, + }, + {sequence: (*[]TstX)(nil), key: "A", match: "a", expect: false}, + {sequence: TstX{A: "a", B: "b"}, key: "A", match: "a", expect: false}, + {sequence: []map[string]*TstX{{"foo": nil}}, key: "foo.B", match: "d", expect: false}, + { + sequence: []TstX{ + {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, + }, + key: "B", op: "op", match: "f", + expect: false, + }, //{[]*Page{page1, page2}, "Type", "v", []*Page{page1}}, //{[]*Page{page1, page2}, "Section", "y", []*Page{page2}}, } { - results, err := Where(this.sequence, this.key, this.match) + var results interface{} + var err error + if len(this.op) > 0 { + results, err = Where(this.sequence, this.key, this.op, this.match) + } else { + results, err = Where(this.sequence, this.key, this.match) + } if b, ok := this.expect.(bool); ok && !b { if err == nil { t.Errorf("[%d] Where didn't return an expected error", i) @@ -438,6 +656,22 @@ func TestWhere(t *testing.T) { } } } + + var err error + _, err = Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1) + if err == nil { + t.Errorf("Where called with none string op value didn't return an expected error") + } + + _, err = Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2) + if err == nil { + t.Errorf("Where called with more than two variable arguments didn't return an expected error") + } + + _, err = Where(map[string]int{"a": 1, "b": 2}, "a") + if err == nil { + t.Errorf("Where called with no variable arguments didn't return an expected error") + } } func TestDelimit(t *testing.T) {