diff --git a/README.md b/README.md index fbd48050..34158bc8 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ $matcher->getError(); // returns null or error message * ``inArray($value)`` * ``oneOf(...$expanders)`` - example usage ``"@string@.oneOf(contains('foo'), contains('bar'), contains('baz'))"`` * ``matchRegex($regex)`` - example usage ``"@string@.matchRegex('/^lorem.+/')"`` +* ``match($subPattern)`` - example usage ``"@json@.match({"id": "@integer@"})"`` ##Example usage @@ -97,7 +98,7 @@ $factory = new SimpleFactory(); $matcher = $factory->createMatcher(); $matcher->match(1, 1); -$matcher->match('string', 'string') +$matcher->match('string', 'string'); ``` ### String matching @@ -111,7 +112,7 @@ $factory = new SimpleFactory(); $matcher = $factory->createMatcher(); $matcher->match('Norbert', '@string@'); -$matcher->match("lorem ipsum dolor", "@string@.startsWith('lorem').contains('ipsum').endsWith('dolor')") +$matcher->match("lorem ipsum dolor", "@string@.startsWith('lorem').contains('ipsum').endsWith('dolor')"); ``` @@ -185,12 +186,12 @@ use Coduo\PHPMatcher\Factory\SimpleFactory; $factory = new SimpleFactory(); $matcher = $factory->createMatcher(); -$matcher->match("@integer@", "@*@"), -$matcher->match("foobar", "@*@"), -$matcher->match(true, "@*@"), -$matcher->match(6.66, "@*@"), -$matcher->match(array("bar"), "@wildcard@"), -$matcher->match(new \stdClass, "@wildcard@"), +$matcher->match("@integer@", "@*@"); +$matcher->match("foobar", "@*@"); +$matcher->match(true, "@*@"); +$matcher->match(6.66, "@*@"); +$matcher->match(array("bar"), "@wildcard@"); +$matcher->match(new \stdClass, "@wildcard@"); ``` ### Expression matching @@ -274,7 +275,7 @@ $matcher->match( '@boolean@', '@double@' ) -) +); ``` ### Json matching @@ -307,7 +308,7 @@ $matcher->match( } ] }' -) +); ``` @@ -355,6 +356,52 @@ XML ); ``` +### Nested Object matching + +```php + +$factory = new SimpleFactory(); +$matcher = $factory->createMatcher(); + +$matcher->match(<<buildParser(); $scalarMatchers = $this->buildScalarMatchers(); - $orMatcher = $this->buildOrMatcher(); + $orMatcher = $this->buildOrMatcher($scalarMatchers); + $jsonMatcher = $this->buildJsonObjectMatcher($scalarMatchers); + $arrayMatcher = $this->buildArrayMatcher($scalarMatchers, $orMatcher, $jsonMatcher, $parser); $chainMatcher = new Matcher\ChainMatcher(array( - $scalarMatchers, $orMatcher, - new Matcher\JsonMatcher($orMatcher), - new Matcher\XmlMatcher($orMatcher), - new Matcher\TextMatcher($scalarMatchers, $this->buildParser()) + $jsonMatcher, + $scalarMatchers, + $arrayMatcher, )); - return $chainMatcher; + $decoratedMatcher = new Matcher\ChainMatcher([ + $chainMatcher, + new Matcher\JsonMatcher($chainMatcher), + new Matcher\XmlMatcher($chainMatcher), + new Matcher\TextMatcher($chainMatcher, $parser), + ]); + + return $decoratedMatcher; + } + + protected function buildArrayMatcher( + Matcher\ValueMatcher $scalarMatchers, + Matcher\ValueMatcher $orMatcher, + Matcher\ValueMatcher $jsonMatcher, + Parser $parser + ) + { + return new Matcher\ArrayMatcher(new Matcher\ChainMatcher([ + $orMatcher, + $scalarMatchers, + $jsonMatcher + ]), $parser); } /** - * @return Matcher\ChainMatcher + * @return Matcher\ValueMatcher */ - protected function buildOrMatcher() + protected function buildOrMatcher(Matcher\ChainMatcher $scalarMatchers) { - $scalarMatchers = $this->buildScalarMatchers(); - $orMatcher = new Matcher\OrMatcher($scalarMatchers); - $arrayMatcher = new Matcher\ArrayMatcher( - new Matcher\ChainMatcher(array( - $orMatcher, - $scalarMatchers - )), - $this->buildParser() - ); + $chainMatcher = new Matcher\ChainMatcher([ + $scalarMatchers, + $this->buildJsonObjectMatcher($scalarMatchers) + ]); - $chainMatcher = new Matcher\ChainMatcher(array( - $orMatcher, - $arrayMatcher, - )); + return new Matcher\OrMatcher($chainMatcher); + } - return $chainMatcher; + protected function buildJsonObjectMatcher(Matcher\ValueMatcher $scalarMatchers) + { + $parser = $this->buildParser(); + + return new Matcher\JsonObjectMatcher($scalarMatchers, $parser); } /** @@ -86,6 +108,10 @@ protected function buildScalarMatchers() */ protected function buildParser() { - return new Parser(new Lexer(), new Parser\ExpanderInitializer()); + if ($this->parser) { + return $this->parser; + } + + return $this->parser = new Parser(new Lexer(), new Parser\ExpanderInitializer()); } } diff --git a/src/Matcher/JsonObjectMatcher.php b/src/Matcher/JsonObjectMatcher.php new file mode 100644 index 00000000..4d2ab934 --- /dev/null +++ b/src/Matcher/JsonObjectMatcher.php @@ -0,0 +1,91 @@ +propertyMatcher = $propertyMatcher; + $this->parser = $parser; + } + + /** + * {@inheritDoc} + */ + public function match($value, $pattern) + { + if (!$this->isJsonPattern($pattern)) { + return false; + } + + if (!is_array($value)) { + $this->error = sprintf("%s \"%s\" is not a valid array.", gettype($value), new StringConverter($value)); + return false; + } + + if ($this->isJsonPattern($pattern)) { + return $this->allExpandersMatch($value, $pattern); + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function canMatch($pattern) + { + return is_string($pattern) && $this->isJsonPattern($pattern); + } + + private function isJsonPattern($pattern) + { + if (!is_string($pattern)) { + return false; + } + + return $this->parser->hasValidSyntax($pattern) && $this->parser->parse($pattern)->is('json'); + } + + /** + * @param $value + * @param $pattern + * @return bool + * @throws \Coduo\PHPMatcher\Exception\UnknownExpanderException + */ + private function allExpandersMatch($value, $pattern) + { + $typePattern = $this->parser->parse($pattern); + if (!$typePattern->matchExpanders($value)) { + $this->error = $typePattern->getError(); + return false; + } + + return true; + } +} diff --git a/src/Matcher/OrMatcher.php b/src/Matcher/OrMatcher.php index 5eea2958..5d61c3e0 100644 --- a/src/Matcher/OrMatcher.php +++ b/src/Matcher/OrMatcher.php @@ -25,8 +25,10 @@ public function __construct(ChainMatcher $chainMatcher) public function match($value, $pattern) { $patterns = explode('||', $pattern); + $patterns = array_map('trim', $patterns); + foreach ($patterns as $childPattern) { - if ($this->matchChild($value, $childPattern)){ + if ($this->matchChild($value, $childPattern)) { return true; } } diff --git a/src/Matcher/Pattern/Expander/Match.php b/src/Matcher/Pattern/Expander/Match.php new file mode 100644 index 00000000..38335f1a --- /dev/null +++ b/src/Matcher/Pattern/Expander/Match.php @@ -0,0 +1,60 @@ +value = $value; + } + + /** + * @param $value + * @return boolean + */ + public function match($value) + { + if (!is_array($value)) { + $this->error = sprintf("Match expander require \"array\", got \"%s\".", new StringConverter($value)); + return false; + } + + $match = true; + $matcher = (new SimpleFactory)->createMatcher(); + foreach ($value as $singleRowValue) { + if (!$matcher->match($singleRowValue, $this->value)) { + $match = false; + $this->error = $matcher->getError(); + } + } + unset($matcher); + + return $match; + } + + /** + * @return string|null + */ + public function getError() + { + return $this->error; + } +} diff --git a/src/Parser/ExpanderInitializer.php b/src/Parser/ExpanderInitializer.php index 9f21bcd7..5adfd60a 100644 --- a/src/Parser/ExpanderInitializer.php +++ b/src/Parser/ExpanderInitializer.php @@ -28,7 +28,7 @@ final class ExpanderInitializer "count" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\Count", "contains" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\Contains", "matchRegex" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\MatchRegex", - + "match" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\Match", "oneOf" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\OneOf" ); diff --git a/tests/Matcher/JsonObjectMatcherTest.php b/tests/Matcher/JsonObjectMatcherTest.php new file mode 100644 index 00000000..b5183533 --- /dev/null +++ b/tests/Matcher/JsonObjectMatcherTest.php @@ -0,0 +1,236 @@ +matcher = $factory->createMatcher(); + } + + + /** + * @dataProvider positiveMatchData + */ + public function test_positive_match_arrays($value, $pattern) + { + $this->assertTrue($this->matcher->match($value, $pattern), $this->matcher->getError()); + } + + /** + * @dataProvider negativeMatchData + */ + public function test_negative_match_arrays($value, $pattern) + { + $this->assertFalse($this->matcher->match($value, $pattern)); + } + + public static function positiveMatchData() + { + return array( + [ + [ + [ + 'firstName' => 'Norbert', + 'lastName' => 'Orzechowicz' + ] + ], + '@null@||@json@.match({ + "firstName": "@string@", + "lastName": "@string@" + }) + ' + ], + [ + [[ + 'firstName' => 'Norbert', + 'lastName' => 'Orzechowicz' + ]], + '@json@.match({ + "firstName": "@string@", + "lastName": "@string@" + }) + ' + ], + [ + [ + "id" => 1, + "groups" => [ + ["name" => 'asdas'], + ["name" => 1], + ] + ], + [ + "id" => "@integer@", + "groups" => ' + @json@.match({ + "name": "@string@||@integer@" + }) + ' + ], + ], + [ + [ + "id" => 1, + "gallery" => [ + "id" => 1, + "images" => [['id' => 1], ['id' => 2]], + ] + ], + [ + "id" => "@integer@", + "gallery" => [ + 'id' => '@integer@', + 'images' => ' + @json@.match({ + "id": "@integer@" + }) + ' + ] + ], + ], + [ + [ + "id" => 1, + "galleries" => [[ + "id" => 1, + "cover" => null, + "images" => [['id' => 1], ['id' => 2]], + ]] + ], + [ + "id" => "@integer@", + "galleries" => ' + @json@.match({ + "id": "@integer@", + "cover": \' + @null@||@json@.match({ + "id": "@integer@" + }) + \', + "images": \' + @json@.match({ + "id": "@integer@" + }) + \' + }) + ' + ], + ], + [ + ["photos" => [1, 2, 2, null]], + ["photos" => '@json@.match("@null@||@integer@")'] + ] + ); + } + + public static function negativeMatchData() + { + return array( + [ + [ + [ + 'firstName' => 'Norbert', + 'lastName' => 'Orzechowicz' + ] + ], + '@null@||@json@.match({ + "avatar": "@null@", + "firstName": "@string@", + "lastName": "@string@" + }) + ' + ], + [ + [[ + 'firstName' => 'Norbert', + 'lastName' => 'Orzechowicz' + ]], + '@json@.match({ + "firstName": "@string@", + "lastName": "@string@", + "email": "@string@.isEmail()" + }) + ' + ], + [ + [ + "id" => 1, + "groups" => [ + ["name" => 'asdas'], + ["name" => 1], + ] + ], + [ + "id" => "@integer@", + "groups" => ' + @json@.match("@array@.count(0)") + ' + ], + ], + [ + [ + "id" => 1, + "gallery" => [ + "id" => 1, + "images" => [['id' => 1], ['id' => 2]], + ] + ], + [ + "id" => "@integer@", + "gallery" => [ + 'id' => '@integer@', + 'images' => ' + @json@.match({ + "id": "@integer@", + "url": "@string@.isUrl()" + }) + ' + ] + ], + ], + [ + [ + "id" => 1, + "galleries" => [[ + "id" => 1, + "cover" => null, + "images" => [['id' => 1], ['id' => 2]], + ]] + ], + [ + "id" => "@integer@", + "galleries" => ' + @json@.match({ + "id": "@integer@", + "cover": \' + @json@.match({ + "id": "@integer@" + }) + \', + "images": \' + @json@.match({ + "id": "@integer@" + }) + \' + }) + ' + ], + ], + [ + ["photos" => [1, 2, 2.0, null]], + ["photos" => '@json@.match("@null@||@double@")'] + ] + ); + } +} diff --git a/tests/Matcher/Pattern/Expander/MatchTest.php b/tests/Matcher/Pattern/Expander/MatchTest.php new file mode 100644 index 00000000..bb65f4e7 --- /dev/null +++ b/tests/Matcher/Pattern/Expander/MatchTest.php @@ -0,0 +1,44 @@ +assertEquals($expectedResult, $expander->match($haystack)); + } + + public static function examplesProvider() + { + return array( + array("ipsum", array("ipsum"), true), + array(1, array(1, 1, 1), true), + array(array("foo" => "bar"), array(array("foo" => "bar")), true), + ); + } + + /** + * @dataProvider invalidCasesProvider + */ + public function test_error_when_matching_fail($boundary, $value, $errorMessage) + { + $expander = new Match($boundary); + $this->assertFalse($expander->match($value)); + $this->assertEquals($errorMessage, $expander->getError()); + } + + public static function invalidCasesProvider() + { + return array( + array("ipsum", array("ipsum lorem"), "\"ipsum lorem\" does not match \"ipsum\" pattern"), + array("lorem", new \DateTime(), "Match expander require \"array\", got \"\DateTime\"."), + ); + } +}