From 534f0135332197e7896b7d64fab352059a5be90b Mon Sep 17 00:00:00 2001 From: Mauro Valota <7254293+valmoz@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:50:26 +0200 Subject: [PATCH] feat: adds negated patterns (#6) * feat: adds negated patterns * fix: fixed parser --- grammar/ApiFilter.g4 | 2 +- src/ApiFilter/Filter/Operator.php | 2 + src/ApiFilter/FilterFactory.php | 8 +- src/ApiFilter/Parser/ApiFilter.interp | 2 +- src/ApiFilter/Parser/ApiFilterParser.php | 97 +++++++++++++++++------- tests/ApiFilter/Filter/ConditionTest.php | 9 +++ tests/ApiFilter/FilterFactoryTest.php | 46 +++++++++++ 7 files changed, 135 insertions(+), 31 deletions(-) diff --git a/grammar/ApiFilter.g4 b/grammar/ApiFilter.g4 index 07e93e4..0b81dc6 100644 --- a/grammar/ApiFilter.g4 +++ b/grammar/ApiFilter.g4 @@ -26,7 +26,7 @@ comparisonop: EQ | GT | GTE | LT | LTE | NEQ; value: (BOOL | STRING | integer | decimal); pattern: FIELD patternop STRING; -patternop: LIKE | CONTAINS | STARTSWITH | ENDSWITH; +patternop: LIKE | CONTAINS | NOT LIKE | NOT CONTAINS | STARTSWITH | ENDSWITH; integer: INT; decimal: INT DOT INT; diff --git a/src/ApiFilter/Filter/Operator.php b/src/ApiFilter/Filter/Operator.php index 469f0cd..7d8c752 100644 --- a/src/ApiFilter/Filter/Operator.php +++ b/src/ApiFilter/Filter/Operator.php @@ -11,6 +11,8 @@ abstract class Operator const LTE = "<="; const NEQ = "<>"; const LIKE = "like"; + const NOT_LIKE = "not like"; + const NOT_CONTAINS = "not contains"; const CONTAINS = "contains"; const STARTS_WITH = "starts with"; const ENDS_WITH = "ends with"; diff --git a/src/ApiFilter/FilterFactory.php b/src/ApiFilter/FilterFactory.php index 72c0e9d..452e7df 100644 --- a/src/ApiFilter/FilterFactory.php +++ b/src/ApiFilter/FilterFactory.php @@ -165,7 +165,13 @@ public function visitPattern(Context\PatternContext $context): Condition public function visitPatternop(Context\PatternopContext $context) { $op = null; - if ($context->LIKE()) { + if ($context->NOT()) { + if ($context->LIKE()) { + $op = Operator::NOT_LIKE; + } elseif ($context->CONTAINS()) { + $op = Operator::NOT_CONTAINS; + } + } elseif ($context->LIKE()) { $op = Operator::LIKE; } elseif ($context->CONTAINS()) { $op = Operator::CONTAINS; diff --git a/src/ApiFilter/Parser/ApiFilter.interp b/src/ApiFilter/Parser/ApiFilter.interp index e9f8ffc..6057abb 100644 --- a/src/ApiFilter/Parser/ApiFilter.interp +++ b/src/ApiFilter/Parser/ApiFilter.interp @@ -72,4 +72,4 @@ decimal atn: -[3, 24715, 42794, 33075, 47597, 16764, 15335, 30598, 22884, 3, 28, 91, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 37, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 45, 10, 3, 12, 3, 14, 3, 48, 11, 3, 3, 4, 3, 4, 3, 4, 5, 4, 53, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 67, 10, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 5, 9, 77, 10, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 2, 3, 4, 14, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 2, 5, 4, 2, 3, 3, 20, 20, 3, 2, 3, 8, 3, 2, 9, 12, 2, 88, 2, 26, 3, 2, 2, 2, 4, 36, 3, 2, 2, 2, 6, 52, 3, 2, 2, 2, 8, 54, 3, 2, 2, 2, 10, 58, 3, 2, 2, 2, 12, 62, 3, 2, 2, 2, 14, 70, 3, 2, 2, 2, 16, 76, 3, 2, 2, 2, 18, 78, 3, 2, 2, 2, 20, 82, 3, 2, 2, 2, 22, 84, 3, 2, 2, 2, 24, 86, 3, 2, 2, 2, 26, 27, 5, 4, 3, 2, 27, 28, 7, 2, 2, 3, 28, 3, 3, 2, 2, 2, 29, 30, 8, 3, 1, 2, 30, 37, 5, 6, 4, 2, 31, 37, 5, 18, 10, 2, 32, 33, 7, 23, 2, 2, 33, 34, 5, 4, 3, 2, 34, 35, 7, 24, 2, 2, 35, 37, 3, 2, 2, 2, 36, 29, 3, 2, 2, 2, 36, 31, 3, 2, 2, 2, 36, 32, 3, 2, 2, 2, 37, 46, 3, 2, 2, 2, 38, 39, 12, 4, 2, 2, 39, 40, 7, 18, 2, 2, 40, 45, 5, 4, 3, 5, 41, 42, 12, 3, 2, 2, 42, 43, 7, 19, 2, 2, 43, 45, 5, 4, 3, 4, 44, 38, 3, 2, 2, 2, 44, 41, 3, 2, 2, 2, 45, 48, 3, 2, 2, 2, 46, 44, 3, 2, 2, 2, 46, 47, 3, 2, 2, 2, 47, 5, 3, 2, 2, 2, 48, 46, 3, 2, 2, 2, 49, 53, 5, 8, 5, 2, 50, 53, 5, 10, 6, 2, 51, 53, 5, 12, 7, 2, 52, 49, 3, 2, 2, 2, 52, 50, 3, 2, 2, 2, 52, 51, 3, 2, 2, 2, 53, 7, 3, 2, 2, 2, 54, 55, 7, 27, 2, 2, 55, 56, 5, 14, 8, 2, 56, 57, 5, 16, 9, 2, 57, 9, 3, 2, 2, 2, 58, 59, 7, 27, 2, 2, 59, 60, 9, 2, 2, 2, 60, 61, 7, 21, 2, 2, 61, 11, 3, 2, 2, 2, 62, 66, 7, 27, 2, 2, 63, 67, 7, 8, 2, 2, 64, 65, 7, 20, 2, 2, 65, 67, 7, 22, 2, 2, 66, 63, 3, 2, 2, 2, 66, 64, 3, 2, 2, 2, 67, 68, 3, 2, 2, 2, 68, 69, 7, 21, 2, 2, 69, 13, 3, 2, 2, 2, 70, 71, 9, 3, 2, 2, 71, 15, 3, 2, 2, 2, 72, 77, 7, 16, 2, 2, 73, 77, 7, 17, 2, 2, 74, 77, 5, 22, 12, 2, 75, 77, 5, 24, 13, 2, 76, 72, 3, 2, 2, 2, 76, 73, 3, 2, 2, 2, 76, 74, 3, 2, 2, 2, 76, 75, 3, 2, 2, 2, 77, 17, 3, 2, 2, 2, 78, 79, 7, 27, 2, 2, 79, 80, 5, 20, 11, 2, 80, 81, 7, 17, 2, 2, 81, 19, 3, 2, 2, 2, 82, 83, 9, 4, 2, 2, 83, 21, 3, 2, 2, 2, 84, 85, 7, 25, 2, 2, 85, 23, 3, 2, 2, 2, 86, 87, 7, 25, 2, 2, 87, 88, 7, 26, 2, 2, 88, 89, 7, 25, 2, 2, 89, 25, 3, 2, 2, 2, 8, 36, 44, 46, 52, 66, 76] \ No newline at end of file +[3, 24715, 42794, 33075, 47597, 16764, 15335, 30598, 22884, 3, 28, 99, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 3, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 37, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 7, 3, 45, 10, 3, 12, 3, 14, 3, 48, 11, 3, 3, 4, 3, 4, 3, 4, 5, 4, 53, 10, 4, 3, 5, 3, 5, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 5, 7, 67, 10, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 9, 5, 9, 77, 10, 9, 3, 10, 3, 10, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 91, 10, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 3, 13, 2, 3, 4, 14, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 2, 4, 4, 2, 3, 3, 20, 20, 3, 2, 3, 8, 2, 101, 2, 26, 3, 2, 2, 2, 4, 36, 3, 2, 2, 2, 6, 52, 3, 2, 2, 2, 8, 54, 3, 2, 2, 2, 10, 58, 3, 2, 2, 2, 12, 62, 3, 2, 2, 2, 14, 70, 3, 2, 2, 2, 16, 76, 3, 2, 2, 2, 18, 78, 3, 2, 2, 2, 20, 90, 3, 2, 2, 2, 22, 92, 3, 2, 2, 2, 24, 94, 3, 2, 2, 2, 26, 27, 5, 4, 3, 2, 27, 28, 7, 2, 2, 3, 28, 3, 3, 2, 2, 2, 29, 30, 8, 3, 1, 2, 30, 37, 5, 6, 4, 2, 31, 37, 5, 18, 10, 2, 32, 33, 7, 23, 2, 2, 33, 34, 5, 4, 3, 2, 34, 35, 7, 24, 2, 2, 35, 37, 3, 2, 2, 2, 36, 29, 3, 2, 2, 2, 36, 31, 3, 2, 2, 2, 36, 32, 3, 2, 2, 2, 37, 46, 3, 2, 2, 2, 38, 39, 12, 4, 2, 2, 39, 40, 7, 18, 2, 2, 40, 45, 5, 4, 3, 5, 41, 42, 12, 3, 2, 2, 42, 43, 7, 19, 2, 2, 43, 45, 5, 4, 3, 4, 44, 38, 3, 2, 2, 2, 44, 41, 3, 2, 2, 2, 45, 48, 3, 2, 2, 2, 46, 44, 3, 2, 2, 2, 46, 47, 3, 2, 2, 2, 47, 5, 3, 2, 2, 2, 48, 46, 3, 2, 2, 2, 49, 53, 5, 8, 5, 2, 50, 53, 5, 10, 6, 2, 51, 53, 5, 12, 7, 2, 52, 49, 3, 2, 2, 2, 52, 50, 3, 2, 2, 2, 52, 51, 3, 2, 2, 2, 53, 7, 3, 2, 2, 2, 54, 55, 7, 27, 2, 2, 55, 56, 5, 14, 8, 2, 56, 57, 5, 16, 9, 2, 57, 9, 3, 2, 2, 2, 58, 59, 7, 27, 2, 2, 59, 60, 9, 2, 2, 2, 60, 61, 7, 21, 2, 2, 61, 11, 3, 2, 2, 2, 62, 66, 7, 27, 2, 2, 63, 67, 7, 8, 2, 2, 64, 65, 7, 20, 2, 2, 65, 67, 7, 22, 2, 2, 66, 63, 3, 2, 2, 2, 66, 64, 3, 2, 2, 2, 67, 68, 3, 2, 2, 2, 68, 69, 7, 21, 2, 2, 69, 13, 3, 2, 2, 2, 70, 71, 9, 3, 2, 2, 71, 15, 3, 2, 2, 2, 72, 77, 7, 16, 2, 2, 73, 77, 7, 17, 2, 2, 74, 77, 5, 22, 12, 2, 75, 77, 5, 24, 13, 2, 76, 72, 3, 2, 2, 2, 76, 73, 3, 2, 2, 2, 76, 74, 3, 2, 2, 2, 76, 75, 3, 2, 2, 2, 77, 17, 3, 2, 2, 2, 78, 79, 7, 27, 2, 2, 79, 80, 5, 20, 11, 2, 80, 81, 7, 17, 2, 2, 81, 19, 3, 2, 2, 2, 82, 91, 7, 9, 2, 2, 83, 91, 7, 10, 2, 2, 84, 85, 7, 22, 2, 2, 85, 91, 7, 9, 2, 2, 86, 87, 7, 22, 2, 2, 87, 91, 7, 10, 2, 2, 88, 91, 7, 11, 2, 2, 89, 91, 7, 12, 2, 2, 90, 82, 3, 2, 2, 2, 90, 83, 3, 2, 2, 2, 90, 84, 3, 2, 2, 2, 90, 86, 3, 2, 2, 2, 90, 88, 3, 2, 2, 2, 90, 89, 3, 2, 2, 2, 91, 21, 3, 2, 2, 2, 92, 93, 7, 25, 2, 2, 93, 23, 3, 2, 2, 2, 94, 95, 7, 25, 2, 2, 95, 96, 7, 26, 2, 2, 96, 97, 7, 25, 2, 2, 97, 25, 3, 2, 2, 2, 9, 36, 44, 46, 52, 66, 76, 90] \ No newline at end of file diff --git a/src/ApiFilter/Parser/ApiFilterParser.php b/src/ApiFilter/Parser/ApiFilterParser.php index d285ebd..c707979 100644 --- a/src/ApiFilter/Parser/ApiFilterParser.php +++ b/src/ApiFilter/Parser/ApiFilterParser.php @@ -66,7 +66,7 @@ final class ApiFilterParser extends Parser */ private const SERIALIZED_ATN = "\u{3}\u{608B}\u{A72A}\u{8133}\u{B9ED}\u{417C}\u{3BE7}\u{7786}\u{5964}" . - "\u{3}\u{1C}\u{5B}\u{4}\u{2}\u{9}\u{2}\u{4}\u{3}\u{9}\u{3}\u{4}\u{4}" . + "\u{3}\u{1C}\u{63}\u{4}\u{2}\u{9}\u{2}\u{4}\u{3}\u{9}\u{3}\u{4}\u{4}" . "\u{9}\u{4}\u{4}\u{5}\u{9}\u{5}\u{4}\u{6}\u{9}\u{6}\u{4}\u{7}\u{9}" . "\u{7}\u{4}\u{8}\u{9}\u{8}\u{4}\u{9}\u{9}\u{9}\u{4}\u{A}\u{9}\u{A}" . "\u{4}\u{B}\u{9}\u{B}\u{4}\u{C}\u{9}\u{C}\u{4}\u{D}\u{9}\u{D}\u{3}" . @@ -79,16 +79,17 @@ final class ApiFilterParser extends Parser "\u{7}\u{3}\u{7}\u{3}\u{7}\u{5}\u{7}\u{43}\u{A}\u{7}\u{3}\u{7}\u{3}" . "\u{7}\u{3}\u{8}\u{3}\u{8}\u{3}\u{9}\u{3}\u{9}\u{3}\u{9}\u{3}\u{9}" . "\u{5}\u{9}\u{4D}\u{A}\u{9}\u{3}\u{A}\u{3}\u{A}\u{3}\u{A}\u{3}\u{A}" . - "\u{3}\u{B}\u{3}\u{B}\u{3}\u{C}\u{3}\u{C}\u{3}\u{D}\u{3}\u{D}\u{3}" . - "\u{D}\u{3}\u{D}\u{3}\u{D}\u{2}\u{3}\u{4}\u{E}\u{2}\u{4}\u{6}\u{8}" . - "\u{A}\u{C}\u{E}\u{10}\u{12}\u{14}\u{16}\u{18}\u{2}\u{5}\u{4}\u{2}" . - "\u{3}\u{3}\u{14}\u{14}\u{3}\u{2}\u{3}\u{8}\u{3}\u{2}\u{9}\u{C}\u{2}" . - "\u{58}\u{2}\u{1A}\u{3}\u{2}\u{2}\u{2}\u{4}\u{24}\u{3}\u{2}\u{2}\u{2}" . + "\u{3}\u{B}\u{3}\u{B}\u{3}\u{B}\u{3}\u{B}\u{3}\u{B}\u{3}\u{B}\u{3}" . + "\u{B}\u{3}\u{B}\u{5}\u{B}\u{5B}\u{A}\u{B}\u{3}\u{C}\u{3}\u{C}\u{3}" . + "\u{D}\u{3}\u{D}\u{3}\u{D}\u{3}\u{D}\u{3}\u{D}\u{2}\u{3}\u{4}\u{E}" . + "\u{2}\u{4}\u{6}\u{8}\u{A}\u{C}\u{E}\u{10}\u{12}\u{14}\u{16}\u{18}" . + "\u{2}\u{4}\u{4}\u{2}\u{3}\u{3}\u{14}\u{14}\u{3}\u{2}\u{3}\u{8}\u{2}" . + "\u{65}\u{2}\u{1A}\u{3}\u{2}\u{2}\u{2}\u{4}\u{24}\u{3}\u{2}\u{2}\u{2}" . "\u{6}\u{34}\u{3}\u{2}\u{2}\u{2}\u{8}\u{36}\u{3}\u{2}\u{2}\u{2}\u{A}" . "\u{3A}\u{3}\u{2}\u{2}\u{2}\u{C}\u{3E}\u{3}\u{2}\u{2}\u{2}\u{E}\u{46}" . "\u{3}\u{2}\u{2}\u{2}\u{10}\u{4C}\u{3}\u{2}\u{2}\u{2}\u{12}\u{4E}\u{3}" . - "\u{2}\u{2}\u{2}\u{14}\u{52}\u{3}\u{2}\u{2}\u{2}\u{16}\u{54}\u{3}\u{2}" . - "\u{2}\u{2}\u{18}\u{56}\u{3}\u{2}\u{2}\u{2}\u{1A}\u{1B}\u{5}\u{4}\u{3}" . + "\u{2}\u{2}\u{2}\u{14}\u{5A}\u{3}\u{2}\u{2}\u{2}\u{16}\u{5C}\u{3}\u{2}" . + "\u{2}\u{2}\u{18}\u{5E}\u{3}\u{2}\u{2}\u{2}\u{1A}\u{1B}\u{5}\u{4}\u{3}" . "\u{2}\u{1B}\u{1C}\u{7}\u{2}\u{2}\u{3}\u{1C}\u{3}\u{3}\u{2}\u{2}\u{2}" . "\u{1D}\u{1E}\u{8}\u{3}\u{1}\u{2}\u{1E}\u{25}\u{5}\u{6}\u{4}\u{2}\u{1F}" . "\u{25}\u{5}\u{12}\u{A}\u{2}\u{20}\u{21}\u{7}\u{17}\u{2}\u{2}\u{21}" . @@ -120,11 +121,17 @@ final class ApiFilterParser extends Parser "\u{4C}\u{4B}\u{3}\u{2}\u{2}\u{2}\u{4D}\u{11}\u{3}\u{2}\u{2}\u{2}\u{4E}" . "\u{4F}\u{7}\u{1B}\u{2}\u{2}\u{4F}\u{50}\u{5}\u{14}\u{B}\u{2}\u{50}" . "\u{51}\u{7}\u{11}\u{2}\u{2}\u{51}\u{13}\u{3}\u{2}\u{2}\u{2}\u{52}" . - "\u{53}\u{9}\u{4}\u{2}\u{2}\u{53}\u{15}\u{3}\u{2}\u{2}\u{2}\u{54}\u{55}" . - "\u{7}\u{19}\u{2}\u{2}\u{55}\u{17}\u{3}\u{2}\u{2}\u{2}\u{56}\u{57}" . - "\u{7}\u{19}\u{2}\u{2}\u{57}\u{58}\u{7}\u{1A}\u{2}\u{2}\u{58}\u{59}" . - "\u{7}\u{19}\u{2}\u{2}\u{59}\u{19}\u{3}\u{2}\u{2}\u{2}\u{8}\u{24}\u{2C}" . - "\u{2E}\u{34}\u{42}\u{4C}"; + "\u{5B}\u{7}\u{9}\u{2}\u{2}\u{53}\u{5B}\u{7}\u{A}\u{2}\u{2}\u{54}\u{55}" . + "\u{7}\u{16}\u{2}\u{2}\u{55}\u{5B}\u{7}\u{9}\u{2}\u{2}\u{56}\u{57}" . + "\u{7}\u{16}\u{2}\u{2}\u{57}\u{5B}\u{7}\u{A}\u{2}\u{2}\u{58}\u{5B}" . + "\u{7}\u{B}\u{2}\u{2}\u{59}\u{5B}\u{7}\u{C}\u{2}\u{2}\u{5A}\u{52}\u{3}" . + "\u{2}\u{2}\u{2}\u{5A}\u{53}\u{3}\u{2}\u{2}\u{2}\u{5A}\u{54}\u{3}\u{2}" . + "\u{2}\u{2}\u{5A}\u{56}\u{3}\u{2}\u{2}\u{2}\u{5A}\u{58}\u{3}\u{2}\u{2}" . + "\u{2}\u{5A}\u{59}\u{3}\u{2}\u{2}\u{2}\u{5B}\u{15}\u{3}\u{2}\u{2}\u{2}" . + "\u{5C}\u{5D}\u{7}\u{19}\u{2}\u{2}\u{5D}\u{17}\u{3}\u{2}\u{2}\u{2}" . + "\u{5E}\u{5F}\u{7}\u{19}\u{2}\u{2}\u{5F}\u{60}\u{7}\u{1A}\u{2}\u{2}" . + "\u{60}\u{61}\u{7}\u{19}\u{2}\u{2}\u{61}\u{19}\u{3}\u{2}\u{2}\u{2}" . + "\u{9}\u{24}\u{2C}\u{2E}\u{34}\u{42}\u{4C}\u{5A}"; protected static $atn; protected static $decisionToDFA; @@ -609,20 +616,49 @@ public function patternop() : Context\PatternopContext $this->enterRule($localContext, 18, self::RULE_patternop); try { - $this->enterOuterAlt($localContext, 1); - $this->setState(80); + $this->setState(88); + $this->errorHandler->sync($this); - $_la = $this->input->LA(1); + switch ($this->getInterpreter()->adaptivePredict($this->input, 6, $this->ctx)) { + case 1: + $this->enterOuterAlt($localContext, 1); + $this->setState(80); + $this->match(self::LIKE); + break; - if (!(((($_la) & ~0x3f) === 0 && ((1 << $_la) & ((1 << self::LIKE) | (1 << self::CONTAINS) | (1 << self::STARTSWITH) | (1 << self::ENDSWITH))) !== 0))) { - $this->errorHandler->recoverInline($this); - } else { - if ($this->input->LA(1) === Token::EOF) { - $this->matchedEOF = true; - } + case 2: + $this->enterOuterAlt($localContext, 2); + $this->setState(81); + $this->match(self::CONTAINS); + break; - $this->errorHandler->reportMatch($this); - $this->consume(); + case 3: + $this->enterOuterAlt($localContext, 3); + $this->setState(82); + $this->match(self::NOT); + $this->setState(83); + $this->match(self::LIKE); + break; + + case 4: + $this->enterOuterAlt($localContext, 4); + $this->setState(84); + $this->match(self::NOT); + $this->setState(85); + $this->match(self::CONTAINS); + break; + + case 5: + $this->enterOuterAlt($localContext, 5); + $this->setState(86); + $this->match(self::STARTSWITH); + break; + + case 6: + $this->enterOuterAlt($localContext, 6); + $this->setState(87); + $this->match(self::ENDSWITH); + break; } } catch (RecognitionException $exception) { $localContext->exception = $exception; @@ -646,7 +682,7 @@ public function integer() : Context\IntegerContext try { $this->enterOuterAlt($localContext, 1); - $this->setState(82); + $this->setState(90); $this->match(self::INT); } catch (RecognitionException $exception) { $localContext->exception = $exception; @@ -670,11 +706,11 @@ public function decimal() : Context\DecimalContext try { $this->enterOuterAlt($localContext, 1); - $this->setState(84); + $this->setState(92); $this->match(self::INT); - $this->setState(85); + $this->setState(93); $this->match(self::DOT); - $this->setState(86); + $this->setState(94); $this->match(self::INT); } catch (RecognitionException $exception) { $localContext->exception = $exception; @@ -1297,6 +1333,11 @@ public function CONTAINS() : ?TerminalNode return $this->getToken(ApiFilterParser::CONTAINS, 0); } + public function NOT() : ?TerminalNode + { + return $this->getToken(ApiFilterParser::NOT, 0); + } + public function STARTSWITH() : ?TerminalNode { return $this->getToken(ApiFilterParser::STARTSWITH, 0); diff --git a/tests/ApiFilter/Filter/ConditionTest.php b/tests/ApiFilter/Filter/ConditionTest.php index 017c351..0bb9414 100644 --- a/tests/ApiFilter/Filter/ConditionTest.php +++ b/tests/ApiFilter/Filter/ConditionTest.php @@ -96,6 +96,12 @@ public function testOperator() $condition->setOp(Operator::CONTAINS); $this->assertEquals(Operator::CONTAINS, $condition->getOp()); + $condition->setOp(Operator::NOT_LIKE); + $this->assertEquals(Operator::NOT_LIKE, $condition->getOp()); + + $condition->setOp(Operator::NOT_CONTAINS); + $this->assertEquals(Operator::NOT_CONTAINS, $condition->getOp()); + $condition->setOp(Operator::STARTS_WITH); $this->assertEquals(Operator::STARTS_WITH, $condition->getOp()); @@ -116,5 +122,8 @@ public function testToString() $condition = new Condition("city", Operator::LIKE, "%aco"); $this->assertEquals("city like %aco", (string)$condition); + + $condition = new Condition("city", Operator::NOT_LIKE, "%aco"); + $this->assertEquals("city not like %aco", (string)$condition); } } diff --git a/tests/ApiFilter/FilterFactoryTest.php b/tests/ApiFilter/FilterFactoryTest.php index 884961b..8c06cc2 100644 --- a/tests/ApiFilter/FilterFactoryTest.php +++ b/tests/ApiFilter/FilterFactoryTest.php @@ -143,6 +143,34 @@ public function testContainsOp() $this->assertEquals($expected2, $filter2); } + /** + * Test 'not like' operator + */ + public function testNotLikeOp() + { + $filter1 = $this->factory->initFilter("name not like '%ergam%'"); + $expected1 = new Filter(new Condition("name", Operator::NOT_LIKE, "%ergam%")); + $this->assertEquals($expected1, $filter1); + + $filter2 = $this->factory->initFilter("name NOT LIKE '%ergam%'"); + $expected2 = new Filter(new Condition("name", Operator::NOT_LIKE, "%ergam%")); + $this->assertEquals($expected2, $filter2); + } + + /** + * Test 'not contains' operator + */ + public function testNotContainsOp() + { + $filter1 = $this->factory->initFilter("name not contains 'ergam'"); + $expected1 = new Filter(new Condition("name", Operator::NOT_CONTAINS, "ergam")); + $this->assertEquals($expected1, $filter1); + + $filter2 = $this->factory->initFilter("name NOT CONTAINS 'ergam'"); + $expected2 = new Filter(new Condition("name", Operator::NOT_CONTAINS, "ergam")); + $this->assertEquals($expected2, $filter2); + } + /** * Test 'starts with' operator */ @@ -203,6 +231,24 @@ public function testEndsWithOp() $this->assertEquals($expected6, $filter6); } + /** + * Test no not starts with operator + */ + public function testNoNotStartsWithOperator() + { + $this->expectException(ParseCancellationException::class); + $this->factory->initFilter("name not starts with 'Mariano'"); + } + + /** + * Test no not ends with operator + */ + public function testNoNotEndsWithOperator() + { + $this->expectException(ParseCancellationException::class); + $this->factory->initFilter("name not ends with 'Mariano'"); + } + /** * Test no starts operator */