Create KeySet rule

This commit is contained in:
Henrique Moody 2015-07-20 07:16:46 -03:00
parent 29fd82815b
commit 7d9d19009a
6 changed files with 426 additions and 0 deletions

50
docs/KeySet.md Normal file
View file

@ -0,0 +1,50 @@
# KeySet
- `v::keySet(Key $rule...)`
Validates a keys in a defined structure.
```php
$dict = array('foo' => 42);
v::keySet(
v::key('foo', v::int())
)->validate($dict); //true
```
Extra keys are not allowed:
```php
$dict = array('foo' => 42, 'bar' => 'String');
v::keySet(
v::key('foo', v::int())
)->validate($dict); //false
```
Missing required keys are not allowed:
```php
$dict = array('foo' => 42, 'bar' => 'String');
v::keySet(
v::key('foo', v::int()),
v::key('bar', v::string()),
v::key('baz', v::bool())
)->validate($dict); //false
```
Missing non-required keys are allowed:
```php
$dict = array('foo' => 42, 'bar' => 'String');
v::keySet(
v::key('foo', v::int()),
v::key('bar', v::string()),
v::key('baz', v::bool(), false)
)->validate($dict); //true
```
The keys' order is not considered in the validation.
See also:
* [Key](Key.md)

View file

@ -90,6 +90,7 @@
* [EndsWith](EndsWith.md)
* [In](In.md)
* [Key](Key.md)
* [KeySet](KeySet.md)
* [Length](Length.md)
* [NotEmpty](NotEmpty.md)
* [StartsWith](StartsWith.md)
@ -213,6 +214,7 @@
* [Ip](Ip.md)
* [Json](Json.md)
* [Key](Key.md)
* [KeySet](KeySet.md)
* [LeapDate](LeapDate.md)
* [LeapYear](LeapYear.md)
* [Length](Length.md)

View file

@ -0,0 +1,57 @@
<?php
/*
* This file is part of Respect/Validation.
*
* (c) Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
*
* For the full copyright and license information, please view the "LICENSE.md"
* file that was distributed with this source code.
*/
namespace Respect\Validation\Exceptions;
class KeySetException extends AbstractGroupedException
{
const STRUCTURE = 2;
/**
* @var array
*/
public static $defaultTemplates = array(
self::MODE_DEFAULT => array(
self::NONE => 'All of the required rules must pass for {{name}}',
self::SOME => 'These rules must pass for {{name}}',
self::STRUCTURE => 'Must have keys {{keys}}',
),
self::MODE_NEGATIVE => array(
self::NONE => 'None of these rules must pass for {{name}}',
self::SOME => 'These rules must not pass for {{name}}',
self::STRUCTURE => 'Must not have keys {{keys}}',
),
);
/**
* {@inheritdoc}
*/
public function chooseTemplate()
{
if ($this->getParam('keys')) {
return static::STRUCTURE;
}
return parent::chooseTemplate();
}
/**
* {@inheritdoc}
*/
public function setParam($name, $value)
{
if ($name === 'keys') {
$value = trim(json_encode($value), '[]');
}
return parent::setParam($name, $value);
}
}

145
library/Rules/KeySet.php Normal file
View file

@ -0,0 +1,145 @@
<?php
/*
* This file is part of Respect/Validation.
*
* (c) Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
*
* For the full copyright and license information, please view the "LICENSE.md"
* file that was distributed with this source code.
*/
namespace Respect\Validation\Rules;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Exceptions\KeySetException;
use Respect\Validation\Validatable;
/**
* Validates a keys in a defined structure.
*
* @author Henrique Moody <henriquemoody@gmail.com>
*/
class KeySet extends AllOf
{
/**
* @param AllOf $rule
*
* @return Validatable
*/
private function filterAllOf(AllOf $rule)
{
$rules = $rule->getRules();
if (count($rules) != 1) {
throw new ComponentException('AllOf rule must have only one Key rule');
}
return current($rules);
}
/**
* {@inheritdoc}
*/
public function addRule($rule)
{
if ($rule instanceof AllOf) {
$rule = $this->filterAllOf($rule);
}
if (!$rule instanceof Key) {
throw new ComponentException('KeySet rule accepts only Key rules');
}
$this->appendRule($rule);
return $this;
}
/**
* {@inheritdoc}
*/
public function addRules(array $rules)
{
foreach ($rules as $rule) {
$this->addRule($rule);
}
return $this;
}
/**
* @return array
*/
public function getKeys()
{
$keys = array();
foreach ($this->getRules() as $keyRule) {
$keys[] = $keyRule->reference;
}
return $keys;
}
/**
* @param array $input
*
* @return bool
*/
private function hasValidStructure($input)
{
foreach ($this->getRules() as $keyRule) {
if (!array_key_exists($keyRule->reference, $input) && $keyRule->mandatory) {
return false;
}
unset($input[$keyRule->reference]);
}
return (count($input) == 0);
}
/**
* @throws KeySetException
*/
private function checkKeys($input)
{
if (!$this->hasValidStructure($input)) {
$params = array('keys' => $this->getKeys());
$exception = $this->reportError($input, $params);
throw $exception;
}
}
/**
* {@inheritdoc}
*/
public function assert($input)
{
$this->checkKeys($input);
return parent::assert($input);
}
/**
* {@inheritdoc}
*/
public function check($input)
{
$this->checkKeys($input);
return parent::check($input);
}
/**
* {@inheritdoc}
*/
public function validate($input)
{
if (!$this->hasValidStructure($input)) {
return false;
}
return parent::validate($input);
}
}

View file

@ -15,6 +15,7 @@ use ReflectionClass;
use Respect\Validation\Exceptions\AllOfException;
use Respect\Validation\Exceptions\ComponentException;
use Respect\Validation\Rules\AllOf;
use Respect\Validation\Rules\Key;
/**
* @method static Validator age(int $minAge = null, int $maxAge = null)
@ -65,6 +66,7 @@ use Respect\Validation\Rules\AllOf;
* @method static Validator ip(mixed $ipOptions = null)
* @method static Validator json()
* @method static Validator key(string $reference, Validatable $referenceValidator = null, bool $mandatory = true)
* @method static Validator keySet(Key $rule...)
* @method static Validator leapDate(string $format)
* @method static Validator leapYear()
* @method static Validator length(int $min = null, int $max = null, bool $inclusive = true)

170
tests/Rules/KeySetTest.php Normal file
View file

@ -0,0 +1,170 @@
<?php
/*
* This file is part of Respect/Validation.
*
* (c) Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
*
* For the full copyright and license information, please view the "LICENSE.md"
* file that was distributed with this source code.
*/
namespace Respect\Validation\Rules;
use PHPUnit_Framework_TestCase;
class KeySetTest extends PHPUnit_Framework_TestCase
{
public function testShouldAcceptKeyRule()
{
$key = new Key('foo', new AlwaysValid(), false);
$keySet = new KeySet($key);
$rules = $keySet->getRules();
$this->assertSame(current($rules), $key);
}
public function testShouldAcceptAllOfWithOneKeyRule()
{
$key = new Key('foo', new AlwaysValid(), false);
$allOf = new AllOf($key);
$keySet = new KeySet($allOf);
$rules = $keySet->getRules();
$this->assertSame(current($rules), $key);
}
/**
* @expectedException Respect\Validation\Exceptions\ComponentException
* @expectedExceptionMessage AllOf rule must have only one Key rule
*/
public function testShouldNotAcceptAllOfWithMoreThanOneKeyRule()
{
$key1 = new Key('foo', new AlwaysValid(), false);
$key2 = new Key('bar', new AlwaysValid(), false);
$allOf = new AllOf($key1, $key2);
new KeySet($allOf);
}
/**
* @expectedException Respect\Validation\Exceptions\ComponentException
* @expectedExceptionMessage KeySet rule accepts only Key rules
*/
public function testShouldNotAcceptAllOfWithANonKeyRule()
{
$alwaysValid = new AlwaysValid();
$allOf = new AllOf($alwaysValid);
new KeySet($allOf);
}
/**
* @expectedException Respect\Validation\Exceptions\ComponentException
* @expectedExceptionMessage KeySet rule accepts only Key rules
*/
public function testShouldNotAcceptANonKeyRule()
{
$alwaysValid = new AlwaysValid();
new KeySet($alwaysValid);
}
public function testShouldReturnKeys()
{
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), false);
$keySet = new KeySet($key1, $key2);
$this->assertEquals(array('foo', 'bar'), $keySet->getKeys());
}
public function testShouldValidateKeysWhenThereAreMissingRequiredKeys()
{
$input = array(
'foo' => 42,
);
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), true);
$keySet = new KeySet($key1, $key2);
$this->assertFalse($keySet->validate($input));
}
public function testShouldValidateKeysWhenThereAreMissingNonRequiredKeys()
{
$input = array(
'foo' => 42,
);
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), false);
$keySet = new KeySet($key1, $key2);
$this->assertTrue($keySet->validate($input));
}
public function testShouldValidateKeysWhenThereAreMoreKeys()
{
$input = array(
'foo' => 42,
'bar' => 'String',
'baz' => false,
);
$key1 = new Key('foo', new AlwaysValid(), false);
$key2 = new Key('bar', new AlwaysValid(), false);
$keySet = new KeySet($key1, $key2);
$this->assertFalse($keySet->validate($input));
}
public function testShouldValidateKeysWhenEmpty()
{
$input = array();
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), true);
$keySet = new KeySet($key1, $key2);
$this->assertFalse($keySet->validate($input));
}
/**
* @expectedException Respect\Validation\Exceptions\KeySetException
* @expectedExceptionMessage Must have keys "foo","bar"
*/
public function testShouldCheckKeys()
{
$input = array();
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), true);
$keySet = new KeySet($key1, $key2);
$keySet->check($input);
}
/**
* @expectedException Respect\Validation\Exceptions\KeySetException
* @expectedExceptionMessage Must have keys "foo","bar"
*/
public function testShouldAssertKeys()
{
$input = array();
$key1 = new Key('foo', new AlwaysValid(), true);
$key2 = new Key('bar', new AlwaysValid(), true);
$keySet = new KeySet($key1, $key2);
$keySet->assert($input);
}
}