diff --git a/execution/execute.go b/execution/execute.go index 445d0a5..8103052 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -102,6 +102,11 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return branchExprExecutor(e) case ast.ArrayExpr: return arrayExprExecutor(e) + case ast.RegexExpr: + // Noop + return func(data *model.Value) (*model.Value, error) { + return data, nil + }, nil default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 266fa0c..41a9f38 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -65,8 +65,20 @@ func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { return nil, fmt.Errorf("error getting right bool value: %w", err) } return model.NewBoolValue(leftBool || rightBool), nil - //case lexer.Like: - //case lexer.NotLike: + case lexer.Like, lexer.NotLike: + leftStr, err := left.StringValue() + if err != nil { + return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + } + rightPatt, ok := e.Right.(ast.RegexExpr) + if !ok { + return nil, fmt.Errorf("like requires right side to be a regex pattern") + } + res := rightPatt.Regex.MatchString(leftStr) + if e.Operator.Kind == lexer.NotLike { + res = !res + } + return model.NewBoolValue(res), nil default: return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) } diff --git a/execution/execute_test.go b/execution/execute_test.go index 6d88ef2..f9c699f 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -168,6 +168,14 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `2 <= 2`, out: model.NewBoolValue(true), })) + t.Run("like", runTest(testCase{ + s: `"hello world" =~ r/ello/`, + out: model.NewBoolValue(true), + })) + t.Run("not like", runTest(testCase{ + s: `"hello world" !~ r/helloworld/`, + out: model.NewBoolValue(true), + })) }) t.Run("variables", func(t *testing.T) { diff --git a/execution/func.go b/execution/func.go index 193dda9..e5485a7 100644 --- a/execution/func.go +++ b/execution/func.go @@ -60,4 +60,31 @@ func init() { } return model.NewIntValue(intRes), nil }) + + RegisterFunc("toString", func(data *model.Value, args model.Values) (*model.Value, error) { + switch data.Type() { + case model.TypeString: + return data, nil + case model.TypeInt: + i, err := data.IntValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%d", i)), nil + case model.TypeFloat: + i, err := data.FloatValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%f", i)), nil + case model.TypeBool: + i, err := data.BoolValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%v", i)), nil + default: + return nil, fmt.Errorf("cannot convert %s to string", data.Type()) + } + }) } diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go index b369b9f..bb116ef 100644 --- a/selector/ast/expression_literal.go +++ b/selector/ast/expression_literal.go @@ -1,5 +1,7 @@ package ast +import "regexp" + type NumberFloatExpr struct { Value float64 } @@ -23,3 +25,9 @@ type BoolExpr struct { } func (BoolExpr) expr() {} + +type RegexExpr struct { + Regex *regexp.Regexp +} + +func (RegexExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index cfb96e3..f984c92 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -57,6 +57,7 @@ const ( Branch Map Filter + RegexPattern ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index fd60150..27213ea 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -197,6 +197,19 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return ptr.To(NewToken(kind, other, pos, l)) } + matchRegexPattern := func(pos int) *Token { + if !(p.src[pos] == 'r' && p.peekRuneEqual(pos+1, '/')) { + return nil + } + start := pos + pos += 2 + for !p.peekRuneEqual(pos, '/') { + pos++ + } + pos++ + return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start)) + } + if t := matchStr(pos, "null", true, Null); t != nil { return *t, nil } @@ -225,6 +238,10 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return *t, nil } + if t := matchRegexPattern(pos); t != nil { + return *t, nil + } + if unicode.IsDigit(rune(p.src[pos])) { // Handle whole numbers for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) { diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 2bd2105..fed02c5 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -46,6 +46,14 @@ func TestTokenizer_Parse(t *testing.T) { }, })) + t.Run("regex", runTest(testCase{ + in: `r/asd/ r/hello there/`, + out: []TokenKind{ + RegexPattern, + RegexPattern, + }, + })) + t.Run("everything", runTest(testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", out: []TokenKind{ diff --git a/selector/parser/parse_literal.go b/selector/parser/parse_literal.go index 870390e..932934a 100644 --- a/selector/parser/parse_literal.go +++ b/selector/parser/parse_literal.go @@ -1,6 +1,8 @@ package parser import ( + "fmt" + "regexp" "strconv" "strings" @@ -62,3 +64,22 @@ func parseNumberLiteral(p *Parser) (ast.Expr, error) { }, nil } } + +func parseRegexPattern(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.RegexPattern); err != nil { + return nil, err + } + + pattern := p.current() + + p.advance() + + comp, err := regexp.Compile(pattern.Value) + if err != nil { + return nil, fmt.Errorf("failed to compile regexp pattern: %w", err) + } + + return ast.RegexExpr{ + Regex: comp, + }, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index c6eae06..7810e2c 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -108,6 +108,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseMap(p) case lexer.Filter: left, err = parseFilter(p) + case lexer.RegexPattern: + left, err = parseRegexPattern(p) default: return nil, &UnexpectedTokenError{ Token: p.current(),