Skip to content

Commit

Permalink
Add CHECK_MODE_ONLY_REQUIRED_DEFAULTS
Browse files Browse the repository at this point in the history
If CHECK_MODE_ONLY_REQUIRED_DEFAULTS is set, then only apply defaults
if they are marked as required.
  • Loading branch information
erayd committed Mar 6, 2017
1 parent f638716 commit 16a28ff
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 60 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ third argument to `Validator::validate()`, or can be provided as the third argum
| `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects |
| `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible |
| `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set |
| `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required |
| `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails |

Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS`
Expand Down
13 changes: 7 additions & 6 deletions src/JsonSchema/Constraints/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface
{
protected $inlineSchemaProperty = '$schema';

const CHECK_MODE_NONE = 0x00000000;
const CHECK_MODE_NORMAL = 0x00000001;
const CHECK_MODE_TYPE_CAST = 0x00000002;
const CHECK_MODE_COERCE_TYPES = 0x00000004;
const CHECK_MODE_APPLY_DEFAULTS = 0x00000008;
const CHECK_MODE_EXCEPTIONS = 0x00000010;
const CHECK_MODE_NONE = 0x00000000;
const CHECK_MODE_NORMAL = 0x00000001;
const CHECK_MODE_TYPE_CAST = 0x00000002;
const CHECK_MODE_COERCE_TYPES = 0x00000004;
const CHECK_MODE_APPLY_DEFAULTS = 0x00000008;
const CHECK_MODE_EXCEPTIONS = 0x00000010;
const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000020;

/**
* Bubble down the path
Expand Down
99 changes: 71 additions & 28 deletions src/JsonSchema/Constraints/UndefinedConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,34 +111,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
}

// Apply default values from schema
if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) {
if (isset($schema->properties) && LooseTypeCheck::isObject($value)) {
// $value is an object or assoc array, and properties are defined - treat as an object
foreach ($schema->properties as $currentProperty => $propertyDefinition) {
if (!LooseTypeCheck::propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) {
if (is_object($propertyDefinition->default)) {
LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default);
} else {
LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default);
}
}
}
} elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) {
// $value is an array, and items are defined - treat as plain array
foreach ($schema->items as $currentItem => $itemDefinition) {
if (!isset($value[$currentItem]) && isset($itemDefinition->default)) {
if (is_object($itemDefinition->default)) {
$value[$currentItem] = clone $itemDefinition->default;
} else {
$value[$currentItem] = $itemDefinition->default;
}
}
}
} elseif (($value instanceof self || $value === null) && isset($schema->default)) {
// $value is a leaf, not a container - apply the default directly
$value = is_object($schema->default) ? clone $schema->default : $schema->default;
}
}
$this->applyDefaultValues($value, $schema);

// Verify required values
if ($this->getTypeCheck()->isObject($value)) {
Expand Down Expand Up @@ -200,6 +173,76 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
}
}

/**
* Apply default values
*
* @param mixed $value
* @param mixed $schema
*/
protected function applyDefaultValues(&$value, $schema)
{
// only apply defaults if feature is enabled
if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) {
return;
}

// check whether this default should be applied
$shouldApply = function ($definition, $name = null) use ($schema) {
// required-only mode is off
if (!$this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS)) {
return true;
}
// draft-04 required is set
if (
$name !== null
&& isset($schema->required)
&& is_array($schema->required)
&& in_array($name, $schema->required)
) {
return true;
}
// draft-03 required is set
if (isset($definition->required) && !is_array($definition->required) && $definition->required) {
return true;
}
// default case
return false;
};

// apply defaults if appropriate
if (isset($schema->properties) && LooseTypeCheck::isObject($value)) {
// $value is an object or assoc array, and properties are defined - treat as an object
foreach ($schema->properties as $currentProperty => $propertyDefinition) {
if (
!LooseTypeCheck::propertyExists($value, $currentProperty)
&& isset($propertyDefinition->default)
&& $shouldApply($propertyDefinition, $currentProperty)
) {
// assign default value
if (is_object($propertyDefinition->default)) {
LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default);
} else {
LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default);
}
}
}
} elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) {
// $value is an array, and items are defined - treat as plain array
foreach ($schema->items as $currentItem => $itemDefinition) {
if (!isset($value[$currentItem]) && isset($itemDefinition->default) && $shouldApply($itemDefinition)) {
if (is_object($itemDefinition->default)) {
$value[$currentItem] = clone $itemDefinition->default;
} else {
$value[$currentItem] = $itemDefinition->default;
}
}
}
} elseif (($value instanceof self || $value === null) && isset($schema->default) && $shouldApply($schema)) {
// $value is a leaf, not a container - apply the default directly
$value = is_object($schema->default) ? clone $schema->default : $schema->default;
}
}

/**
* Validate allOf, anyOf, and oneOf properties
*
Expand Down
73 changes: 47 additions & 26 deletions tests/Constraints/DefaultPropertiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,95 +19,116 @@ class DefaultPropertiesTest extends VeryBaseTestCase
public function getValidTests()
{
return array(
array(// default value for entire object
array(// #0 default value for entire object
'',
'{"default":"valueOne"}',
'"valueOne"'
),
array(// default value in an empty object
array(// #1 default value in an empty object
'{}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"valueOne"}'
),
array(// default value for top-level property
array(// #2 default value for top-level property
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// default value for sub-property
array(// #3 default value for sub-property
'{"propertyOne":{}}',
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
),
array(// default value for sub-property with sibling
array(// #4 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
array(// #5 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
array(// #6 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
array(// #7 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
array(// #8 default value for an already set property
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"alreadySetValueOne"}'
),
array(//default item value for an array
array(// #9 default item value for an array
'["valueOne"]',
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
'["valueOne","valueTwo"]'
),
array(//default item value for an empty array
array(// #10 default item value for an empty array
'[]',
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
'["valueOne"]'
),
array(//property without a default available
array(// #11 property without a default available
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"type":"string"}}}',
'{"propertyOne":"alreadySetValueOne"}'
),
array(// default property value is an object
array(// #12 default property value is an object
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":{}}}}',
'{"propertyOne":"valueOne","propertyTwo":{}}'
),
array(// default item value is an object
array(// #13 default item value is an object
'[]',
'{"type":"array","items":[{"default":{}}]}',
'[{}]'
),
array(// #14 only set required values (draft-04)
'{}',
'{
"properties": {
"propertyOne": {"default": "valueOne"},
"propertyTwo": {"default": "valueTwo"}
},
"required": ["propertyTwo"]
}',
'{"propertyTwo":"valueTwo"}',
Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS
),
array(// #15 only set required values (draft-03)
'{}',
'{
"properties": {
"propertyOne": {"default": "valueOne"},
"propertyTwo": {"default": "valueTwo", "required": true}
}
}',
'{"propertyTwo":"valueTwo"}',
Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS
)
);
}

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

if ($validator === null) {
$factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS);
$validator = new Validator($factory);
}
$validator->validate($inputDecoded, json_decode($schema));
$checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS;
$validator = new Validator();
$validator->validate($inputDecoded, json_decode($schema), $checkMode);

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

Expand All @@ -119,22 +140,22 @@ public function testValidCases($input, $schema, $expectOutput = null, $validator
/**
* @dataProvider getValidTests
*/
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null)
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null, $checkMode = 0)
{
$input = json_decode($input, true);

$factory = new Factory(null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS);
self::testValidCases($input, $schema, $expectOutput, new Validator($factory));
$checkMode |= Constraint::CHECK_MODE_TYPE_CAST;
self::testValidCases($input, $schema, $expectOutput, $checkMode);
}

/**
* @dataProvider getValidTests
*/
public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null)
public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null, $checkMode = 0)
{
$input = json_decode($input, true);
$factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS);
self::testValidCases($input, $schema, $expectOutput, new Validator($factory));

self::testValidCases($input, $schema, $expectOutput, $checkMode);
}

public function testNoModificationViaReferences()
Expand Down

0 comments on commit 16a28ff

Please sign in to comment.