diff --git a/pubsub/query/query.go b/pubsub/query/query.go index fdfb87d7..56f2829d 100644 --- a/pubsub/query/query.go +++ b/pubsub/query/query.go @@ -22,6 +22,14 @@ type Query struct { parser *QueryParser } +// Condition represents a single condition within a query and consists of tag +// (e.g. "tx.gas"), operator (e.g. "=") and operand (e.g. "7"). +type Condition struct { + Tag string + Op Operator + Operand interface{} +} + // New parses the given string and returns a query or error if the string is // invalid. func New(s string) (*Query, error) { @@ -48,17 +56,91 @@ func (q *Query) String() string { return q.str } -type operator uint8 +// Operator is an operator that defines some kind of relation between tag and +// operand (equality, etc.). +type Operator uint8 const ( - opLessEqual operator = iota - opGreaterEqual - opLess - opGreater - opEqual - opContains + // "<=" + OpLessEqual Operator = iota + // ">=" + OpGreaterEqual + // "<" + OpLess + // ">" + OpGreater + // "=" + OpEqual + // "CONTAINS"; used to check if a string contains a certain sub string. + OpContains ) +// Conditions returns a list of conditions. +func (q *Query) Conditions() []Condition { + conditions := make([]Condition, 0) + + buffer, begin, end := q.parser.Buffer, 0, 0 + + var tag string + var op Operator + + // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") + for _, token := range q.parser.Tokens() { + switch token.pegRule { + + case rulePegText: + begin, end = int(token.begin), int(token.end) + case ruletag: + tag = buffer[begin:end] + case rulele: + op = OpLessEqual + case rulege: + op = OpGreaterEqual + case rulel: + op = OpLess + case ruleg: + op = OpGreater + case ruleequal: + op = OpEqual + case rulecontains: + op = OpContains + case rulevalue: + // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") + valueWithoutSingleQuotes := buffer[begin+1 : end-1] + conditions = append(conditions, Condition{tag, op, valueWithoutSingleQuotes}) + case rulenumber: + number := buffer[begin:end] + if strings.Contains(number, ".") { // if it looks like a floating-point number + value, err := strconv.ParseFloat(number, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) + } + conditions = append(conditions, Condition{tag, op, value}) + } else { + value, err := strconv.ParseInt(number, 10, 64) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number)) + } + conditions = append(conditions, Condition{tag, op, value}) + } + case ruletime: + value, err := time.Parse(time.RFC3339, buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + conditions = append(conditions, Condition{tag, op, value}) + case ruledate: + value, err := time.Parse("2006-01-02", buffer[begin:end]) + if err != nil { + panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", err, buffer[begin:end])) + } + conditions = append(conditions, Condition{tag, op, value}) + } + } + + return conditions +} + // Matches returns true if the query matches the given set of tags, false otherwise. // // For example, query "name=John" matches tags = {"name": "John"}. More @@ -71,7 +153,7 @@ func (q *Query) Matches(tags map[string]interface{}) bool { buffer, begin, end := q.parser.Buffer, 0, 0 var tag string - var op operator + var op Operator // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") for _, token := range q.parser.Tokens() { @@ -82,17 +164,17 @@ func (q *Query) Matches(tags map[string]interface{}) bool { case ruletag: tag = buffer[begin:end] case rulele: - op = opLessEqual + op = OpLessEqual case rulege: - op = opGreaterEqual + op = OpGreaterEqual case rulel: - op = opLess + op = OpLess case ruleg: - op = opGreater + op = OpGreater case ruleequal: - op = opEqual + op = OpEqual case rulecontains: - op = opContains + op = OpContains case rulevalue: // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") valueWithoutSingleQuotes := buffer[begin+1 : end-1] @@ -149,7 +231,7 @@ func (q *Query) Matches(tags map[string]interface{}) bool { // value from it to the operand using the operator. // // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } -func match(tag string, op operator, operand reflect.Value, tags map[string]interface{}) bool { +func match(tag string, op Operator, operand reflect.Value, tags map[string]interface{}) bool { // look up the tag from the query in tags value, ok := tags[tag] if !ok { @@ -163,15 +245,15 @@ func match(tag string, op operator, operand reflect.Value, tags map[string]inter return false } switch op { - case opLessEqual: + case OpLessEqual: return v.Before(operandAsTime) || v.Equal(operandAsTime) - case opGreaterEqual: + case OpGreaterEqual: return v.Equal(operandAsTime) || v.After(operandAsTime) - case opLess: + case OpLess: return v.Before(operandAsTime) - case opGreater: + case OpGreater: return v.After(operandAsTime) - case opEqual: + case OpEqual: return v.Equal(operandAsTime) } case reflect.Float64: @@ -197,15 +279,15 @@ func match(tag string, op operator, operand reflect.Value, tags map[string]inter panic(fmt.Sprintf("Incomparable types: %T (%v) vs float64 (%v)", value, value, operandFloat64)) } switch op { - case opLessEqual: + case OpLessEqual: return v <= operandFloat64 - case opGreaterEqual: + case OpGreaterEqual: return v >= operandFloat64 - case opLess: + case OpLess: return v < operandFloat64 - case opGreater: + case OpGreater: return v > operandFloat64 - case opEqual: + case OpEqual: return v == operandFloat64 } case reflect.Int64: @@ -231,15 +313,15 @@ func match(tag string, op operator, operand reflect.Value, tags map[string]inter panic(fmt.Sprintf("Incomparable types: %T (%v) vs int64 (%v)", value, value, operandInt)) } switch op { - case opLessEqual: + case OpLessEqual: return v <= operandInt - case opGreaterEqual: + case OpGreaterEqual: return v >= operandInt - case opLess: + case OpLess: return v < operandInt - case opGreater: + case OpGreater: return v > operandInt - case opEqual: + case OpEqual: return v == operandInt } case reflect.String: @@ -248,9 +330,9 @@ func match(tag string, op operator, operand reflect.Value, tags map[string]inter return false } switch op { - case opEqual: + case OpEqual: return v == operand.String() - case opContains: + case OpContains: return strings.Contains(v, operand.String()) } default: diff --git a/pubsub/query/query_test.go b/pubsub/query/query_test.go index 431ae1fe..93b63a15 100644 --- a/pubsub/query/query_test.go +++ b/pubsub/query/query_test.go @@ -62,3 +62,24 @@ func TestMustParse(t *testing.T) { assert.Panics(t, func() { query.MustParse("=") }) assert.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") }) } + +func TestConditions(t *testing.T) { + txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z") + require.NoError(t, err) + + testCases := []struct { + s string + conditions []query.Condition + }{ + {"tm.events.type='NewBlock'", []query.Condition{query.Condition{"tm.events.type", query.OpEqual, "NewBlock"}}}, + {"tx.gas > 7 AND tx.gas < 9", []query.Condition{query.Condition{"tx.gas", query.OpGreater, int64(7)}, query.Condition{"tx.gas", query.OpLess, int64(9)}}}, + {"tx.time >= TIME 2013-05-03T14:45:00Z", []query.Condition{query.Condition{"tx.time", query.OpGreaterEqual, txTime}}}, + } + + for _, tc := range testCases { + query, err := query.New(tc.s) + require.Nil(t, err) + + assert.Equal(t, tc.conditions, query.Conditions()) + } +}