diff --git a/.travis.yml b/.travis.yml index 0f0d8b32..b4dde454 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,4 @@ before_script: - curl -s http://getcomposer.org/installer | php - php composer.phar --dev install -script: phpunit --coverage-text \ No newline at end of file +script: bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index 0e82be47..986b8163 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ ], "require": { "php": ">=5.3.0", + "coduo/php-to-string": "1.0.*@dev", "symfony/property-access": "~2.3", "symfony/expression-language": "~2.4" }, diff --git a/src/Coduo/PHPMatcher/Matcher.php b/src/Coduo/PHPMatcher/Matcher.php index 9fca62b7..0357e774 100644 --- a/src/Coduo/PHPMatcher/Matcher.php +++ b/src/Coduo/PHPMatcher/Matcher.php @@ -3,25 +3,36 @@ use Coduo\PHPMatcher\Matcher\PropertyMatcher; -class Matcher implements PropertyMatcher +class Matcher { /** * @var Matcher\PropertyMatcher */ private $matcher; + /** + * @param PropertyMatcher $matcher + */ public function __construct(PropertyMatcher $matcher) { $this->matcher = $matcher; } + /** + * @param mixed $value + * @param mixed $pattern + * @return bool + */ public function match($value, $pattern) { return $this->matcher->match($value, $pattern); } - public function canMatch($pattern) + /** + * @return null|string + */ + public function getError() { - return true; + return $this->matcher->getError(); } } diff --git a/src/Coduo/PHPMatcher/Matcher/ArrayMatcher.php b/src/Coduo/PHPMatcher/Matcher/ArrayMatcher.php index f9944193..744bc38e 100644 --- a/src/Coduo/PHPMatcher/Matcher/ArrayMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/ArrayMatcher.php @@ -6,15 +6,13 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\PropertyAccessor; -class ArrayMatcher implements PropertyMatcher +class ArrayMatcher extends Matcher { /** * @var PropertyMatcher */ private $propertyMatcher; - private $paths; - /** * @var PropertyAccessor */ @@ -60,6 +58,7 @@ private function iterateMatch(array $value, array $pattern) $path = sprintf("[%s]", $key); if (!$this->hasValue($pattern, $path)) { + $this->error = sprintf('There is no element under path %s in pattern array.', $path); return false; } $elementPattern = $this->getValue($pattern, $path); @@ -70,6 +69,7 @@ private function iterateMatch(array $value, array $pattern) } if (!is_array($element)) { + $this->error = $this->propertyMatcher->getError(); return false; } diff --git a/src/Coduo/PHPMatcher/Matcher/CaptureMatcher.php b/src/Coduo/PHPMatcher/Matcher/CaptureMatcher.php index bf1b2a39..9bc7107b 100644 --- a/src/Coduo/PHPMatcher/Matcher/CaptureMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/CaptureMatcher.php @@ -2,7 +2,7 @@ namespace Coduo\PHPMatcher\Matcher; -class CaptureMatcher implements PropertyMatcher, \ArrayAccess +class CaptureMatcher extends Matcher implements \ArrayAccess { const MATCH_PATTERN = "/^:.*:$/"; @@ -39,6 +39,7 @@ public function offsetSet($offset, $value) $this->captures[$offset] = $value; } } + public function offsetExists($offset) { return isset($this->captures[$offset]); diff --git a/src/Coduo/PHPMatcher/Matcher/ChainMatcher.php b/src/Coduo/PHPMatcher/Matcher/ChainMatcher.php index fd4d1cd1..27481267 100644 --- a/src/Coduo/PHPMatcher/Matcher/ChainMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/ChainMatcher.php @@ -2,15 +2,26 @@ namespace Coduo\PHPMatcher\Matcher; -class ChainMatcher implements PropertyMatcher +use Coduo\ToString\String; + +class ChainMatcher extends Matcher { + /** + * @var array|PropertyMatcher[] + */ private $matchers; + /** + * @param array|PropertyMatcher[] $matchers + */ public function __construct(array $matchers = array()) { $this->matchers = $matchers; } + /** + * @param PropertyMatcher $matcher + */ public function addMatcher(PropertyMatcher $matcher) { $this->matchers[] = $matcher; @@ -26,9 +37,19 @@ public function match($value, $pattern) if (true === $propertyMatcher->match($value, $pattern)) { return true; } + + $this->error = $propertyMatcher->getError(); } } + if (!isset($this->error)) { + $this->error = sprintf( + 'Any matcher from chain can\'t match value "%s" to pattern "%s"', + new String($value), + new String($pattern) + ); + } + return false; } @@ -39,5 +60,4 @@ public function canMatch($pattern) { return true; } - } diff --git a/src/Coduo/PHPMatcher/Matcher/ExpressionMatcher.php b/src/Coduo/PHPMatcher/Matcher/ExpressionMatcher.php index 169e2893..380f0e74 100644 --- a/src/Coduo/PHPMatcher/Matcher/ExpressionMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/ExpressionMatcher.php @@ -2,9 +2,10 @@ namespace Coduo\PHPMatcher\Matcher; +use Coduo\ToString\String; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -class ExpressionMatcher implements PropertyMatcher +class ExpressionMatcher extends Matcher { const MATCH_PATTERN = "/^expr\((.*?)\)$/"; @@ -15,8 +16,13 @@ public function match($value, $pattern) { $language = new ExpressionLanguage(); preg_match(self::MATCH_PATTERN, $pattern, $matches); + $expressionResult = $language->evaluate($matches[1], array('value' => $value)); - return $language->evaluate($matches[1], array('value' => $value)); + if (!$expressionResult) { + $this->error = sprintf("\"%s\" expression fails for value \"%s\".", $pattern, new String($value)); + } + + return $expressionResult; } /** diff --git a/src/Coduo/PHPMatcher/Matcher/JsonMatcher.php b/src/Coduo/PHPMatcher/Matcher/JsonMatcher.php index e933d0db..6f67e64d 100644 --- a/src/Coduo/PHPMatcher/Matcher/JsonMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/JsonMatcher.php @@ -2,7 +2,7 @@ namespace Coduo\PHPMatcher\Matcher; -class JsonMatcher implements PropertyMatcher +class JsonMatcher extends Matcher { const TRANSFORM_QUOTATION_PATTERN = '/([^"])@(integer|string|array|double|wildcard|boolean)@([^"])/'; const TRANSFORM_QUOTATION_REPLACEMENT = '$1"@$2@"$3'; @@ -30,8 +30,13 @@ public function match($value, $pattern) } $pattern = $this->transformPattern($pattern); + $match = $this->matcher->match(json_decode($value, true), json_decode($pattern, true)); + if (!$match) { + $this->error = $this->matcher->getError(); + return false; + } - return $this->matcher->match(json_decode($value, true), json_decode($pattern, true)); + return true; } /** diff --git a/src/Coduo/PHPMatcher/Matcher/Matcher.php b/src/Coduo/PHPMatcher/Matcher/Matcher.php new file mode 100644 index 00000000..736da261 --- /dev/null +++ b/src/Coduo/PHPMatcher/Matcher/Matcher.php @@ -0,0 +1,19 @@ +error; + } +} diff --git a/src/Coduo/PHPMatcher/Matcher/PropertyMatcher.php b/src/Coduo/PHPMatcher/Matcher/PropertyMatcher.php index 7d37d483..f9ce5ff3 100644 --- a/src/Coduo/PHPMatcher/Matcher/PropertyMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/PropertyMatcher.php @@ -21,4 +21,10 @@ public function match($value, $pattern); */ public function canMatch($pattern); + /** + * Returns a string description why matching failed + * + * @return null|string + */ + public function getError(); } diff --git a/src/Coduo/PHPMatcher/Matcher/ScalarMatcher.php b/src/Coduo/PHPMatcher/Matcher/ScalarMatcher.php index 21d4df83..9e34b2da 100644 --- a/src/Coduo/PHPMatcher/Matcher/ScalarMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/ScalarMatcher.php @@ -2,14 +2,21 @@ namespace Coduo\PHPMatcher\Matcher; -class ScalarMatcher implements PropertyMatcher +use Coduo\ToString\String; + +class ScalarMatcher extends Matcher { /** * {@inheritDoc} */ public function match($value, $pattern) { - return $value === $pattern; + if ($value !== $pattern) { + $this->error = sprintf("\"%s\" does not match \"%s\".", new String($value), new String($pattern)); + return false; + } + + return true; } /** diff --git a/src/Coduo/PHPMatcher/Matcher/TypeMatcher.php b/src/Coduo/PHPMatcher/Matcher/TypeMatcher.php index 12c165ad..1e537f7c 100644 --- a/src/Coduo/PHPMatcher/Matcher/TypeMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/TypeMatcher.php @@ -2,7 +2,9 @@ namespace Coduo\PHPMatcher\Matcher; -class TypeMatcher implements PropertyMatcher +use Coduo\ToString\String; + +class TypeMatcher extends Matcher { const MATCH_PATTERN = "/^@(string|integer|boolean|double|array)@$/"; @@ -11,7 +13,12 @@ class TypeMatcher implements PropertyMatcher */ public function match($value, $pattern) { - return gettype($value) === $this->extractType($pattern); + if (gettype($value) !== $this->extractType($pattern)) { + $this->error = sprintf("%s \"%s\" does not match %s pattern.", gettype($value), new String($value), $pattern); + return false; + } + + return true; } /** @@ -22,6 +29,7 @@ public function canMatch($pattern) return is_string($pattern) && 0 !== preg_match(self::MATCH_PATTERN, $pattern); } + private function extractType($pattern) { return str_replace("@", "", $pattern); diff --git a/src/Coduo/PHPMatcher/Matcher/WildcardMatcher.php b/src/Coduo/PHPMatcher/Matcher/WildcardMatcher.php index f8052ec2..dd52c922 100644 --- a/src/Coduo/PHPMatcher/Matcher/WildcardMatcher.php +++ b/src/Coduo/PHPMatcher/Matcher/WildcardMatcher.php @@ -2,7 +2,7 @@ namespace Coduo\PHPMatcher\Matcher; -class WildcardMatcher implements PropertyMatcher +class WildcardMatcher extends Matcher { const MATCH_PATTERN = "/^@(\*|wildcard)@$/"; @@ -21,5 +21,4 @@ public function canMatch($pattern) { return is_string($pattern) && 0 !== preg_match(self::MATCH_PATTERN, $pattern); } - } diff --git a/tests/Coduo/PHPMatcher/Matcher/ArrayMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/ArrayMatcherTest.php index c9d741d2..4074baa3 100644 --- a/tests/Coduo/PHPMatcher/Matcher/ArrayMatcherTest.php +++ b/tests/Coduo/PHPMatcher/Matcher/ArrayMatcherTest.php @@ -50,6 +50,18 @@ public function test_negative_match_when_cant_find_matcher_that_can_match_array_ $this->assertFalse($matcher->match(array('test' => 1), array('test' => 1))); } + public function test_error_when_path_does_not_exist() + { + $this->assertFalse($this->matcher->match(array('foo' => 'foo value'), array('bar' => 'bar value'))); + $this->assertEquals($this->matcher->getError(), 'There is no element under path [foo] in pattern array.'); + } + + public function test_error_when_matching_fail() + { + $this->assertFalse($this->matcher->match(array('foo' => 'foo value'), array('foo' => 'bar value'))); + $this->assertEquals($this->matcher->getError(), '"foo value" does not match "bar value".'); + } + public static function positiveMatchData() { $simpleArr = array( diff --git a/tests/Coduo/PHPMatcher/Matcher/ChainMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/ChainMatcherTest.php new file mode 100644 index 00000000..5b1c418d --- /dev/null +++ b/tests/Coduo/PHPMatcher/Matcher/ChainMatcherTest.php @@ -0,0 +1,89 @@ +firstMatcher = $this->getMock('Coduo\PHPMatcher\Matcher\PropertyMatcher'); + $this->secondMatcher = $this->getMock('Coduo\PHPMatcher\Matcher\PropertyMatcher'); + + $this->matcher = new ChainMatcher(array( + $this->firstMatcher, + $this->secondMatcher + )); + } + + public function test_only_one_matcher_can_match_but_none_matchers_match() + { + $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(false)); + $this->firstMatcher->expects($this->never())->method('match'); + $this->secondMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); + $this->secondMatcher->expects($this->once())->method('match')->will($this->returnValue(false)); + + $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), false); + } + + public function test_none_matchers_can_match() + { + $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(false)); + $this->firstMatcher->expects($this->never())->method('match'); + $this->secondMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(false)); + $this->secondMatcher->expects($this->never())->method('match'); + + $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), false); + } + + public function test_first_matcher_match() + { + $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); + $this->firstMatcher->expects($this->once())->method('match')->will($this->returnValue(true)); + $this->secondMatcher->expects($this->never())->method('canMatch'); + $this->secondMatcher->expects($this->never())->method('match'); + + $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), true); + } + + public function test_if_there_is_error_description_only_from_last_matcher_that_fails() + { + $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); + $this->firstMatcher->expects($this->once())->method('match')->will($this->returnValue(false)); + $this->firstMatcher->expects($this->once())->method('getError') + ->will($this->returnValue('First matcher error')); + + $this->secondMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(true)); + $this->secondMatcher->expects($this->once())->method('match')->will($this->returnValue(false)); + $this->secondMatcher->expects($this->once())->method('getError') + ->will($this->returnValue('Second matcher error')); + + $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), false); + $this->assertEquals($this->matcher->getError(), 'Second matcher error'); + } + + public function test_error_description_when_any_matcher_can_match() + { + $this->firstMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(false)); + $this->secondMatcher->expects($this->once())->method('canMatch')->will($this->returnValue(false)); + + $this->assertEquals($this->matcher->match('foo', 'foo_pattern'), false); + $this->assertEquals($this->matcher->getError(), 'Any matcher from chain can\'t match value "foo" to pattern "foo_pattern"'); + } +} diff --git a/tests/Coduo/PHPMatcher/Matcher/ExpressionMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/ExpressionMatcherTest.php index 46584e57..573285a8 100644 --- a/tests/Coduo/PHPMatcher/Matcher/ExpressionMatcherTest.php +++ b/tests/Coduo/PHPMatcher/Matcher/ExpressionMatcherTest.php @@ -41,6 +41,16 @@ public function test_negative_match($value, $pattern) $this->assertFalse($matcher->match($value, $pattern)); } + /** + * @dataProvider negativeMatchDescription + */ + public function test_negative_match_description($value, $pattern, $error) + { + $matcher = new ExpressionMatcher(); + $matcher->match($value, $pattern); + $this->assertEquals($error, $matcher->getError()); + } + public static function positiveCanMatchData() { return array( @@ -76,4 +86,16 @@ public static function negativeMatchData() array("foo", "expr(value != 'foo')"), ); } + + public static function negativeMatchDescription() + { + return array( + array(4, "expr(value < 2)", "\"expr(value < 2)\" expression fails for value \"4\"."), + array( + new \DateTime('2014-04-01'), + "expr(value.format('Y-m-d') == '2014-04-02')", + "\"expr(value.format('Y-m-d') == '2014-04-02')\" expression fails for value \"\\DateTime\"." + ), + ); + } } diff --git a/tests/Coduo/PHPMatcher/Matcher/JsonMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/JsonMatcherTest.php index 18b3133e..bbd92151 100644 --- a/tests/Coduo/PHPMatcher/Matcher/JsonMatcherTest.php +++ b/tests/Coduo/PHPMatcher/Matcher/JsonMatcherTest.php @@ -60,6 +60,25 @@ public function test_negative_matches($value, $pattern) $this->assertFalse($this->matcher->match($value, $pattern)); } + public function test_error_when_matching_fail() + { + $value = json_encode(array( + 'users' => array( + array('name' => 'Norbert'), + array('name' => 'Michał') + ) + )); + $pattern = json_encode(array( + 'users' => array( + array('name' => '@string@'), + array('name' => '@boolean@') + ) + )); + + $this->assertFalse($this->matcher->match($value, $pattern)); + $this->assertEquals($this->matcher->getError(), '"Michał" does not match "@boolean@".'); + } + public static function positivePatterns() { return array( diff --git a/tests/Coduo/PHPMatcher/Matcher/ScalarMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/ScalarMatcherTest.php index af405b37..a4de1836 100644 --- a/tests/Coduo/PHPMatcher/Matcher/ScalarMatcherTest.php +++ b/tests/Coduo/PHPMatcher/Matcher/ScalarMatcherTest.php @@ -41,6 +41,16 @@ public function test_negative_matches($value, $pattern) $this->assertFalse($matcher->match($value, $pattern)); } + /** + * @dataProvider negativeMatchDescription + */ + public function test_negative_match_description($value, $pattern, $error) + { + $matcher = new ScalarMatcher(); + $matcher->match($value, $pattern); + $this->assertEquals($error, $matcher->getError()); + } + public static function negativeMatches() { return array( @@ -79,4 +89,14 @@ public static function negativeCanMatches() array(array()) ); } + + public static function negativeMatchDescription() + { + return array( + array("test", "norbert", "\"test\" does not match \"norbert\"."), + array(new \stdClass, 1, "\"\\stdClass\" does not match \"1\"."), + array(1.1, false, "\"1.1\" does not match \"false\"."), + array(false, array('foo', 'bar'), "\"false\" does not match \"Array(2)\"."), + ); + } } diff --git a/tests/Coduo/PHPMatcher/Matcher/TypeMatcherTest.php b/tests/Coduo/PHPMatcher/Matcher/TypeMatcherTest.php index 2426c78f..00f5c98d 100644 --- a/tests/Coduo/PHPMatcher/Matcher/TypeMatcherTest.php +++ b/tests/Coduo/PHPMatcher/Matcher/TypeMatcherTest.php @@ -41,6 +41,16 @@ public function test_negative_match($value, $pattern) $this->assertFalse($matcher->match($value, $pattern)); } + /** + * @dataProvider negativeMatchDescription + */ + public function test_negative_match_description($value, $pattern, $error) + { + $matcher = new TypeMatcher(); + $matcher->match($value, $pattern); + $this->assertEquals($error, $matcher->getError()); + } + public static function positiveCanMatchData() { return array( @@ -85,4 +95,15 @@ public static function negativeMatchData() array(1, "@array@") ); } + + public static function negativeMatchDescription() + { + return array( + array("test", "@boolean@", "string \"test\" does not match @boolean@ pattern."), + array(new \stdClass, "@string@", "object \"\\stdClass\" does not match @string@ pattern."), + array(1.1, "@integer@", "double \"1.1\" does not match @integer@ pattern."), + array(false, "@double@", "boolean \"false\" does not match @double@ pattern."), + array(1, "@array@", "integer \"1\" does not match @array@ pattern.") + ); + } }