Skip to content

Commit

Permalink
Add option to apply default values from the schema
Browse files Browse the repository at this point in the history
  • Loading branch information
erayd committed Jan 18, 2017
1 parent 325a0f8 commit 14cadd2
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 1 deletion.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ is_bool($request->processRefund); // true
is_int($request->refundAmount); // true
```

### Default values

If your schema contains default values, you can have these automatically applied during validation:

```php
<?php

use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;

$request = (object)[
'refundAmount'=>17
];

$validator = new Validator();

$validator->coerceDefault($request, (object)[
"type"=>"object",
"properties"=>(object)[
"processRefund"=>(object)[
"type"=>"boolean",
"default"=>true
]
]
]); //validates, and sets defaults for missing properties

is_bool($request->processRefund); // true
$request->processRefund; // true
```

*Note that setting default values also enables type coercion.*

### With inline references

```php
Expand Down
1 change: 1 addition & 0 deletions src/JsonSchema/Constraints/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ abstract class Constraint implements ConstraintInterface

const CHECK_MODE_NORMAL = 0x00000001;
const CHECK_MODE_TYPE_CAST = 0x00000002;
const CHECK_MODE_APPLY_DEFAULTS = 0x00000004;

/**
* @var Factory
Expand Down
15 changes: 15 additions & 0 deletions src/JsonSchema/Constraints/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use JsonSchema\SchemaStorageInterface;
use JsonSchema\Uri\UriRetriever;
use JsonSchema\UriRetrieverInterface;
use JsonSchema\Constraints\Constraint;

/**
* Factory for centralize constraint initialization.
Expand Down Expand Up @@ -148,4 +149,18 @@ public function getCheckMode()
{
return $this->checkMode;
}

/**
* Update apply defaults setting in checkmode
*
* @param boolean $applyDefaults
*/
public function setApplyDefaults($applyDefaults = true)
{
if ($applyDefaults) {
$this->checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS;
} else {
$this->checkMode &= ~Constraint::CHECK_MODE_APPLY_DEFAULTS;
}
}
}
10 changes: 10 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ public static function propertyGet($value, $property)
return $value[$property];
}

public static function propertySet($value, $property, $data)
{
if (is_object($value)) {
$value->{$property} = $data;
}

$value[$property] = $data;
}


public static function propertyExists($value, $property)
{
if (is_object($value)) {
Expand Down
5 changes: 5 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public static function propertyGet($value, $property)
return $value->{$property};
}

public static function propertySet($value, $property, $data)
{
$value->{$property} = $data;
}

public static function propertyExists($value, $property)
{
return property_exists($value, $property);
Expand Down
2 changes: 2 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public static function isArray($value);

public static function propertyGet($value, $property);

public static function propertySet($value, $property, $data);

public static function propertyExists($value, $property);

public static function propertyCount($value);
Expand Down
30 changes: 29 additions & 1 deletion src/JsonSchema/Constraints/UndefinedConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n
}

// check object
if ($this->getTypeCheck()->isObject($value)) {
if (TypeCheck\LooseTypeCheck::isObject($value)) { // Fixes failing assoc tests for default values - currently investigating
//if ($this->getTypeCheck()->isObject($value)) { // to find the root cause of this, noting all other assoc tests pass.
$this->checkObject(
$value,
isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema,
Expand Down Expand Up @@ -118,6 +119,33 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
}
}

// Apply default values from schema
if ($coerce && $this->factory->getCheckMode() & self::CHECK_MODE_APPLY_DEFAULTS) {
if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) {
foreach ($schema->properties as $i => $propertyDefinition) {
if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) {
$this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default);
}
}
} elseif ($this->getTypeCheck()->isArray($value)) {
if (isset($schema->properties)) {
foreach ($schema->properties as $i => $propertyDefinition) {
if (!isset($value[$i]) && isset($propertyDefinition->default)) {
$value[$i] = $propertyDefinition->default;
}
}
} elseif (isset($schema->items)) {
foreach ($schema->items as $i => $itemDefinition) {
if (!isset($value[$i]) && isset($itemDefinition->default)) {
$value[$i] = $itemDefinition->default;
}
}
}
} elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) {
$value = $schema->default;
}
}

// Verify required values
if ($this->getTypeCheck()->isObject($value)) {
if (!($value instanceof UndefinedConstraint) && isset($schema->required) && is_array($schema->required)) {
Expand Down
13 changes: 13 additions & 0 deletions src/JsonSchema/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,17 @@ public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = n

$this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR));
}

/**
* Does everything that coerce does, but will also set values to their default, if the value is not
* set and a default is available in the schema. Note that the first argumen is passwd by
* reference, so you must pass in a variable.
*
* {@inheritDoc}
*/
public function coerceDefault(&$value, $schema = null, JsonPointer $path = null, $i = null)
{
$this->factory->setApplyDefaults(true);
$this->coerce($value, $schema, $path, $i);
}
}
129 changes: 129 additions & 0 deletions tests/Constraints/DefaultPropertiesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JsonSchema\Tests\Constraints;
use JsonSchema\SchemaStorage;
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\Factory;

class DefaultPropertiesTest extends VeryBaseTestCase
{
public function getValidTests()
{
return array(
array(// default value for entire object
'',
'{"default":"valueOne"}',
'"valueOne"'
),
array(// default value in an empty object
'{}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"valueOne"}'
),
array(// default value for top-level property
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// default value for sub-property
'{"propertyOne":{}}',
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
),
array(// default value for sub-property with sibling
'{"propertyOne":{"propertyTwo":"valueTwo"}}',
'{"properties":{"propertyOne":{"properties":{"propertyThree":{"default":"valueThree"}}}}}',
'{"propertyOne":{"propertyTwo":"valueTwo","propertyThree":"valueThree"}}'
),
array(// default value for top-level property with type check
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// default value for top-level property with v3 required check
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo","required":"true"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// default value for top-level property with v4 required check
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(//default value for an already set property
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"alreadySetValueOne"}'
),
array(//default value is required
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo","required":true}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(//default item value for an array
'["valueOne"]',
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
'["valueOne","valueTwo"]'
),
array(//default item value for an empty array
'[]',
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
'["valueOne"]'
),
array(//property without a default available
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"type":"string"}}}',
'{"propertyOne":"alreadySetValueOne"}'
)
);
}

/**
* @dataProvider getValidTests
*/
public function testValidCases($input, $schema, $expectOutput = null)
{
if (is_string($input)) {
$assoc = false;
$inputDecoded = json_decode($input);
} else {
$assoc = true;
$inputDecoded = $input;
}

$validator = new Validator();
$validator->coerceDefault($inputDecoded, json_decode($schema));

$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));

if ($expectOutput !== null) {
$this->assertEquals($expectOutput, json_encode($inputDecoded));
}
}

/**
* @dataProvider getValidTests
*/
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null)
{
$input = json_decode($input, true);
self::testValidCases($input, $schema, $expectOutput);
}

public function testDontApplyDefaults()
{
$f = new Factory();
$f->setApplyDefaults(false);

$this->assertEquals(0, $f->getCheckMode() & Constraint::CHECK_MODE_APPLY_DEFAULTS);
}

}

0 comments on commit 14cadd2

Please sign in to comment.