mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Rename search
package to filter
package
This commit is contained in:
87
pkg/filter/ast.go
Normal file
87
pkg/filter/ast.go
Normal 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
212
pkg/filter/ast_test.go
Normal 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
82
pkg/filter/http.go
Normal 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
276
pkg/filter/lexer.go
Normal 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
91
pkg/filter/lexer_test.go
Normal 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
250
pkg/filter/parser.go
Normal 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
249
pkg/filter/parser_test.go
Normal 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())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user