diff --git a/README.md b/README.md index 8295f56..cb92efb 100644 --- a/README.md +++ b/README.md @@ -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}}}]`. @@ -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? a 1`, true, "[]"}, {`a || 1`, true, "[]"}, {`a && 1`, true, "[]"}, diff --git a/scanner.go b/scanner.go index 3aa3e9d..0c625a4 100644 --- a/scanner.go +++ b/scanner.go @@ -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. @@ -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. @@ -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 { @@ -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 @@ -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] @@ -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 @@ -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 @@ -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 @@ -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 == '<' || @@ -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. diff --git a/scanner_test.go b/scanner_test.go index f53ec27..e51d538 100644 --- a/scanner_test.go +++ b/scanner_test.go @@ -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 }`}}}, @@ -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 !=}`}, @@ -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 )}`}}},