diff --git a/README.md b/README.md index fbd48050..15b90232 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.+/')"`` +* ``optional()`` - work's only with ``ArrayMatcher``, ``JsonMatcher`` and ``XmlMatcher`` ##Example usage @@ -237,7 +238,8 @@ $matcher->match( 'id' => 1, 'firstName' => 'Norbert', 'lastName' => 'Orzechowicz', - 'roles' => array('ROLE_USER') + 'roles' => array('ROLE_USER'), + 'position' => 'Developer', ), array( 'id' => 2, @@ -261,7 +263,8 @@ $matcher->match( 'id' => '@integer@.greaterThan(0)', 'firstName' => '@string@', 'lastName' => 'Orzechowicz', - 'roles' => '@array@' + 'roles' => '@array@', + 'position' => '@string@.optional()' ), array( 'id' => '@integer@', @@ -303,7 +306,8 @@ $matcher->match( "firstName": @string@, "lastName": @string@, "created": "@string@.isDateTime()", - "roles": @array@ + "roles": @array@, + "posiiton": "@string@.optional()" } ] }' @@ -347,6 +351,7 @@ XML @string@ @string@ + @integer@.optional() diff --git a/src/Matcher/ArrayMatcher.php b/src/Matcher/ArrayMatcher.php index f1de7f1d..bd86f1a8 100644 --- a/src/Matcher/ArrayMatcher.php +++ b/src/Matcher/ArrayMatcher.php @@ -2,6 +2,7 @@ namespace Coduo\PHPMatcher\Matcher; +use Coduo\PHPMatcher\Exception\Exception; use Coduo\PHPMatcher\Parser; use Coduo\ToString\StringConverter; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -153,12 +154,12 @@ function ($item) use ($skipPattern) { } ); - $notExistingKeys = array_diff_key($pattern, $values); - + $notExistingKeys = $this->findNotExistingKeys($pattern, $values); if (count($notExistingKeys) > 0) { $keyNames = array_keys($notExistingKeys); $path = $this->formatFullPath($parentPath, $this->formatAccessPath($keyNames[0])); $this->setMissingElementInError('value', $path); + return false; } } @@ -166,6 +167,33 @@ function ($item) use ($skipPattern) { return true; } + /** + * Finds not existing keys + * Excludes keys with pattern which includes Optional Expander + * + * @param $pattern + * @param $values + * @return array + */ + private function findNotExistingKeys($pattern, $values) + { + $notExistingKeys = array_diff_key($pattern, $values); + + return array_filter($notExistingKeys, function ($pattern) use ($values) { + if (is_array($pattern)) { + return !$this->match($values, $pattern); + } + + try { + $typePattern = $this->parser->parse($pattern); + } catch (Exception $e) { + return true; + } + + return !$typePattern->hasExpander('optional'); + }); + } + /** * @param $value * @param $pattern diff --git a/src/Matcher/Pattern/Expander/Contains.php b/src/Matcher/Pattern/Expander/Contains.php index 2fb61173..ebc5927b 100644 --- a/src/Matcher/Pattern/Expander/Contains.php +++ b/src/Matcher/Pattern/Expander/Contains.php @@ -7,6 +7,8 @@ final class Contains implements PatternExpander { + const NAME = 'contains'; + /** * @var null|string */ @@ -22,6 +24,14 @@ final class Contains implements PatternExpander */ private $ignoreCase; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $string * @param bool $ignoreCase diff --git a/src/Matcher/Pattern/Expander/Count.php b/src/Matcher/Pattern/Expander/Count.php index 1698a417..6f14da2f 100644 --- a/src/Matcher/Pattern/Expander/Count.php +++ b/src/Matcher/Pattern/Expander/Count.php @@ -7,6 +7,8 @@ final class Count implements PatternExpander { + const NAME = 'count'; + /** * @var null|string */ @@ -17,6 +19,14 @@ final class Count implements PatternExpander */ private $value; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $value */ diff --git a/src/Matcher/Pattern/Expander/EndsWith.php b/src/Matcher/Pattern/Expander/EndsWith.php index 36a8682a..7510a1e3 100644 --- a/src/Matcher/Pattern/Expander/EndsWith.php +++ b/src/Matcher/Pattern/Expander/EndsWith.php @@ -7,6 +7,8 @@ final class EndsWith implements PatternExpander { + const NAME = 'endsWith'; + /** * @var */ @@ -22,6 +24,14 @@ final class EndsWith implements PatternExpander */ private $ignoreCase; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param string $stringEnding * @param bool $ignoreCase diff --git a/src/Matcher/Pattern/Expander/GreaterThan.php b/src/Matcher/Pattern/Expander/GreaterThan.php index 8acc4248..a4b1c15b 100644 --- a/src/Matcher/Pattern/Expander/GreaterThan.php +++ b/src/Matcher/Pattern/Expander/GreaterThan.php @@ -7,6 +7,8 @@ final class GreaterThan implements PatternExpander { + const NAME = 'greaterThan'; + /** * @var */ @@ -17,6 +19,14 @@ final class GreaterThan implements PatternExpander */ private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $boundary */ diff --git a/src/Matcher/Pattern/Expander/InArray.php b/src/Matcher/Pattern/Expander/InArray.php index c7c10e61..ae8e468a 100644 --- a/src/Matcher/Pattern/Expander/InArray.php +++ b/src/Matcher/Pattern/Expander/InArray.php @@ -7,6 +7,8 @@ final class InArray implements PatternExpander { + const NAME = 'inArray'; + /** * @var null|string */ @@ -17,6 +19,14 @@ final class InArray implements PatternExpander */ private $value; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $value */ diff --git a/src/Matcher/Pattern/Expander/IsDateTime.php b/src/Matcher/Pattern/Expander/IsDateTime.php index 1612459f..73c6d651 100644 --- a/src/Matcher/Pattern/Expander/IsDateTime.php +++ b/src/Matcher/Pattern/Expander/IsDateTime.php @@ -7,11 +7,21 @@ final class IsDateTime implements PatternExpander { + const NAME = 'isDateTime'; + /** * @var null|string */ private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param string $value * @return boolean diff --git a/src/Matcher/Pattern/Expander/IsEmail.php b/src/Matcher/Pattern/Expander/IsEmail.php index 337dcc0c..32ccad56 100644 --- a/src/Matcher/Pattern/Expander/IsEmail.php +++ b/src/Matcher/Pattern/Expander/IsEmail.php @@ -7,11 +7,21 @@ final class IsEmail implements PatternExpander { + const NAME = 'isEmail'; + /** * @var null|string */ private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param string $value * @return boolean diff --git a/src/Matcher/Pattern/Expander/IsEmpty.php b/src/Matcher/Pattern/Expander/IsEmpty.php index 673a53e8..dacc6e55 100644 --- a/src/Matcher/Pattern/Expander/IsEmpty.php +++ b/src/Matcher/Pattern/Expander/IsEmpty.php @@ -7,8 +7,18 @@ final class IsEmpty implements PatternExpander { + const NAME = 'isEmpty'; + private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param mixed $value * diff --git a/src/Matcher/Pattern/Expander/IsNotEmpty.php b/src/Matcher/Pattern/Expander/IsNotEmpty.php index 65b7754c..b5fdfbac 100644 --- a/src/Matcher/Pattern/Expander/IsNotEmpty.php +++ b/src/Matcher/Pattern/Expander/IsNotEmpty.php @@ -7,8 +7,18 @@ final class IsNotEmpty implements PatternExpander { + const NAME = 'isNotEmpty'; + private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $value * @return boolean diff --git a/src/Matcher/Pattern/Expander/IsUrl.php b/src/Matcher/Pattern/Expander/IsUrl.php index ca4ad5cf..de45bb78 100644 --- a/src/Matcher/Pattern/Expander/IsUrl.php +++ b/src/Matcher/Pattern/Expander/IsUrl.php @@ -7,11 +7,21 @@ final class IsUrl implements PatternExpander { + const NAME = 'isUrl'; + /** * @var null|string */ private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param string $value * @return boolean diff --git a/src/Matcher/Pattern/Expander/LowerThan.php b/src/Matcher/Pattern/Expander/LowerThan.php index e17a74f7..48df2b8f 100644 --- a/src/Matcher/Pattern/Expander/LowerThan.php +++ b/src/Matcher/Pattern/Expander/LowerThan.php @@ -7,6 +7,8 @@ final class LowerThan implements PatternExpander { + const NAME = 'lowerThan'; + /** * @var */ @@ -17,6 +19,14 @@ final class LowerThan implements PatternExpander */ private $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param $boundary */ diff --git a/src/Matcher/Pattern/Expander/MatchRegex.php b/src/Matcher/Pattern/Expander/MatchRegex.php index 73a83e89..4c5f1ddc 100644 --- a/src/Matcher/Pattern/Expander/MatchRegex.php +++ b/src/Matcher/Pattern/Expander/MatchRegex.php @@ -7,6 +7,8 @@ final class MatchRegex implements PatternExpander { + const NAME = 'matchRegex'; + /** * @var null|string */ @@ -17,6 +19,14 @@ final class MatchRegex implements PatternExpander */ private $pattern; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + /** * @param string $pattern */ diff --git a/src/Matcher/Pattern/Expander/OneOf.php b/src/Matcher/Pattern/Expander/OneOf.php index 7407e62e..b8bcb896 100644 --- a/src/Matcher/Pattern/Expander/OneOf.php +++ b/src/Matcher/Pattern/Expander/OneOf.php @@ -7,6 +7,8 @@ final class OneOf implements PatternExpander { + const NAME = 'oneOf'; + /** * @var PatternExpander[] */ @@ -14,6 +16,14 @@ final class OneOf implements PatternExpander protected $error; + /** + * {@inheritdoc} + */ + public static function is(string $name) + { + return self::NAME === $name; + } + public function __construct() { if (func_num_args() < 2) { diff --git a/src/Matcher/Pattern/Expander/Optional.php b/src/Matcher/Pattern/Expander/Optional.php new file mode 100644 index 00000000..dbc6a391 --- /dev/null +++ b/src/Matcher/Pattern/Expander/Optional.php @@ -0,0 +1,34 @@ +error; } + + /** + * {@inheritdoc} + */ + public function hasExpander(string $expanderName) + { + foreach ($this->expanders as $expander) { + if ($expander::is($expanderName)) { + return true; + } + } + + return false; + } } diff --git a/src/Parser/ExpanderInitializer.php b/src/Parser/ExpanderInitializer.php index 9f21bcd7..cc9b3dcb 100644 --- a/src/Parser/ExpanderInitializer.php +++ b/src/Parser/ExpanderInitializer.php @@ -2,35 +2,36 @@ namespace Coduo\PHPMatcher\Parser; -use Coduo\PHPMatcher\AST\Expander; +use Coduo\PHPMatcher\AST\Expander as ExpanderNode; use Coduo\PHPMatcher\Exception\InvalidArgumentException; use Coduo\PHPMatcher\Exception\InvalidExpanderTypeException; use Coduo\PHPMatcher\Exception\UnknownExpanderClassException; use Coduo\PHPMatcher\Exception\UnknownExpanderException; use Coduo\PHPMatcher\Matcher\Pattern\PatternExpander; +use Coduo\PHPMatcher\Matcher\Pattern\Expander; final class ExpanderInitializer { /** * @var array */ - private $expanderDefinitions = array( - "startsWith" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\StartsWith", - "endsWith" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\EndsWith", - "isNotEmpty" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\IsNotEmpty", - "isEmpty" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\IsEmpty", - "isDateTime" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\IsDateTime", - "isEmail" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\IsEmail", - "isUrl" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\IsUrl", - "lowerThan" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\LowerThan", - "greaterThan" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\GreaterThan", - "inArray" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\InArray", - "count" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\Count", - "contains" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\Contains", - "matchRegex" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\MatchRegex", - - "oneOf" => "Coduo\\PHPMatcher\\Matcher\\Pattern\\Expander\\OneOf" - ); + private $expanderDefinitions = [ + Expander\Contains::NAME => Expander\Contains::class, + Expander\Count::NAME => Expander\Count::class, + Expander\EndsWith::NAME => Expander\EndsWith::class, + Expander\GreaterThan::NAME => Expander\GreaterThan::class, + Expander\InArray::NAME => Expander\InArray::class, + Expander\IsDateTime::NAME => Expander\IsDateTime::class, + Expander\IsEmail::NAME => Expander\IsEmail::class, + Expander\IsEmpty::NAME => Expander\IsEmpty::class, + Expander\IsNotEmpty::NAME => Expander\IsNotEmpty::class, + Expander\IsUrl::NAME => Expander\IsUrl::class, + Expander\LowerThan::NAME => Expander\LowerThan::class, + Expander\MatchRegex::NAME => Expander\MatchRegex::class, + Expander\OneOf::NAME => Expander\OneOf::class, + Expander\Optional::NAME => Expander\Optional::class, + Expander\StartsWith::NAME => Expander\StartsWith::class, + ]; /** * @param string $expanderName @@ -70,12 +71,12 @@ public function getExpanderDefinition($expanderName) } /** - * @param Expander $expanderNode + * @param ExpanderNode $expanderNode * @throws InvalidExpanderTypeException * @throws UnknownExpanderException * @return PatternExpander */ - public function initialize(Expander $expanderNode) + public function initialize(ExpanderNode $expanderNode) { if (!array_key_exists($expanderNode->getName(), $this->expanderDefinitions)) { throw new UnknownExpanderException(sprintf("Unknown expander \"%s\"", $expanderNode->getName())); @@ -86,7 +87,7 @@ public function initialize(Expander $expanderNode) if ($expanderNode->hasArguments()) { $arguments = array(); foreach ($expanderNode->getArguments() as $argument) { - $arguments[] = ($argument instanceof Expander) + $arguments[] = ($argument instanceof ExpanderNode) ? $this->initialize($argument) : $argument; } diff --git a/tests/Matcher/Pattern/Expander/OptionalTest.php b/tests/Matcher/Pattern/Expander/OptionalTest.php new file mode 100644 index 00000000..ef8b0a87 --- /dev/null +++ b/tests/Matcher/Pattern/Expander/OptionalTest.php @@ -0,0 +1,30 @@ +assertEquals($expectedResult, $expander->match($value)); + } + + public static function examplesProvider() + { + return array( + array(array(), true), + array(array('data'), true), + array('', true), + array(0, true), + array(10.1, true), + array(null, true), + array('Lorem ipsum', true), + ); + } +} diff --git a/tests/Matcher/Pattern/PatternTest.php b/tests/Matcher/Pattern/PatternTest.php new file mode 100644 index 00000000..185af9c0 --- /dev/null +++ b/tests/Matcher/Pattern/PatternTest.php @@ -0,0 +1,44 @@ +pattern = new TypePattern('dummy'); + $this->pattern->addExpander(new isEmail()); + $this->pattern->addExpander(new isEmpty()); + $this->pattern->addExpander(new Optional()); + } + + /** + * @dataProvider examplesProvider + */ + public function test_has_expander($expander, $expectedResult) + { + $this->assertEquals($expectedResult, $this->pattern->hasExpander($expander)); + } + + public static function examplesProvider() + { + return array( + array("isEmail", true), + array("isEmpty", true), + array("optional", true), + array("isUrl", false), + array("non existing expander", false), + ); + } +} diff --git a/tests/MatcherTest.php b/tests/MatcherTest.php index bdeebf27..6d750205 100644 --- a/tests/MatcherTest.php +++ b/tests/MatcherTest.php @@ -175,6 +175,23 @@ public function jsonDataProvider() "nextPage": "@string@" }', ), + 'excludes missing property from match for optional property' => array( + /** @lang JSON */ + '{ + "users":[], + "prevPage": "http:\/\/example.com\/api\/users\/1?limit=2", + "currPage": 2 + }', + /** @lang JSON */ + '{ + "users":[ + "@...@" + ], + "prevPage": "@string@.optional()", + "nextPage": "@string@.optional()", + "currPage": "@integer@.optional()" + }', + ), ); } @@ -208,6 +225,46 @@ public function test_matcher_with_xml() + +XML; + + $this->assertTrue($this->matcher->match($xml, $xmlPattern)); + $this->assertTrue(PHPMatcher::match($xml, $xmlPattern)); + } + + + + public function test_matcher_with_xml_including_optional_node() + { + $xml = << + + + + + IBM + Any Value + + + + +XML; + $xmlPattern = << + + + + + @string@.optional() + @string@.optional() + @integer@.optional() + + + XML;