Skip to content

Commit

Permalink
added support for negative numbers and array/any operators
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Dec 25, 2022
1 parent 137cb8c commit 20dee0c
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 26 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ fexpr
[![GoDoc](https://godoc.org/github.com/ganigeorgiev/fexpr?status.svg)](https://pkg.go.dev/github.com/ganigeorgiev/fexpr)
================================================================================

**fexpr** is a filter query language parser that generates extremely easy to work with AST structure so that you can create safely SQL, Elasticsearch, etc. queries from user input.
**fexpr** is a filter query language parser that generates easy to work with AST structure so that you can create safely SQL, Elasticsearch, etc. queries from user input.

Or in other words, transform the string `"id > 1"` into the struct `[{&& {{identifier id} > {number 1}}}]`.

Expand Down Expand Up @@ -48,13 +48,21 @@ func main() {
- **`<=`** Less than or equal operator (eg. `a<=b`)
- **`~`** Like/Contains operator (eg. `a~b`)
- **`!~`** NOT Like/Contains operator (eg. `a!~b`)
- **`?=`** Array/Any equal operator (eg. `a?=b`)
- **`?!=`** Array/Any NOT Equal operator (eg. `a?!=b`)
- **`?>`** Array/Any Greater than operator (eg. `a?>b`)
- **`?>=`** Array/Any Greater than or equal operator (eg. `a?>=b`)
- **`?<`** Array/Any Less than or equal operator (eg. `a?<b`)
- **`?<=`** Array/Any Less than or equal operator (eg. `a?<=b`)
- **`?~`** Array/Any Like/Contains operator (eg. `a?~b`)
- **`?!~`** Array/Any NOT Like/Contains operator (eg. `a?!~b`)
- **`&&`** AND join operator (eg. `a=b && c=d`)
- **`||`** OR join operator (eg. `a=b || c=d`)
- **`()`** Parenthesis (eg. `(a=1 && b=2) || (a=3 && b=4)`)


#### Numbers
Number tokens are any integer or decimal numbers. **Example**: `123`, `10.50`.
Number tokens are any integer or decimal numbers. **Example**: `123`, `10.50`, `-14`.


#### Identifiers
Expand Down
10 changes: 5 additions & 5 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,22 @@ func Parse(text string) ([]ExprGroup, error) {
switch step {
case stepBeforeSign:
if t.Type != TokenIdentifier && t.Type != TokenText && t.Type != TokenNumber {
return nil, fmt.Errorf("Expected left operand (identifier, text or number), got %q (%s)", t.Literal, t.Type)
return nil, fmt.Errorf("expected left operand (identifier, text or number), got %q (%s)", t.Literal, t.Type)
}

expr = Expr{Left: t}

step = stepSign
case stepSign:
if t.Type != TokenSign {
return nil, fmt.Errorf("Expected a sign operator, got %q (%s)", t.Literal, t.Type)
return nil, fmt.Errorf("expected a sign operator, got %q (%s)", t.Literal, t.Type)
}

expr.Op = SignOp(t.Literal)
step = stepAfterSign
case stepAfterSign:
if t.Type != TokenIdentifier && t.Type != TokenText && t.Type != TokenNumber {
return nil, fmt.Errorf("Expected right operand (identifier, text or number), got %q (%s)", t.Literal, t.Type)
return nil, fmt.Errorf("expected right operand (identifier, text or number), got %q (%s)", t.Literal, t.Type)
}

expr.Right = t
Expand All @@ -96,7 +96,7 @@ func Parse(text string) ([]ExprGroup, error) {
step = StepJoin
case StepJoin:
if t.Type != TokenJoin {
return nil, fmt.Errorf("Expected && or ||, got %q (%s)", t.Literal, t.Type)
return nil, fmt.Errorf("expected && or ||, got %q (%s)", t.Literal, t.Type)
}

join = JoinAnd
Expand All @@ -109,7 +109,7 @@ func Parse(text string) ([]ExprGroup, error) {
}

if step != StepJoin {
return nil, errors.New("Invalid formatted filter expression.")
return nil, errors.New("invalid formatted filter expression")
}

return result, nil
Expand Down
2 changes: 2 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ func TestParse(t *testing.T) {
{`a ! 1`, true, "[]"},
{`a - 1`, true, "[]"},
{`a + 1`, true, "[]"},
{`1 - 1`, true, "[]"},
{`1 + 1`, true, "[]"},
{`> a 1`, true, "[]"},
{`a || 1`, true, "[]"},
{`a && 1`, true, "[]"},
Expand Down
60 changes: 43 additions & 17 deletions scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ const (
SignLte SignOp = "<="
SignGt SignOp = ">"
SignGte SignOp = ">="

// array/any operators
SignAnyEq SignOp = "?="
SignAnyNeq SignOp = "?!="
SignAnyLike SignOp = "?~"
SignAnyNlike SignOp = "?!~"
SignAnyLt SignOp = "?<"
SignAnyLte SignOp = "?<="
SignAnyGt SignOp = "?>"
SignAnyGte SignOp = "?>="
)

// TokenType represents a Token type.
Expand Down Expand Up @@ -112,7 +122,7 @@ func (s *Scanner) Scan() (Token, error) {
return Token{Type: TokenEOF, Literal: ""}, nil
}

return Token{Type: TokenUnexpected, Literal: string(ch)}, fmt.Errorf("Unexpected character %q", ch)
return Token{Type: TokenUnexpected, Literal: string(ch)}, fmt.Errorf("unexpected character %q", ch)
}

// scanWhitespace consumes all contiguous whitespace runes.
Expand Down Expand Up @@ -176,6 +186,9 @@ func (s *Scanner) scanIdentifier() (Token, error) {
func (s *Scanner) scanNumber() (Token, error) {
var buf bytes.Buffer

// read the number first rune to skip the sign (if exist)
buf.WriteRune(s.read())

// Read every subsequent digit rune into the buffer.
// Non-digit runes and EOF will cause the loop to exit.
for {
Expand All @@ -198,7 +211,7 @@ func (s *Scanner) scanNumber() (Token, error) {

var err error
if !isNumber(literal) {
err = fmt.Errorf("Invalid number %q", literal)
err = fmt.Errorf("invalid number %q", literal)
}

return Token{Type: TokenNumber, Literal: literal}, err
Expand Down Expand Up @@ -239,7 +252,7 @@ func (s *Scanner) scanText() (Token, error) {

var err error
if !hasMatchingQuotes {
err = fmt.Errorf("Invalid quoted text %q", literal)
err = fmt.Errorf("invalid quoted text %q", literal)
} else {
// unquote
literal = literal[1 : len(literal)-1]
Expand Down Expand Up @@ -277,7 +290,7 @@ func (s *Scanner) scanSign() (Token, error) {

var err error
if !isSignOperator(literal) {
err = fmt.Errorf("Invalid sign operator %q", literal)
err = fmt.Errorf("invalid sign operator %q", literal)
}

return Token{Type: TokenSign, Literal: literal}, err
Expand Down Expand Up @@ -309,7 +322,7 @@ func (s *Scanner) scanJoin() (Token, error) {

var err error
if !isJoinOperator(literal) {
err = fmt.Errorf("Invalid join operator %q", literal)
err = fmt.Errorf("invalid join operator %q", literal)
}

return Token{Type: TokenJoin, Literal: literal}, err
Expand Down Expand Up @@ -365,7 +378,7 @@ func (s *Scanner) scanGroup() (Token, error) {

var err error
if !isGroupStartRune(firstChar) || openGroups > 0 {
err = fmt.Errorf("Invalid formatted group - missing %d closing bracket(s).", openGroups)
err = fmt.Errorf("invalid formatted group - missing %d closing bracket(s)", openGroups)
}

return Token{Type: TokenGroup, Literal: literal}, err
Expand Down Expand Up @@ -415,12 +428,13 @@ func isTextStartRune(ch rune) bool {

// isNumberStartRune checks if a rune is a valid number start character (aka. digit).
func isNumberStartRune(ch rune) bool {
return isDigitRune(ch)
return ch == '-' || isDigitRune(ch)
}

// isSignStartRune checks if a rune is a valid sign operator start character.
func isSignStartRune(ch rune) bool {
return ch == '=' ||
ch == '?' ||
ch == '!' ||
ch == '>' ||
ch == '<' ||
Expand All @@ -439,16 +453,28 @@ func isGroupStartRune(ch rune) bool {

// isSignOperator checks if a literal is a valid sign operator.
func isSignOperator(literal string) bool {
op := SignOp(literal)

return op == SignEq ||
op == SignNeq ||
op == SignLt ||
op == SignLte ||
op == SignGt ||
op == SignGte ||
op == SignLike ||
op == SignNlike
switch SignOp(literal) {
case
SignEq,
SignNeq,
SignLt,
SignLte,
SignGt,
SignGte,
SignLike,
SignNlike,
SignAnyEq,
SignAnyNeq,
SignAnyLike,
SignAnyNlike,
SignAnyLt,
SignAnyLte,
SignAnyGt,
SignAnyGte:
return true
}

return false
}

// isJoinOperator checks if a literal is a valid join type operator.
Expand Down
24 changes: 22 additions & 2 deletions scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@ func TestScannerScan(t *testing.T) {
{`test"d`, []output{{false, `{identifier test}`}, {true, `{text "d}`}}},
// number
{`123`, []output{{false, `{number 123}`}}},
{`123.123`, []output{{false, `{number 123.123}`}}},
{`-123`, []output{{false, `{number -123}`}}},
{`-123.456`, []output{{false, `{number -123.456}`}}},
{`123.456`, []output{{false, `{number 123.456}`}}},
{`.123`, []output{{true, `{unexpected .}`}, {false, `{number 123}`}}},
{`- 123`, []output{{true, `{number -}`}, {false, `{whitespace }`}, {false, `{number 123}`}}},
{`12-3`, []output{{false, `{number 12}`}, {false, `{number -3}`}}},
{`123.abc`, []output{{true, `{number 123.}`}, {false, `{identifier abc}`}}},
// text
{`""`, []output{{false, `{text }`}}},
Expand All @@ -58,7 +62,7 @@ func TestScannerScan(t *testing.T) {
{`'||test&&'&&123`, []output{{false, `{text ||test&&}`}, {false, `{join &&}`}, {false, `{number 123}`}}},
// expression signs
{`=!=`, []output{{true, `{sign =!=}`}}},
{`= != ~ !~ > >= < <=`, []output{
{`= != ~ !~ > >= < <= ?= ?!= ?~ ?!~ ?> ?>= ?< ?<=`, []output{
{false, `{sign =}`},
{false, `{whitespace }`},
{false, `{sign !=}`},
Expand All @@ -74,6 +78,22 @@ func TestScannerScan(t *testing.T) {
{false, `{sign <}`},
{false, `{whitespace }`},
{false, `{sign <=}`},
{false, `{whitespace }`},
{false, `{sign ?=}`},
{false, `{whitespace }`},
{false, `{sign ?!=}`},
{false, `{whitespace }`},
{false, `{sign ?~}`},
{false, `{whitespace }`},
{false, `{sign ?!~}`},
{false, `{whitespace }`},
{false, `{sign ?>}`},
{false, `{whitespace }`},
{false, `{sign ?>=}`},
{false, `{whitespace }`},
{false, `{sign ?<}`},
{false, `{whitespace }`},
{false, `{sign ?<=}`},
}},
// groups/parenthesis
{`a)`, []output{{false, `{identifier a}`}, {true, `{unexpected )}`}}},
Expand Down

0 comments on commit 20dee0c

Please sign in to comment.