Rename search package to filter package

This commit is contained in:
David Stotijn
2022-03-31 15:12:54 +02:00
parent 2ce4218a30
commit aa9822854d
21 changed files with 364 additions and 362 deletions

87
pkg/filter/ast.go Normal file
View File

@ -0,0 +1,87 @@
package filter
import (
"encoding/gob"
"regexp"
"strconv"
"strings"
)
type Expression interface {
String() string
}
type PrefixExpression struct {
Operator TokenType
Right Expression
}
func (pe PrefixExpression) String() string {
b := strings.Builder{}
b.WriteString("(")
b.WriteString(pe.Operator.String())
b.WriteString(" ")
b.WriteString(pe.Right.String())
b.WriteString(")")
return b.String()
}
type InfixExpression struct {
Operator TokenType
Left Expression
Right Expression
}
func (ie InfixExpression) String() string {
b := strings.Builder{}
b.WriteString("(")
b.WriteString(ie.Left.String())
b.WriteString(" ")
b.WriteString(ie.Operator.String())
b.WriteString(" ")
b.WriteString(ie.Right.String())
b.WriteString(")")
return b.String()
}
type StringLiteral struct {
Value string
}
func (sl StringLiteral) String() string {
return strconv.Quote(sl.Value)
}
type RegexpLiteral struct {
*regexp.Regexp
}
func (rl RegexpLiteral) String() string {
return strconv.Quote(rl.Regexp.String())
}
func (rl RegexpLiteral) MarshalBinary() ([]byte, error) {
return []byte(rl.Regexp.String()), nil
}
func (rl *RegexpLiteral) UnmarshalBinary(data []byte) error {
re, err := regexp.Compile(string(data))
if err != nil {
return err
}
*rl = RegexpLiteral{re}
return nil
}
func init() {
// The `filter` package was previously named `search`.
// We use the legacy names for backwards compatibility with existing database data.
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.PrefixExpression", PrefixExpression{})
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.InfixExpression", InfixExpression{})
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.StringLiteral", StringLiteral{})
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.RegexpLiteral", RegexpLiteral{})
}

212
pkg/filter/ast_test.go Normal file
View File

@ -0,0 +1,212 @@
package filter_test
import (
"regexp"
"testing"
"github.com/dstotijn/hetty/pkg/filter"
)
func TestExpressionString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expression filter.Expression
expected string
}{
{
name: "string literal expression",
expression: filter.StringLiteral{Value: "foobar"},
expected: `"foobar"`,
},
{
name: "boolean expression with equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" = "bar")`,
},
{
name: "boolean expression with not equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpNotEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" != "bar")`,
},
{
name: "boolean expression with greater than operator",
expression: filter.InfixExpression{
Operator: filter.TokOpGt,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" > "bar")`,
},
{
name: "boolean expression with less than operator",
expression: filter.InfixExpression{
Operator: filter.TokOpLt,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" < "bar")`,
},
{
name: "boolean expression with greater than or equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpGtEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" >= "bar")`,
},
{
name: "boolean expression with less than or equal operator",
expression: filter.InfixExpression{
Operator: filter.TokOpLtEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" <= "bar")`,
},
{
name: "boolean expression with regular expression operator",
expression: filter.InfixExpression{
Operator: filter.TokOpRe,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" =~ "bar")`,
},
{
name: "boolean expression with not regular expression operator",
expression: filter.InfixExpression{
Operator: filter.TokOpNotRe,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
},
expected: `("foo" !~ "bar")`,
},
{
name: "boolean expression with AND, OR and NOT operators",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.StringLiteral{Value: "bar"},
Right: filter.PrefixExpression{
Operator: filter.TokOpNot,
Right: filter.StringLiteral{Value: "baz"},
},
},
},
expected: `("foo" AND ("bar" OR (NOT "baz")))`,
},
{
name: "boolean expression with nested group",
expression: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.PrefixExpression{
Operator: filter.TokOpNot,
Right: filter.StringLiteral{Value: "baz"},
},
},
expected: `(("foo" AND "bar") OR (NOT "baz"))`,
},
{
name: "implicit boolean expression with string literal operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.StringLiteral{Value: "baz"},
},
expected: `(("foo" AND "bar") AND "baz")`,
},
{
name: "implicit boolean expression nested in group",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
expected: `("foo" AND "bar")`,
},
{
name: "implicit and explicit boolean expression with string literal operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.StringLiteral{Value: "bar"},
Right: filter.StringLiteral{Value: "baz"},
},
},
Right: filter.StringLiteral{Value: "yolo"},
},
expected: `(("foo" AND ("bar" OR "baz")) AND "yolo")`,
},
{
name: "implicit boolean expression with comparison operands",
expression: filter.InfixExpression{
Operator: filter.TokOpAnd,
Left: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.InfixExpression{
Operator: filter.TokOpRe,
Left: filter.StringLiteral{Value: "baz"},
Right: filter.RegexpLiteral{regexp.MustCompile("yolo")},
},
},
expected: `(("foo" = "bar") AND ("baz" =~ "yolo"))`,
},
{
name: "eq operator takes precedence over boolean ops",
expression: filter.InfixExpression{
Operator: filter.TokOpOr,
Left: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "foo"},
Right: filter.StringLiteral{Value: "bar"},
},
Right: filter.InfixExpression{
Operator: filter.TokOpEq,
Left: filter.StringLiteral{Value: "baz"},
Right: filter.StringLiteral{Value: "yolo"},
},
},
expected: `(("foo" = "bar") OR ("baz" = "yolo"))`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := tt.expression.String()
if tt.expected != got {
t.Errorf("expected: %v, got: %v", tt.expected, got)
}
})
}
}

82
pkg/filter/http.go Normal file
View File

@ -0,0 +1,82 @@
package filter
import (
"errors"
"fmt"
"net/http"
)
func MatchHTTPHeaders(op TokenType, expr Expression, headers http.Header) (bool, error) {
if headers == nil {
return false, nil
}
switch op {
case TokOpEq:
strLiteral, ok := expr.(StringLiteral)
if !ok {
return false, errors.New("filter: expression must be a string literal")
}
// Return `true` if at least one header (<key>: <value>) is equal to the string literal.
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return true, nil
}
}
}
return false, nil
case TokOpNotEq:
strLiteral, ok := expr.(StringLiteral)
if !ok {
return false, errors.New("filter: expression must be a string literal")
}
// Return `true` if none of the headers (<key>: <value>) are equal to the string literal.
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return false, nil
}
}
}
return true, nil
case TokOpRe:
re, ok := expr.(RegexpLiteral)
if !ok {
return false, errors.New("filter: expression must be a regular expression")
}
// Return `true` if at least one header (<key>: <value>) matches the regular expression.
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return true, nil
}
}
}
return false, nil
case TokOpNotRe:
re, ok := expr.(RegexpLiteral)
if !ok {
return false, errors.New("filter: expression must be a regular expression")
}
// Return `true` if none of the headers (<key>: <value>) match the regular expression.
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return false, nil
}
}
}
return true, nil
default:
return false, fmt.Errorf("filter: unsupported operator %q", op.String())
}
}

276
pkg/filter/lexer.go Normal file
View File

@ -0,0 +1,276 @@
package filter
import (
"fmt"
"unicode"
"unicode/utf8"
)
type TokenType int
type Token struct {
Type TokenType
Literal string
}
const eof = 0
// Token types.
const (
// Flow.
TokInvalid TokenType = iota
TokEOF
TokParenOpen
TokParenClose
// Literals.
TokString
// Boolean operators.
TokOpNot
TokOpAnd
TokOpOr
// Comparison operators.
TokOpEq
TokOpNotEq
TokOpGt
TokOpLt
TokOpGtEq
TokOpLtEq
TokOpRe
TokOpNotRe
)
var (
keywords = map[string]TokenType{
"NOT": TokOpNot,
"AND": TokOpAnd,
"OR": TokOpOr,
}
reservedRunes = []rune{'=', '!', '<', '>', '(', ')'}
tokenTypeStrings = map[TokenType]string{
TokInvalid: "INVALID",
TokEOF: "EOF",
TokParenOpen: "(",
TokParenClose: ")",
TokString: "STRING",
TokOpNot: "NOT",
TokOpAnd: "AND",
TokOpOr: "OR",
TokOpEq: "=",
TokOpNotEq: "!=",
TokOpGt: ">",
TokOpLt: "<",
TokOpGtEq: ">=",
TokOpLtEq: "<=",
TokOpRe: "=~",
TokOpNotRe: "!~",
}
)
type stateFn func(*Lexer) stateFn
type Lexer struct {
input string
pos int
start int
width int
tokens chan Token
}
func NewLexer(input string) *Lexer {
l := &Lexer{
input: input,
tokens: make(chan Token),
}
go l.run(begin)
return l
}
func (l *Lexer) Next() Token {
return <-l.tokens
}
func (tt TokenType) String() string {
if typeString, ok := tokenTypeStrings[tt]; ok {
return typeString
}
return "<unknown>"
}
func (l *Lexer) run(init stateFn) {
for nextState := init; nextState != nil; {
nextState = nextState(l)
}
close(l.tokens)
}
func (l *Lexer) read() (r rune) {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += l.width
return
}
func (l *Lexer) emit(tokenType TokenType) {
l.tokens <- Token{
Type: tokenType,
Literal: l.input[l.start:l.pos],
}
l.start = l.pos
}
func (l *Lexer) ignore() {
l.start = l.pos
}
func (l *Lexer) skip() {
l.pos += l.width
l.start = l.pos
}
func (l *Lexer) backup() {
l.pos -= l.width
}
func (l *Lexer) errorf(format string, args ...interface{}) stateFn {
l.tokens <- Token{
Type: TokInvalid,
Literal: fmt.Sprintf(format, args...),
}
return nil
}
func begin(l *Lexer) stateFn {
r := l.read()
switch r {
case '=':
if next := l.read(); next == '~' {
l.emit(TokOpRe)
} else {
l.backup()
l.emit(TokOpEq)
}
return begin
case '!':
switch next := l.read(); next {
case '=':
l.emit(TokOpNotEq)
case '~':
l.emit(TokOpNotRe)
default:
return l.errorf("invalid rune %v", r)
}
return begin
case '<':
if next := l.read(); next == '=' {
l.emit(TokOpLtEq)
} else {
l.backup()
l.emit(TokOpLt)
}
return begin
case '>':
if next := l.read(); next == '=' {
l.emit(TokOpGtEq)
} else {
l.backup()
l.emit(TokOpGt)
}
return begin
case '(':
l.emit(TokParenOpen)
return begin
case ')':
l.emit(TokParenClose)
return begin
case '"':
return l.delimString(r)
case eof:
l.emit(TokEOF)
return nil
}
if unicode.IsSpace(r) {
l.ignore()
return begin
}
return unquotedString
}
func (l *Lexer) delimString(delim rune) stateFn {
// Ignore the start delimiter rune.
l.ignore()
for r := l.read(); r != delim; r = l.read() {
if r == eof {
return l.errorf("unexpected EOF, unclosed delimiter")
}
}
// Don't include the end delimiter in emitted token.
l.backup()
l.emit(TokString)
// Skip end delimiter.
l.skip()
return begin
}
func unquotedString(l *Lexer) stateFn {
for r := l.read(); ; r = l.read() {
switch {
case r == eof:
l.backup()
l.emitUnquotedString()
return begin
case unicode.IsSpace(r):
l.backup()
l.emitUnquotedString()
l.skip()
return begin
case isReserved(r):
l.backup()
l.emitUnquotedString()
return begin
}
}
}
func (l *Lexer) emitUnquotedString() {
str := l.input[l.start:l.pos]
if tokType, ok := keywords[str]; ok {
l.emit(tokType)
return
}
l.emit(TokString)
}
func isReserved(r rune) bool {
for _, v := range reservedRunes {
if r == v {
return true
}
}
return false
}

91
pkg/filter/lexer_test.go Normal file
View File

@ -0,0 +1,91 @@
package filter
import "testing"
func TestNextToken(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected []Token
}{
{
name: "unquoted string",
input: "foo bar",
expected: []Token{
{TokString, "foo"},
{TokString, "bar"},
{TokEOF, ""},
},
},
{
name: "quoted string",
input: `"foo bar" "baz"`,
expected: []Token{
{TokString, "foo bar"},
{TokString, "baz"},
{TokEOF, ""},
},
},
{
name: "boolean operator token types",
input: "NOT AND OR",
expected: []Token{
{TokOpNot, "NOT"},
{TokOpAnd, "AND"},
{TokOpOr, "OR"},
{TokEOF, ""},
},
},
{
name: "comparison operator token types",
input: `= != < > <= >= =~ !~`,
expected: []Token{
{TokOpEq, "="},
{TokOpNotEq, "!="},
{TokOpLt, "<"},
{TokOpGt, ">"},
{TokOpLtEq, "<="},
{TokOpGtEq, ">="},
{TokOpRe, "=~"},
{TokOpNotRe, "!~"},
{TokEOF, ""},
},
},
{
name: "with parentheses",
input: "(foo AND bar) OR baz",
expected: []Token{
{TokParenOpen, "("},
{TokString, "foo"},
{TokOpAnd, "AND"},
{TokString, "bar"},
{TokParenClose, ")"},
{TokOpOr, "OR"},
{TokString, "baz"},
{TokEOF, ""},
},
},
}
for i, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
l := NewLexer(tt.input)
for _, exp := range tt.expected {
got := l.Next()
if got.Type != exp.Type {
t.Errorf("invalid type (idx: %v, expected: %v, got: %v)",
i, exp.Type, got.Type)
}
if got.Literal != exp.Literal {
t.Errorf("invalid literal (idx: %v, expected: %v, got: %v)",
i, exp.Literal, got.Literal)
}
}
})
}
}

250
pkg/filter/parser.go Normal file
View File

@ -0,0 +1,250 @@
package filter
import (
"fmt"
"regexp"
)
type precedence int
const (
_ precedence = iota
precLowest
precAnd
precOr
precNot
precEq
precLessGreater
precPrefix
precGroup
)
type (
prefixParser func(*Parser) (Expression, error)
infixParser func(*Parser, Expression) (Expression, error)
)
var (
prefixParsers = map[TokenType]prefixParser{}
infixParsers = map[TokenType]infixParser{}
)
var tokenPrecedences = map[TokenType]precedence{
TokParenOpen: precGroup,
TokOpNot: precNot,
TokOpAnd: precAnd,
TokOpOr: precOr,
TokOpEq: precEq,
TokOpNotEq: precEq,
TokOpGt: precLessGreater,
TokOpLt: precLessGreater,
TokOpGtEq: precLessGreater,
TokOpLtEq: precLessGreater,
TokOpRe: precEq,
TokOpNotRe: precEq,
}
func init() {
// Populate maps in `init`, because package global variables would cause an
// initialization cycle.
infixOperators := []TokenType{
TokOpAnd,
TokOpOr,
TokOpEq,
TokOpNotEq,
TokOpGt,
TokOpLt,
TokOpGtEq,
TokOpLtEq,
TokOpRe,
TokOpNotRe,
}
for _, op := range infixOperators {
infixParsers[op] = parseInfixExpression
}
prefixParsers[TokOpNot] = parsePrefixExpression
prefixParsers[TokString] = parseStringLiteral
prefixParsers[TokParenOpen] = parseGroupedExpression
}
type Parser struct {
l *Lexer
cur Token
peek Token
}
func NewParser(l *Lexer) *Parser {
p := &Parser{l: l}
p.nextToken()
p.nextToken()
return p
}
func ParseQuery(input string) (expr Expression, err error) {
p := &Parser{l: NewLexer(input)}
p.nextToken()
p.nextToken()
if p.curTokenIs(TokEOF) {
return nil, fmt.Errorf("filter: unexpected EOF")
}
for !p.curTokenIs(TokEOF) {
right, err := p.parseExpression(precLowest)
switch {
case err != nil:
return nil, fmt.Errorf("filter: could not parse expression: %w", err)
case expr == nil:
expr = right
default:
expr = InfixExpression{
Operator: TokOpAnd,
Left: expr,
Right: right,
}
}
p.nextToken()
}
return
}
func (p *Parser) nextToken() {
p.cur = p.peek
p.peek = p.l.Next()
}
func (p *Parser) curTokenIs(t TokenType) bool {
return p.cur.Type == t
}
func (p *Parser) peekTokenIs(t TokenType) bool {
return p.peek.Type == t
}
func (p *Parser) curPrecedence() precedence {
if p, ok := tokenPrecedences[p.cur.Type]; ok {
return p
}
return precLowest
}
func (p *Parser) peekPrecedence() precedence {
if p, ok := tokenPrecedences[p.peek.Type]; ok {
return p
}
return precLowest
}
func (p *Parser) parseExpression(prec precedence) (Expression, error) {
prefixParser, ok := prefixParsers[p.cur.Type]
if !ok {
return nil, fmt.Errorf("no prefix parse function for %v found", p.cur.Type)
}
expr, err := prefixParser(p)
if err != nil {
return nil, fmt.Errorf("could not parse expression prefix: %w", err)
}
for !p.peekTokenIs(eof) && prec < p.peekPrecedence() {
infixParser, ok := infixParsers[p.peek.Type]
if !ok {
break
}
p.nextToken()
expr, err = infixParser(p, expr)
if err != nil {
return nil, fmt.Errorf("could not parse infix expression: %w", err)
}
}
return expr, nil
}
func parsePrefixExpression(p *Parser) (Expression, error) {
expr := PrefixExpression{
Operator: p.cur.Type,
}
p.nextToken()
right, err := p.parseExpression(precPrefix)
if err != nil {
return nil, fmt.Errorf("could not parse expression for right operand: %w", err)
}
expr.Right = right
return expr, nil
}
func parseInfixExpression(p *Parser, left Expression) (Expression, error) {
expr := InfixExpression{
Operator: p.cur.Type,
Left: left,
}
prec := p.curPrecedence()
p.nextToken()
right, err := p.parseExpression(prec)
if err != nil {
return nil, fmt.Errorf("could not parse expression for right operand: %w", err)
}
if expr.Operator == TokOpRe || expr.Operator == TokOpNotRe {
if rightStr, ok := right.(StringLiteral); ok {
re, err := regexp.Compile(rightStr.Value)
if err != nil {
return nil, fmt.Errorf("could not compile regular expression %q: %w", rightStr.Value, err)
}
right = RegexpLiteral{re}
}
}
expr.Right = right
return expr, nil
}
func parseStringLiteral(p *Parser) (Expression, error) {
return StringLiteral{Value: p.cur.Literal}, nil
}
func parseGroupedExpression(p *Parser) (Expression, error) {
p.nextToken()
expr, err := p.parseExpression(precLowest)
if err != nil {
return nil, fmt.Errorf("could not parse grouped expression: %w", err)
}
for p.nextToken(); !p.curTokenIs(TokParenClose); p.nextToken() {
if p.curTokenIs(TokEOF) {
return nil, fmt.Errorf("unexpected EOF: unmatched parentheses")
}
right, err := p.parseExpression(precLowest)
if err != nil {
return nil, fmt.Errorf("could not parse expression: %w", err)
}
expr = InfixExpression{
Operator: TokOpAnd,
Left: expr,
Right: right,
}
}
return expr, nil
}

249
pkg/filter/parser_test.go Normal file
View File

@ -0,0 +1,249 @@
package filter
import (
"errors"
"reflect"
"regexp"
"testing"
)
func TestParseQuery(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expectedExpression Expression
expectedError error
}{
{
name: "empty query",
input: "",
expectedExpression: nil,
expectedError: errors.New("filter: unexpected EOF"),
},
{
name: "string literal expression",
input: "foobar",
expectedExpression: StringLiteral{Value: "foobar"},
expectedError: nil,
},
{
name: "boolean expression with equal operator",
input: "foo = bar",
expectedExpression: InfixExpression{
Operator: TokOpEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with not equal operator",
input: "foo != bar",
expectedExpression: InfixExpression{
Operator: TokOpNotEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with greater than operator",
input: "foo > bar",
expectedExpression: InfixExpression{
Operator: TokOpGt,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with less than operator",
input: "foo < bar",
expectedExpression: InfixExpression{
Operator: TokOpLt,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with greater than or equal operator",
input: "foo >= bar",
expectedExpression: InfixExpression{
Operator: TokOpGtEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with less than or equal operator",
input: "foo <= bar",
expectedExpression: InfixExpression{
Operator: TokOpLtEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "boolean expression with regular expression operator",
input: "foo =~ bar",
expectedExpression: InfixExpression{
Operator: TokOpRe,
Left: StringLiteral{Value: "foo"},
Right: RegexpLiteral{regexp.MustCompile("bar")},
},
expectedError: nil,
},
{
name: "boolean expression with not regular expression operator",
input: "foo !~ bar",
expectedExpression: InfixExpression{
Operator: TokOpNotRe,
Left: StringLiteral{Value: "foo"},
Right: RegexpLiteral{regexp.MustCompile("bar")},
},
expectedError: nil,
},
{
name: "boolean expression with AND, OR and NOT operators",
input: "foo AND bar OR NOT baz",
expectedExpression: InfixExpression{
Operator: TokOpAnd,
Left: StringLiteral{Value: "foo"},
Right: InfixExpression{
Operator: TokOpOr,
Left: StringLiteral{Value: "bar"},
Right: PrefixExpression{
Operator: TokOpNot,
Right: StringLiteral{Value: "baz"},
},
},
},
expectedError: nil,
},
{
name: "boolean expression with nested group",
input: "(foo AND bar) OR NOT baz",
expectedExpression: InfixExpression{
Operator: TokOpOr,
Left: InfixExpression{
Operator: TokOpAnd,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
Right: PrefixExpression{
Operator: TokOpNot,
Right: StringLiteral{Value: "baz"},
},
},
expectedError: nil,
},
{
name: "implicit boolean expression with string literal operands",
input: "foo bar baz",
expectedExpression: InfixExpression{
Operator: TokOpAnd,
Left: InfixExpression{
Operator: TokOpAnd,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
Right: StringLiteral{Value: "baz"},
},
expectedError: nil,
},
{
name: "implicit boolean expression nested in group",
input: "(foo bar)",
expectedExpression: InfixExpression{
Operator: TokOpAnd,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
expectedError: nil,
},
{
name: "implicit and explicit boolean expression with string literal operands",
input: "foo bar OR baz yolo",
expectedExpression: InfixExpression{
Operator: TokOpAnd,
Left: InfixExpression{
Operator: TokOpAnd,
Left: StringLiteral{Value: "foo"},
Right: InfixExpression{
Operator: TokOpOr,
Left: StringLiteral{Value: "bar"},
Right: StringLiteral{Value: "baz"},
},
},
Right: StringLiteral{Value: "yolo"},
},
expectedError: nil,
},
{
name: "implicit boolean expression with comparison operands",
input: "foo=bar baz=~yolo",
expectedExpression: InfixExpression{
Operator: TokOpAnd,
Left: InfixExpression{
Operator: TokOpEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
Right: InfixExpression{
Operator: TokOpRe,
Left: StringLiteral{Value: "baz"},
Right: RegexpLiteral{regexp.MustCompile("yolo")},
},
},
expectedError: nil,
},
{
name: "eq operator takes precedence over boolean ops",
input: "foo=bar OR baz=yolo",
expectedExpression: InfixExpression{
Operator: TokOpOr,
Left: InfixExpression{
Operator: TokOpEq,
Left: StringLiteral{Value: "foo"},
Right: StringLiteral{Value: "bar"},
},
Right: InfixExpression{
Operator: TokOpEq,
Left: StringLiteral{Value: "baz"},
Right: StringLiteral{Value: "yolo"},
},
},
expectedError: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseQuery(tt.input)
assertError(t, tt.expectedError, err)
if !reflect.DeepEqual(tt.expectedExpression, got) {
t.Errorf("expected: %v, got: %v", tt.expectedExpression, got)
}
})
}
}
func assertError(t *testing.T, exp, got error) {
t.Helper()
switch {
case exp == nil && got != nil:
t.Fatalf("expected: nil, got: %v", got)
case exp != nil && got == nil:
t.Fatalf("expected: %v, got: nil", exp.Error())
case exp != nil && got != nil && exp.Error() != got.Error():
t.Fatalf("expected: %v, got: %v", exp.Error(), got.Error())
}
}