// Package query provides a parser for a custom query format: // // abci.invoice.number=22 AND abci.invoice.owner=Ivan // // See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar. // More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics // // It has a support for numbers (integer and floating point), dates and times. package query import ( "fmt" "reflect" "regexp" "strconv" "strings" "time" "github.com/pkg/errors" ) var ( numRegex = regexp.MustCompile(`([0-9\.]+)`) ) // Query holds the query string and the query parser. type Query struct { str string 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) { p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)} p.Init() if err := p.Parse(); err != nil { return nil, err } return &Query{str: s, parser: p}, nil } // MustParse turns the given string into a query or panics; for tests or others // cases where you know the string is valid. func MustParse(s string) *Query { q, err := New(s) if err != nil { panic(fmt.Sprintf("failed to parse %s: %v", s, err)) } return q } // String returns the original string. func (q *Query) String() string { return q.str } // 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 // "CONTAINS"; used to check if a string contains a certain sub string. OpContains // "EXISTS"; used to check if a certain event attribute is present. OpExists ) const ( // DateLayout defines a layout for all dates (`DATE date`) DateLayout = "2006-01-02" // TimeLayout defines a layout for all times (`TIME time`) TimeLayout = time.RFC3339 ) // Conditions returns a list of conditions. It returns an error if there is any // error with the provided grammar in the Query. func (q *Query) Conditions() ([]Condition, error) { var ( eventAttr string op Operator ) conditions := make([]Condition, 0) buffer, begin, end := q.parser.Buffer, 0, 0 // 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: eventAttr = 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 ruleexists: op = OpExists conditions = append(conditions, Condition{eventAttr, op, nil}) case rulevalue: // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") valueWithoutSingleQuotes := buffer[begin+1 : end-1] conditions = append(conditions, Condition{eventAttr, op, valueWithoutSingleQuotes}) case rulenumber: number := buffer[begin:end] if strings.ContainsAny(number, ".") { // if it looks like a floating-point number value, err := strconv.ParseFloat(number, 64) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number, ) return nil, err } conditions = append(conditions, Condition{eventAttr, op, value}) } else { value, err := strconv.ParseInt(number, 10, 64) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number, ) return nil, err } conditions = append(conditions, Condition{eventAttr, op, value}) } case ruletime: value, err := time.Parse(TimeLayout, buffer[begin:end]) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end], ) return nil, err } conditions = append(conditions, Condition{eventAttr, op, value}) case ruledate: value, err := time.Parse("2006-01-02", buffer[begin:end]) if err != nil { err = fmt.Errorf( "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], ) return nil, err } conditions = append(conditions, Condition{eventAttr, op, value}) } } return conditions, nil } // Matches returns true if the query matches against any event in the given set // of events, false otherwise. For each event, a match exists if the query is // matched against *any* value in a slice of values. An error is returned if // any attempted event match returns an error. // // For example, query "name=John" matches events = {"name": ["John", "Eric"]}. // More examples could be found in parser_test.go and query_test.go. func (q *Query) Matches(events map[string][]string) (bool, error) { if len(events) == 0 { return false, nil } var ( eventAttr string op Operator ) buffer, begin, end := q.parser.Buffer, 0, 0 // 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: eventAttr = 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 ruleexists: op = OpExists if strings.Contains(eventAttr, ".") { // Searching for a full "type.attribute" event. _, ok := events[eventAttr] if !ok { return false, nil } } else { foundEvent := false loop: for compositeKey := range events { if strings.Index(compositeKey, eventAttr) == 0 { foundEvent = true break loop } } if !foundEvent { return false, nil } } case rulevalue: // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") valueWithoutSingleQuotes := buffer[begin+1 : end-1] // see if the triplet (event attribute, operator, operand) matches any event // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } match, err := match(eventAttr, op, reflect.ValueOf(valueWithoutSingleQuotes), events) if err != nil { return false, err } if !match { return false, nil } case rulenumber: number := buffer[begin:end] if strings.ContainsAny(number, ".") { // if it looks like a floating-point number value, err := strconv.ParseFloat(number, 64) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number, ) return false, err } match, err := match(eventAttr, op, reflect.ValueOf(value), events) if err != nil { return false, err } if !match { return false, nil } } else { value, err := strconv.ParseInt(number, 10, 64) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number, ) return false, err } match, err := match(eventAttr, op, reflect.ValueOf(value), events) if err != nil { return false, err } if !match { return false, nil } } case ruletime: value, err := time.Parse(TimeLayout, buffer[begin:end]) if err != nil { err = fmt.Errorf( "got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end], ) return false, err } match, err := match(eventAttr, op, reflect.ValueOf(value), events) if err != nil { return false, err } if !match { return false, nil } case ruledate: value, err := time.Parse("2006-01-02", buffer[begin:end]) if err != nil { err = fmt.Errorf( "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], ) return false, err } match, err := match(eventAttr, op, reflect.ValueOf(value), events) if err != nil { return false, err } if !match { return false, nil } } } return true, nil } // match returns true if the given triplet (attribute, operator, operand) matches // any value in an event for that attribute. If any match fails with an error, // that error is returned. // // First, it looks up the key in the events and if it finds one, tries to compare // all the values from it to the operand using the operator. // // "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]} func match(attr string, op Operator, operand reflect.Value, events map[string][]string) (bool, error) { // look up the tag from the query in tags values, ok := events[attr] if !ok { return false, nil } for _, value := range values { // return true if any value in the set of the event's values matches match, err := matchValue(value, op, operand) if err != nil { return false, err } if match { return true, nil } } return false, nil } // matchValue will attempt to match a string value against an operator an // operand. A boolean is returned representing the match result. It will return // an error if the value cannot be parsed and matched against the operand type. func matchValue(value string, op Operator, operand reflect.Value) (bool, error) { switch operand.Kind() { case reflect.Struct: // time operandAsTime := operand.Interface().(time.Time) // try our best to convert value from tags to time.Time var ( v time.Time err error ) if strings.ContainsAny(value, "T") { v, err = time.Parse(TimeLayout, value) } else { v, err = time.Parse(DateLayout, value) } if err != nil { return false, errors.Wrapf(err, "failed to convert value %v from event attribute to time.Time", value) } switch op { case OpLessEqual: return (v.Before(operandAsTime) || v.Equal(operandAsTime)), nil case OpGreaterEqual: return (v.Equal(operandAsTime) || v.After(operandAsTime)), nil case OpLess: return v.Before(operandAsTime), nil case OpGreater: return v.After(operandAsTime), nil case OpEqual: return v.Equal(operandAsTime), nil } case reflect.Float64: var v float64 operandFloat64 := operand.Interface().(float64) filteredValue := numRegex.FindString(value) // try our best to convert value from tags to float64 v, err := strconv.ParseFloat(filteredValue, 64) if err != nil { return false, errors.Wrapf(err, "failed to convert value %v from event attribute to float64", filteredValue) } switch op { case OpLessEqual: return v <= operandFloat64, nil case OpGreaterEqual: return v >= operandFloat64, nil case OpLess: return v < operandFloat64, nil case OpGreater: return v > operandFloat64, nil case OpEqual: return v == operandFloat64, nil } case reflect.Int64: var v int64 operandInt := operand.Interface().(int64) filteredValue := numRegex.FindString(value) // if value looks like float, we try to parse it as float if strings.ContainsAny(filteredValue, ".") { v1, err := strconv.ParseFloat(filteredValue, 64) if err != nil { return false, errors.Wrapf(err, "failed to convert value %v from event attribute to float64", filteredValue) } v = int64(v1) } else { var err error // try our best to convert value from tags to int64 v, err = strconv.ParseInt(filteredValue, 10, 64) if err != nil { return false, errors.Wrapf(err, "failed to convert value %v from event attribute to int64", filteredValue) } } switch op { case OpLessEqual: return v <= operandInt, nil case OpGreaterEqual: return v >= operandInt, nil case OpLess: return v < operandInt, nil case OpGreater: return v > operandInt, nil case OpEqual: return v == operandInt, nil } case reflect.String: switch op { case OpEqual: return value == operand.String(), nil case OpContains: return strings.Contains(value, operand.String()), nil } default: return false, fmt.Errorf("unknown kind of operand %v", operand.Kind()) } return false, nil }