diff --git a/src/Filter.php b/src/Filter.php index 402400c..17f7c28 100644 --- a/src/Filter.php +++ b/src/Filter.php @@ -13,9 +13,11 @@ use ipl\Stdlib\Filter\GreaterThanOrEqual; use ipl\Stdlib\Filter\LessThan; use ipl\Stdlib\Filter\LessThanOrEqual; +use ipl\Stdlib\Filter\Similar; use ipl\Stdlib\Filter\None; use ipl\Stdlib\Filter\Rule; use ipl\Stdlib\Filter\Unequal; +use ipl\Stdlib\Filter\Unlike; class Filter { @@ -146,8 +148,6 @@ protected function matchNone(None $rules, $row) /** * Create a rule that matches rows with a column that **equals** the given value * - * Performs a wildcard search if the value contains asterisks. - * * @param string $column * @param array|bool|float|int|string $value * @@ -194,6 +194,57 @@ protected function matchEqual($rule, $row) return false; } + /** + * Create a rule that matches rows with a column that is **similar** to the given value + * + * Performs a wildcard search if the value contains asterisks. + * + * @param string $column + * @param string|string[] $value + * + * @return Condition + */ + public static function similar($column, $value) + { + return new Similar($column, $value); + } + + /** + * Return whether the given rule's value is similar to the given item's value + * + * @param Similar|Unlike $rule + * @param object $row + * + * @return bool + */ + protected function matchSimilar($rule, $row) + { + if (! $rule instanceof Similar && ! $rule instanceof Unlike) { + throw new InvalidArgumentException(sprintf( + 'Rule must be of type %s or %s, got %s instead', + Similar::class, + Unlike::class, + get_php_type($rule) + )); + } + + $rowValue = $this->extractValue($rule->getColumn(), $row); + $value = $rule->getValue(); + $this->normalizeTypes($rowValue, $value); + + if (! is_array($rowValue)) { + $rowValue = [$rowValue]; + } + + foreach ($rowValue as $rowVal) { + if ($this->performSimilarityMatch($value, $rowVal, $rule->ignoresCase())) { + return true; + } + } + + return false; + } + /** * Apply equality matching rules on the given row value * @@ -217,11 +268,34 @@ protected function performEqualityMatch($value, $rowValue, $ignoreCase = false) } elseif (! is_string($value)) { if (is_string($rowValue)) { $value = (string) $value; - } else { - return $rowValue === $value; } } + return $rowValue === $value; + } + + /** + * Apply similarity matching rules on the given row value + * + * @param string|string[] $value + * @param string $rowValue + * @param bool $ignoreCase + * + * @return bool + */ + protected function performSimilarityMatch($value, $rowValue, $ignoreCase = false) + { + if ($ignoreCase) { + $rowValue = strtolower($rowValue); + $value = is_array($value) + ? array_map('strtolower', $value) + : strtolower($value); + } + + if (is_array($value)) { + return in_array($rowValue, $value, true); + } + $wildcardSubSegments = preg_split('~\*~', $value); if (count($wildcardSubSegments) === 1) { return $rowValue === $value; @@ -240,8 +314,6 @@ protected function performEqualityMatch($value, $rowValue, $ignoreCase = false) /** * Create a rule that matches rows with a column that is **unequal** with the given value * - * Performs a wildcard search if the value contains asterisks. - * * @param string $column * @param array|bool|float|int|string $value * @@ -265,6 +337,34 @@ protected function matchUnequal(Unequal $rule, $row) return ! $this->matchEqual($rule, $row); } + /** + * Create a rule that matches rows with a column that is **unlike** with the given value + * + * Performs a wildcard search if the value contains asterisks. + * + * @param string $column + * @param string|string[] $value + * + * @return Condition + */ + public static function unlike($column, $value) + { + return new Unlike($column, $value); + } + + /** + * Return whether the given rule's value is unlike the given item's value + * + * @param Unlike $rule + * @param object $row + * + * @return bool + */ + protected function matchUnlike(Unlike $rule, $row) + { + return ! $this->matchSimilar($rule, $row); + } + /** * Create a rule that matches rows with a column that is **greater** than the given value * @@ -394,6 +494,8 @@ protected function performMatch(Rule $rule, $row) return $this->matchAll($rule, $row); case $rule instanceof Any: return $this->matchAny($rule, $row); + case $rule instanceof Similar: + return $this->matchSimilar($rule, $row); case $rule instanceof Equal: return $this->matchEqual($rule, $row); case $rule instanceof GreaterThan: @@ -408,6 +510,8 @@ protected function performMatch(Rule $rule, $row) return $this->matchNone($rule, $row); case $rule instanceof Unequal: return $this->matchUnequal($rule, $row); + case $rule instanceof Unlike: + return $this->matchUnlike($rule, $row); default: throw new InvalidArgumentException(sprintf( 'Unable to match filter. Rule type %s is unknown', diff --git a/src/Filter/Similar.php b/src/Filter/Similar.php new file mode 100644 index 0000000..2be2914 --- /dev/null +++ b/src/Filter/Similar.php @@ -0,0 +1,31 @@ +ignoreCase = true; + + return $this; + } + + /** + * Return whether this rule ignores case + * + * @return bool + */ + public function ignoresCase() + { + return $this->ignoreCase; + } +} diff --git a/src/Filter/Unlike.php b/src/Filter/Unlike.php new file mode 100644 index 0000000..16b9fb3 --- /dev/null +++ b/src/Filter/Unlike.php @@ -0,0 +1,31 @@ +ignoreCase = true; + + return $this; + } + + /** + * Return whether this rule ignores case + * + * @return bool + */ + public function ignoresCase() + { + return $this->ignoreCase; + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php index b26b3fc..ffe0c7c 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -123,14 +123,39 @@ public function testEqualIgnoresCase() $this->assertTrue(Filter::match($equal, $this->row(0))); } + public function testSimilarMatches() + { + $similar = Filter::similar('problem', '1'); + + $this->assertTrue(Filter::match($similar, $this->row(0))); + } + + public function testSimilarMismatches() + { + $similar = Filter::similar('handled', '1'); + + $this->assertFalse(Filter::match($similar, $this->row(1))); + } + + public function testSimilarIgnoresCase() + { + // single string + $similar = Filter::similar('host', '*LOCAL*') + ->ignoreCase(); + + $this->assertTrue(Filter::match($similar, $this->row(0))); + + // string array + $similar->setValue(['LoCaLhOsT', '127.0.0.1']); + + $this->assertTrue(Filter::match($similar, $this->row(0))); + } + public function testEqualMatchesMultiValuedColumns() { $this->assertTrue(Filter::match(Filter::equal('foo', 'bar'), [ 'foo' => ['foo', 'bar'] ])); - $this->assertTrue(Filter::match(Filter::equal('foo', 'ba*'), [ - 'foo' => ['foo', 'bar'] - ])); $this->assertTrue(Filter::match(Filter::equal('foo', 'BAR')->ignoreCase(), [ 'foo' => ['FoO', 'bAr'] ])); @@ -139,6 +164,22 @@ public function testEqualMatchesMultiValuedColumns() ])); } + public function testSimilarMatchesMultiValuedColumns() + { + $this->assertTrue(Filter::match(Filter::similar('foo', 'bar'), [ + 'foo' => ['foo', 'bar'] + ])); + $this->assertTrue(Filter::match(Filter::similar('foo', 'ba*'), [ + 'foo' => ['foo', 'bar'] + ])); + $this->assertTrue(Filter::match(Filter::similar('foo', 'BAR')->ignoreCase(), [ + 'foo' => ['FoO', 'bAr'] + ])); + $this->assertTrue(Filter::match(Filter::similar('foo', ['bar', 'boar']), [ + 'foo' => ['foo', 'bar'] + ])); + } + public function testUnequalMatches() { $unequal = Filter::unequal('problem', '0'); @@ -167,14 +208,39 @@ public function testUnequalIgnoresCase() $this->assertFalse(Filter::match($equal, $this->row(0))); } + public function testUnlikeMatches() + { + $unlike = Filter::unlike('problem', '0'); + + $this->assertTrue(Filter::match($unlike, $this->row(1))); + } + + public function testUnlikeMismatches() + { + $unlike = Filter::unlike('problem', '1'); + + $this->assertFalse(Filter::match($unlike, $this->row(1))); + } + + public function testUnlikeIgnoresCase() + { + // single string + $similar = Filter::unlike('host', '*LOCAL*') + ->ignoreCase(); + + $this->assertFalse(Filter::match($similar, $this->row(0))); + + // string array + $similar->setValue(['LoCaLhOsT', '127.0.0.1']); + + $this->assertFalse(Filter::match($similar, $this->row(0))); + } + public function testUnequalMatchesMultiValuedColumns() { $this->assertFalse(Filter::match(Filter::unequal('foo', 'bar'), [ 'foo' => ['foo', 'bar'] ])); - $this->assertFalse(Filter::match(Filter::unequal('foo', 'ba*'), [ - 'foo' => ['foo', 'bar'] - ])); $this->assertFalse(Filter::match(Filter::unequal('foo', 'BAR')->ignoreCase(), [ 'foo' => ['FoO', 'bAr'] ])); @@ -183,6 +249,22 @@ public function testUnequalMatchesMultiValuedColumns() ])); } + public function testUnlikeMatchesMultiValuedColumns() + { + $this->assertFalse(Filter::match(Filter::unlike('foo', 'bar'), [ + 'foo' => ['foo', 'bar'] + ])); + $this->assertFalse(Filter::match(Filter::unlike('foo', 'ba*'), [ + 'foo' => ['foo', 'bar'] + ])); + $this->assertFalse(Filter::match(Filter::unlike('foo', 'BAR')->ignoreCase(), [ + 'foo' => ['FoO', 'bAr'] + ])); + $this->assertFalse(Filter::match(Filter::unlike('foo', ['bar', 'boar']), [ + 'foo' => ['foo', 'bar'] + ])); + } + public function testGreaterThanMatches() { $greaterThan = Filter::greaterThan('state', 1); @@ -243,32 +325,46 @@ public function testLessThanOrEqualMismatches() $this->assertFalse(Filter::match($lessThanOrEqual, $this->row(2))); } - public function testEqualWithWildcardMatches() + public function testEqualWithWildcardMismatches() { $equal = Filter::equal('service', '*icinga*'); - $this->assertTrue(Filter::match($equal, $this->row(1))); + $this->assertFalse(Filter::match($equal, $this->row(1))); } - public function testEqualWithWildcardMismatches() + public function testSimilarWithWildcardMatches() { - $equal = Filter::equal('service', '*nagios*'); + $similar = Filter::similar('service', '*icinga*'); - $this->assertFalse(Filter::match($equal, $this->row(1))); + $this->assertTrue(Filter::match($similar, $this->row(1))); + } + + public function testSimilarWithWildcardMismatches() + { + $similar = Filter::similar('service', '*nagios*'); + + $this->assertFalse(Filter::match($similar, $this->row(1))); } public function testUnequalWithWildcardMatches() { - $unequal = Filter::unequal('service', '*nagios*'); + $unequal = Filter::unequal('service', '*icinga*'); $this->assertTrue(Filter::match($unequal, $this->row(1))); } - public function testUnequalWithWildcardMismatches() + public function testUnlikeWithWildcardMatches() { - $unequal = Filter::unequal('service', '*icinga*'); + $unlike = Filter::unlike('service', '*nagios*'); - $this->assertFalse(Filter::match($unequal, $this->row(1))); + $this->assertTrue(Filter::match($unlike, $this->row(1))); + } + + public function testUnlikeWithWildcardMismatches() + { + $unlike = Filter::unlike('service', '*icinga*'); + + $this->assertFalse(Filter::match($unlike, $this->row(1))); } public function testEqualWithArrayMatches() @@ -285,6 +381,20 @@ public function testEqualWithArrayMismatches() $this->assertFalse(Filter::match($equal, $this->row(0))); } + public function testSimilarWithArrayMatches() + { + $equal = Filter::similar('host', ['127.0.0.1', 'localhost']); + + $this->assertTrue(Filter::match($equal, $this->row(0))); + } + + public function testSimilarWithArrayMismatches() + { + $equal = Filter::similar('host', ['10.0.10.20', '10.0.10.21']); + + $this->assertFalse(Filter::match($equal, $this->row(0))); + } + public function testUnequalWithArrayMatches() { $unequal = Filter::unequal('host', ['10.0.20.10', '10.0.20.11']); @@ -292,6 +402,13 @@ public function testUnequalWithArrayMatches() $this->assertTrue(Filter::match($unequal, $this->row(0))); } + public function testUnlikeWithArrayMatches() + { + $unlike = Filter::unlike('host', ['10.0.20.10', '10.0.20.11']); + + $this->assertTrue(Filter::match($unlike, $this->row(0))); + } + public function testUnequalWithArrayMismatches() { $unequal = Filter::unequal('host', ['127.0.0.1', 'localhost']); @@ -299,6 +416,13 @@ public function testUnequalWithArrayMismatches() $this->assertFalse(Filter::match($unequal, $this->row(0))); } + public function testUnlikeWithArrayMismatches() + { + $unlike = Filter::unlike('host', ['127.0.0.1', 'localhost']); + + $this->assertFalse(Filter::match($unlike, $this->row(0))); + } + public function testConditionsAreValueTypeAgnostic() { $this->assertTrue( @@ -422,7 +546,9 @@ public function testChainsCanBeEmpty() public function testConditionsHandleMissingColumnsProperly() { $this->assertFalse(Filter::match(Filter::equal('foo', 'bar'), [])); + $this->assertFalse(Filter::match(Filter::similar('foo', 'bar'), [])); $this->assertTrue(Filter::match(Filter::unequal('bar', 'foo'), [])); + $this->assertTrue(Filter::match(Filter::unlike('bar', 'foo'), [])); $this->assertFalse(Filter::match(Filter::greaterThan('foo', 123), [])); $this->assertFalse(Filter::match(Filter::lessThan('foo', 123), [])); $this->assertFalse(Filter::match(Filter::lessThanOrEqual('foo', 123), []));