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;