diff --git a/pql/ast.go b/pql/ast.go index 13a989b03..9555f9d90 100644 --- a/pql/ast.go +++ b/pql/ast.go @@ -161,19 +161,7 @@ func (c *Call) String() string { if i > 0 { buf.WriteString(", ") } - - switch v := c.Args[key].(type) { - case string: - fmt.Fprintf(&buf, "%v=%q", key, v) - case []interface{}: - fmt.Fprintf(&buf, "%v=%s", key, joinInterfaceSlice(v)) - case []uint64: - fmt.Fprintf(&buf, "%v=%s", key, joinUint64Slice(v)) - case time.Time: - fmt.Fprintf(&buf, "%v=\"%s\"", key, v.Format(TimeFormat)) - default: - fmt.Fprintf(&buf, "%v=%v", key, v) - } + fmt.Fprintf(&buf, "%v=%s", key, FormatValue(c.Args[key])) } // Write closing. @@ -210,6 +198,35 @@ func (c *Call) IsInverse(rowLabel, columnLabel string) bool { return false } +// Condition represents an operation & value. +// When used in an argument map it represents a binary expression. +type Condition struct { + Op Token + Value interface{} +} + +// String returns the string representation of the condition. +func (cond *Condition) String() string { + return fmt.Sprintf("%s %s", cond.Op.String(), FormatValue(cond.Value)) +} + +func FormatValue(v interface{}) string { + switch v := v.(type) { + case string: + return fmt.Sprintf("%q", v) + case []interface{}: + return fmt.Sprintf("%s", joinInterfaceSlice(v)) + case []uint64: + return fmt.Sprintf("%s", joinUint64Slice(v)) + case time.Time: + return fmt.Sprintf("\"%s\"", v.Format(TimeFormat)) + case *Condition: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} + // CopyArgs returns a copy of m. func CopyArgs(m map[string]interface{}) map[string]interface{} { other := make(map[string]interface{}, len(m)) diff --git a/pql/parser.go b/pql/parser.go index 048f6a664..f0d5feeae 100644 --- a/pql/parser.go +++ b/pql/parser.go @@ -161,9 +161,14 @@ func (p *Parser) parseArgs() (map[string]interface{}, error) { } key := lit - // Expect '=' next. - if tok, pos, lit := p.scanIgnoreWhitespace(); tok != EQ { - return nil, parseErrorf(pos, "expected equals sign, found %q", lit) + // Expect '=' or a comparison next. + var op Token + switch tok, pos, lit := p.scanIgnoreWhitespace(); tok { + case ASSIGN: + case EQ, LT, LTE, GT, GTE: + op = tok + default: + return nil, parseErrorf(pos, "expected equals sign or comparison operator, found %q", lit) } // Parse value. @@ -209,6 +214,11 @@ func (p *Parser) parseArgs() (map[string]interface{}, error) { return nil, parseErrorf(pos, "argument key already used: %s", key) } + // If op is specified then create a condition. + if op != 0 { + value = &Condition{Op: op, Value: value} + } + // Add key/value pair to arguments. args[key] = value diff --git a/pql/parser_test.go b/pql/parser_test.go index 8cb8a2858..736a9c7f8 100644 --- a/pql/parser_test.go +++ b/pql/parser_test.go @@ -169,4 +169,24 @@ func TestParser_Parse(t *testing.T) { t.Fatalf("unexpected call: %#v", q.Calls[0]) } }) + + // Parse with condition arguments. + t.Run("WithCondition", func(t *testing.T) { + q, err := pql.ParseString(`MyCall(key=foo, x == 12.25, y >= 100)`) + if err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(q.Calls[0], + &pql.Call{ + Name: "MyCall", + Args: map[string]interface{}{ + "key": "foo", + "x": &pql.Condition{Op: pql.EQ, Value: 12.25}, + "y": &pql.Condition{Op: pql.GTE, Value: int64(100)}, + }, + }, + ) { + t.Fatalf("unexpected call: %#v", q.Calls[0]) + } + }) + } diff --git a/pql/scanner.go b/pql/scanner.go index a559d8ccb..38fd7d6fc 100644 --- a/pql/scanner.go +++ b/pql/scanner.go @@ -59,26 +59,38 @@ func (s *Scanner) Scan() (tok Token, pos Pos, lit string) { // Otherwise parse individual characters. switch ch { case eof: - tok = EOF - return + return EOF, pos, "" case '=': - tok = EQ + if next := s.read(); next == '=' { + return EQ, pos, "==" + } + s.unread() + return ASSIGN, pos, string(ch) + case '<': + if next := s.read(); next == '=' { + return LTE, pos, "<=" + } + s.unread() + return LT, pos, string(ch) + case '>': + if next := s.read(); next == '=' { + return GTE, pos, ">=" + } + s.unread() + return GT, pos, string(ch) case ',': - tok = COMMA + return COMMA, pos, string(ch) case '(': - tok = LPAREN + return LPAREN, pos, string(ch) case ')': - tok = RPAREN + return RPAREN, pos, string(ch) case '[': - tok = LBRACK + return LBRACK, pos, string(ch) case ']': - tok = RBRACK + return RBRACK, pos, string(ch) default: - tok = ILLEGAL + return ILLEGAL, pos, string(ch) } - - lit = string(ch) - return } // read returns the next code point from the underlying reader and updates the pos. diff --git a/pql/scanner_test.go b/pql/scanner_test.go index 5b7a06b2f..49d272ce5 100644 --- a/pql/scanner_test.go +++ b/pql/scanner_test.go @@ -23,42 +23,50 @@ import ( func TestScanner_Scan(t *testing.T) { var tests = []struct { - s string - tok pql.Token - lit string - pos pql.Pos + name string + s string + tok pql.Token + lit string + pos pql.Pos }{ // Special tokens (EOF, ILLEGAL, WS) - {s: ``, tok: pql.EOF}, - {s: `#`, tok: pql.ILLEGAL, lit: `#`}, - {s: ` `, tok: pql.WS, lit: " "}, - {s: "\t", tok: pql.WS, lit: "\t"}, - {s: "\n", tok: pql.WS, lit: "\n"}, + {name: "EOF", s: ``, tok: pql.EOF}, + {name: "ILLEGAL", s: `#`, tok: pql.ILLEGAL, lit: `#`}, + {name: "WS/SPACE", s: ` `, tok: pql.WS, lit: " "}, + {name: "WS/TAB", s: "\t", tok: pql.WS, lit: "\t"}, + {name: "WS/NEWLINE", s: "\n", tok: pql.WS, lit: "\n"}, - {s: `=`, tok: pql.EQ, lit: `=`}, - {s: `,`, tok: pql.COMMA, lit: `,`}, - {s: `(`, tok: pql.LPAREN, lit: `(`}, - {s: `)`, tok: pql.RPAREN, lit: `)`}, - {s: `[`, tok: pql.LBRACK, lit: `[`}, - {s: `]`, tok: pql.RBRACK, lit: `]`}, + {name: "ASSIGN", s: `=`, tok: pql.ASSIGN, lit: `=`}, + {name: "EQ", s: `==`, tok: pql.EQ, lit: `==`}, + {name: "LT", s: `<`, tok: pql.LT, lit: `<`}, + {name: "LTE", s: `<=`, tok: pql.LTE, lit: `<=`}, + {name: "GT", s: `>`, tok: pql.GT, lit: `>`}, + {name: "GTE", s: `>=`, tok: pql.GTE, lit: `>=`}, + {name: "COMMA", s: `,`, tok: pql.COMMA, lit: `,`}, + {name: "LPAREN", s: `(`, tok: pql.LPAREN, lit: `(`}, + {name: "RPAREN", s: `)`, tok: pql.RPAREN, lit: `)`}, + {name: "LBRACK", s: `[`, tok: pql.LBRACK, lit: `[`}, + {name: "RBRACK", s: `]`, tok: pql.RBRACK, lit: `]`}, - {s: `foo`, tok: pql.IDENT, lit: `foo`}, - {s: `100`, tok: pql.INTEGER, lit: `100`}, - {s: `100.3`, tok: pql.FLOAT, lit: `100.3`}, + {name: "IDENT", s: `foo`, tok: pql.IDENT, lit: `foo`}, + {name: "INTEGER", s: `100`, tok: pql.INTEGER, lit: `100`}, + {name: "FLOAT", s: `100.3`, tok: pql.FLOAT, lit: `100.3`}, - {s: `all`, tok: pql.ALL, lit: `all`}, - {s: `ALL`, tok: pql.ALL, lit: `ALL`}, // case insensitive + {name: "ALL", s: `all`, tok: pql.ALL, lit: `all`}, + {name: "ALL/CASE", s: `ALL`, tok: pql.ALL, lit: `ALL`}, // case insensitive } for i, tt := range tests { - s := pql.NewScanner(strings.NewReader(tt.s)) - tok, pos, lit := s.Scan() - if tt.tok != tok { - t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.tok, tok, lit) - } else if tt.pos.Line != pos.Line || tt.pos.Char != pos.Char { - t.Errorf("%d. %q pos mismatch: exp=%#v got=%#v", i, tt.s, tt.pos, pos) - } else if tt.lit != lit { - t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.lit, lit) - } + t.Run(tt.name, func(t *testing.T) { + s := pql.NewScanner(strings.NewReader(tt.s)) + tok, pos, lit := s.Scan() + if tt.tok != tok { + t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.tok, tok, lit) + } else if tt.pos.Line != pos.Line || tt.pos.Char != pos.Char { + t.Errorf("%d. %q pos mismatch: exp=%#v got=%#v", i, tt.s, tt.pos, pos) + } else if tt.lit != lit { + t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.lit, lit) + } + }) } } diff --git a/pql/token.go b/pql/token.go index 4d71117af..1327df9ce 100644 --- a/pql/token.go +++ b/pql/token.go @@ -37,7 +37,12 @@ const ( ALL keyword_end - EQ // = + ASSIGN // = + EQ // == + LT // < + LTE // <= + GT // > + GTE // >= COMMA // , LPAREN // ( RPAREN // ) @@ -56,7 +61,12 @@ var tokens = [...]string{ ALL: "ALL", - EQ: "=", + ASSIGN: "=", + EQ: "==", + LT: "<", + LTE: "<=", + GT: ">", + GTE: ">=", COMMA: ",", LPAREN: "(", RPAREN: ")",