From 72b94c167462977b92ad69205e20cd47e227f689 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 8 Mar 2017 04:20:26 +1300 Subject: [PATCH 01/20] Add URI translation, package:// URI scheme & bundle spec schemas (#362) * Add URI translation for retrieval & add local copies of spec schema * Use dist copies of schemas No need to keep duplicate files around in package://tests/fixtures/ if we're distributing them for users anyway. * Move package:// translation after all other rules Allows users to rewrite to package:// targets and still have the URI work. --- dist/schema/json-schema-draft-03.json | 174 ++++++++++++++++++ dist/schema/json-schema-draft-04.json | 150 +++++++++++++++ src/JsonSchema/Uri/UriRetriever.php | 38 +++- tests/Constraints/VeryBaseTestCase.php | 4 +- tests/Uri/UriRetrieverTest.php | 49 +++++ tests/fixtures/foobar.json | 12 ++ tests/fixtures/json-schema-draft-03.json | 193 -------------------- tests/fixtures/json-schema-draft-04.json | 221 ----------------------- 8 files changed, 424 insertions(+), 417 deletions(-) create mode 100644 dist/schema/json-schema-draft-03.json create mode 100644 dist/schema/json-schema-draft-04.json create mode 100644 tests/fixtures/foobar.json delete mode 100644 tests/fixtures/json-schema-draft-03.json delete mode 100644 tests/fixtures/json-schema-draft-04.json diff --git a/dist/schema/json-schema-draft-03.json b/dist/schema/json-schema-draft-03.json new file mode 100644 index 00000000..7a1a2d38 --- /dev/null +++ b/dist/schema/json-schema-draft-03.json @@ -0,0 +1,174 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "http://json-schema.org/draft-03/schema#", + "type": "object", + + "properties": { + "type": { + "type": [ "string", "array" ], + "items": { + "type": [ "string", { "$ref": "#" } ] + }, + "uniqueItems": true, + "default": "any" + }, + + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + + "additionalProperties": { + "type": [ { "$ref": "#" }, "boolean" ], + "default": {} + }, + + "items": { + "type": [ { "$ref": "#" }, "array" ], + "items": { "$ref": "#" }, + "default": {} + }, + + "additionalItems": { + "type": [ { "$ref": "#" }, "boolean" ], + "default": {} + }, + + "required": { + "type": "boolean", + "default": false + }, + + "dependencies": { + "type": "object", + "additionalProperties": { + "type": [ "string", "array", { "$ref": "#" } ], + "items": { + "type": "string" + } + }, + "default": {} + }, + + "minimum": { + "type": "number" + }, + + "maximum": { + "type": "number" + }, + + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + + "maxItems": { + "type": "integer", + "minimum": 0 + }, + + "uniqueItems": { + "type": "boolean", + "default": false + }, + + "pattern": { + "type": "string", + "format": "regex" + }, + + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + + "maxLength": { + "type": "integer" + }, + + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + + "default": { + "type": "any" + }, + + "title": { + "type": "string" + }, + + "description": { + "type": "string" + }, + + "format": { + "type": "string" + }, + + "divisibleBy": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 1 + }, + + "disallow": { + "type": [ "string", "array" ], + "items": { + "type": [ "string", { "$ref": "#" } ] + }, + "uniqueItems": true + }, + + "extends": { + "type": [ { "$ref": "#" }, "array" ], + "items": { "$ref": "#" }, + "default": {} + }, + + "id": { + "type": "string", + "format": "uri" + }, + + "$ref": { + "type": "string", + "format": "uri" + }, + + "$schema": { + "type": "string", + "format": "uri" + } + }, + + "dependencies": { + "exclusiveMinimum": "minimum", + "exclusiveMaximum": "maximum" + }, + + "default": {} +} diff --git a/dist/schema/json-schema-draft-04.json b/dist/schema/json-schema-draft-04.json new file mode 100644 index 00000000..85eb502a --- /dev/null +++ b/dist/schema/json-schema-draft-04.json @@ -0,0 +1,150 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/src/JsonSchema/Uri/UriRetriever.php b/src/JsonSchema/Uri/UriRetriever.php index ebb7eb33..65452788 100644 --- a/src/JsonSchema/Uri/UriRetriever.php +++ b/src/JsonSchema/Uri/UriRetriever.php @@ -24,6 +24,14 @@ */ class UriRetriever implements BaseUriRetrieverInterface { + /** + * @var array Map of URL translations + */ + protected $translationMap = array( + // use local copies of the spec schemas + '|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' + ); + /** * @var null|UriRetrieverInterface */ @@ -134,7 +142,7 @@ public function resolvePointer($jsonSchema, $uri) /** * {@inheritdoc} */ - public function retrieve($uri, $baseUri = null) + public function retrieve($uri, $baseUri = null, $translate = true) { $resolver = new UriResolver(); $resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri); @@ -146,6 +154,11 @@ public function retrieve($uri, $baseUri = null) $fetchUri = $resolver->generate($arParts); } + // apply URI translations + if ($translate) { + $fetchUri = $this->translate($fetchUri); + } + $jsonSchema = $this->loadSchema($fetchUri); // Use the JSON pointer if specified @@ -291,4 +304,27 @@ public function isValid($uri) return !empty($components); } + + /** + * Set a URL translation rule + */ + public function setTranslation($from, $to) + { + $this->translationMap[$from] = $to; + } + + /** + * Apply URI translation rules + */ + public function translate($uri) + { + foreach ($this->translationMap as $from => $to) { + $uri = preg_replace($from, $to, $uri); + } + + // translate references to local files within the json-schema package + $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri); + + return $uri; + } } diff --git a/tests/Constraints/VeryBaseTestCase.php b/tests/Constraints/VeryBaseTestCase.php index 7d8eb267..7cc0d1c6 100644 --- a/tests/Constraints/VeryBaseTestCase.php +++ b/tests/Constraints/VeryBaseTestCase.php @@ -66,7 +66,7 @@ private function getJsonSchemaDraft03() { if (!$this->jsonSchemaDraft03) { $this->jsonSchemaDraft03 = json_decode( - file_get_contents(__DIR__ . '/../fixtures/json-schema-draft-03.json') + file_get_contents(__DIR__ . '/../../dist/schema/json-schema-draft-03.json') ); } @@ -80,7 +80,7 @@ private function getJsonSchemaDraft04() { if (!$this->jsonSchemaDraft04) { $this->jsonSchemaDraft04 = json_decode( - file_get_contents(__DIR__ . '/../fixtures/json-schema-draft-04.json') + file_get_contents(__DIR__ . '/../../dist/schema/json-schema-draft-04.json') ); } diff --git a/tests/Uri/UriRetrieverTest.php b/tests/Uri/UriRetrieverTest.php index 5d0d0e95..01df161b 100644 --- a/tests/Uri/UriRetrieverTest.php +++ b/tests/Uri/UriRetrieverTest.php @@ -10,6 +10,7 @@ namespace JsonSchema\Tests\Uri; use JsonSchema\Exception\JsonDecodingException; +use JsonSchema\Uri\UriRetriever; use JsonSchema\Validator; /** @@ -279,4 +280,52 @@ private function mockRetriever($schema) $retriever->setAccessible(true); $retriever->setValue($factory, $retrieverMock); } + + public function testTranslations() + { + $retriever = new UriRetriever(); + + $uri = 'http://example.com/foo/bar'; + $translated = 'file://another/bar'; + + $retriever->setTranslation('|^https?://example.com/foo/bar#?|', 'file://another/bar'); + $this->assertEquals($translated, $retriever->translate($uri)); + } + + public function testPackageURITranslation() + { + $retriever = new UriRetriever(); + $root = sprintf('file://%s/', realpath(__DIR__ . '/../..')); + + $uri = $retriever->translate('package://foo/bar.json'); + $this->assertEquals("${root}foo/bar.json", $uri); + } + + public function testDefaultDistTranslations() + { + $retriever = new UriRetriever(); + $root = sprintf('file://%s/dist/schema/', realpath(__DIR__ . '/../..')); + + $this->assertEquals( + $root . 'json-schema-draft-03.json', + $retriever->translate('http://json-schema.org/draft-03/schema#') + ); + + $this->assertEquals( + $root . 'json-schema-draft-04.json', + $retriever->translate('http://json-schema.org/draft-04/schema#') + ); + } + + public function testRetrieveSchemaFromPackage() + { + $retriever = new UriRetriever(); + + // load schema from package + $schema = $retriever->retrieve('package://tests/fixtures/foobar.json'); + $this->assertNotFalse($schema); + + // check that the schema was loaded & processed correctly + $this->assertEquals('454f423bd7edddf0bc77af4130ed9161', md5(json_encode($schema))); + } } diff --git a/tests/fixtures/foobar.json b/tests/fixtures/foobar.json new file mode 100644 index 00000000..b27b6861 --- /dev/null +++ b/tests/fixtures/foobar.json @@ -0,0 +1,12 @@ +{ + "$id": "http://example.com/foo/bar#", + "type": "object", + "properties": { + "foo": { + "type": "string", + "default": "bar" + } + }, + "required": ["foo"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema-draft-03.json b/tests/fixtures/json-schema-draft-03.json deleted file mode 100644 index dcf07342..00000000 --- a/tests/fixtures/json-schema-draft-03.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "id": "http://json-schema.org/draft-03/schema#", - "type": "object", - "properties": { - "type": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true, - "default": "any" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "additionalProperties": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "items": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "additionalItems": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "required": { - "type": "boolean", - "default": false - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "array", - { - "$ref": "#" - } - ], - "items": { - "type": "string" - } - }, - "default": {} - }, - "minimum": { - "type": "number" - }, - "maximum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minItems": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxItems": { - "type": "integer", - "minimum": 0 - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "minLength": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxLength": { - "type": "integer" - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "default": { - "type": "any" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "format": { - "type": "string" - }, - "divisibleBy": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 1 - }, - "disallow": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true - }, - "extends": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "id": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - } - }, - "dependencies": { - "exclusiveMinimum": "minimum", - "exclusiveMaximum": "maximum" - }, - "default": {} -} \ No newline at end of file diff --git a/tests/fixtures/json-schema-draft-04.json b/tests/fixtures/json-schema-draft-04.json deleted file mode 100644 index 96e7f16a..00000000 --- a/tests/fixtures/json-schema-draft-04.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#" - } - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/positiveInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": { - "$ref": "#/definitions/positiveInteger" - }, - "minLength": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/schemaArray" - } - ], - "default": {} - }, - "maxItems": { - "$ref": "#/definitions/positiveInteger" - }, - "minItems": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": { - "$ref": "#/definitions/positiveInteger" - }, - "minProperties": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/stringArray" - }, - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/stringArray" - } - ] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "allOf": { - "$ref": "#/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schemaArray" - }, - "not": { - "$ref": "#" - } - }, - "dependencies": { - "exclusiveMaximum": [ - "maximum" - ], - "exclusiveMinimum": [ - "minimum" - ] - }, - "default": {} -} \ No newline at end of file From 3dba977b0f26591a5a6432765e124abdb4ad68b3 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Tue, 7 Mar 2017 07:21:05 -0800 Subject: [PATCH 02/20] centralize errors (#364) * centralize errors * isolate 'more' info * throw exception for missing error message * swap args --- composer.json | 3 +- src/JsonSchema/ConstraintError.php | 104 ++++++++++++++++++ src/JsonSchema/Constraints/BaseConstraint.php | 22 ++-- .../Constraints/CollectionConstraint.php | 16 ++- .../Constraints/ConstraintInterface.php | 10 +- src/JsonSchema/Constraints/EnumConstraint.php | 3 +- .../Constraints/FormatConstraint.php | 45 +++++--- .../Constraints/NumberConstraint.php | 21 ++-- .../Constraints/ObjectConstraint.php | 14 ++- .../Constraints/StringConstraint.php | 7 +- src/JsonSchema/Constraints/TypeConstraint.php | 7 +- .../Constraints/UndefinedConstraint.php | 30 +++-- .../Constraints/AdditionalPropertiesTest.php | 7 +- tests/Constraints/OfPropertiesTest.php | 21 +++- tests/Constraints/PointerTest.php | 28 ++++- 15 files changed, 268 insertions(+), 70 deletions(-) create mode 100644 src/JsonSchema/ConstraintError.php diff --git a/composer.json b/composer.json index f1f6faea..7707dfb7 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ } }], "require": { - "php": ">=5.3.3" + "php": ">=5.3.3", + "marc-mabe/php-enum":"2.3.1" }, "require-dev": { "json-schema/JSON-Schema-Test-Suite": "1.2.0", diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php new file mode 100644 index 00000000..d7b73346 --- /dev/null +++ b/src/JsonSchema/ConstraintError.php @@ -0,0 +1,104 @@ +getValue(); + static $messages = array( + self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items', + self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties', + self::ALL_OF => 'Failed to match all schemas', + self::ANY_OF => 'Failed to match at least one schema', + self::DEPENDENCIES => '%s depends on %s, which is missing', + self::DISALLOW => 'Disallowed value was matched', + self::DIVISIBLE_BY => 'Is not divisible by %d', + self::ENUM => 'Does not have a value in the enumeration %s', + self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d', + self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d', + self::FORMAT_COLOR => 'Invalid color', + self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD', + self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', + self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch', + self::FORMAT_EMAIL => 'Invalid email', + self::FORMAT_HOSTNAME => 'Invalid hostname', + self::FORMAT_IP => 'Invalid IP address', + self::FORMAT_PHONE => 'Invalid phone number', + self::FORMAT_REGEX=> 'Invalid regex format %s', + self::FORMAT_STYLE => 'Invalid style', + self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', + self::FORMAT_URL => 'Invalid URL format', + self::LENGTH_MAX => 'Must be at most %d characters long', + self::LENGTH_MIN => 'Must be at least %d characters long', + self::MAX_ITEMS => 'There must be a maximum of %d items in the array', + self::MAXIMUM => 'Must have a maximum value less than or equal to %d', + self::MIN_ITEMS => 'There must be a minimum of %d items in the array', + self::MINIMUM => 'Must have a minimum value greater than or equal to %d', + self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', + self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum', + self::MULTIPLE_OF => 'Must be a multiple of %d', + self::NOT => 'Matched a schema which it should not', + self::ONE_OF => 'Failed to match exactly one schema', + self::REQUIRED => 'The property %s is required', + self::REQUIRED_D3 => 'Is missing and it is required', + self::REQUIRES => 'The presence of the property %s requires that %s also be present', + self::PATTERN => 'Does not match the regex pattern %s', + self::PREGEX_INVALID => 'The pattern %s is invalid', + self::PROPERTIES_MIN => 'Must contain a minimum of %d properties', + self::PROPERTIES_MAX => 'Must contain no more than %d properties', + self::TYPE => '%s value found, but %s is required', + self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array' + ); + + if (!isset($messages[$name])) { + throw new InvalidArgumentException('Missing error message for ' . $name); + } + + return $messages[$name]; + } +} diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index ef1bdc54..bc608f6d 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\ValidationException; @@ -36,23 +37,30 @@ public function __construct(Factory $factory = null) $this->factory = $factory ?: new Factory(); } - public function addError(JsonPointer $path = null, $message, $constraint = '', array $more = null) + public function addError(ConstraintError $constraint, JsonPointer $path = null, array $more = array()) { + $message = $constraint ? $constraint->getMessage() : ''; + $name = $constraint ? $constraint->getValue() : ''; $error = array( 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), 'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'), - 'message' => $message, - 'constraint' => $constraint, + 'message' => ucfirst(vsprintf($message, array_map(function ($val) { + if (is_scalar($val)) { + return $val; + } + + return json_encode($val); + }, array_values($more)))), + 'constraint' => array( + 'name' => $name, + 'params' => $more + ) ); if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message'])); } - if (is_array($more) && count($more) > 0) { - $error += $more; - } - $this->errors[] = $error; } diff --git a/src/JsonSchema/Constraints/CollectionConstraint.php b/src/JsonSchema/Constraints/CollectionConstraint.php index 3c594b3c..a4227866 100644 --- a/src/JsonSchema/Constraints/CollectionConstraint.php +++ b/src/JsonSchema/Constraints/CollectionConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -26,12 +27,12 @@ public function check(&$value, $schema = null, JsonPointer $path = null, $i = nu { // Verify minItems if (isset($schema->minItems) && count($value) < $schema->minItems) { - $this->addError($path, 'There must be a minimum of ' . $schema->minItems . ' items in the array', 'minItems', array('minItems' => $schema->minItems)); + $this->addError(ConstraintError::MIN_ITEMS(), $path, array('minItems' => $schema->minItems)); } // Verify maxItems if (isset($schema->maxItems) && count($value) > $schema->maxItems) { - $this->addError($path, 'There must be a maximum of ' . $schema->maxItems . ' items in the array', 'maxItems', array('maxItems' => $schema->maxItems)); + $this->addError(ConstraintError::MAX_ITEMS(), $path, array('maxItems' => $schema->maxItems)); } // Verify uniqueItems @@ -43,7 +44,7 @@ public function check(&$value, $schema = null, JsonPointer $path = null, $i = nu }, $value); } if (count(array_unique($unique)) != count($value)) { - $this->addError($path, 'There are no duplicates allowed in the array', 'uniqueItems'); + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); } } @@ -124,7 +125,14 @@ protected function validateItems(&$value, $schema = null, JsonPointer $path = nu $this->checkUndefined($v, $schema->additionalItems, $path, $k); } else { $this->addError( - $path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems)); + ConstraintError::ADDITIONAL_ITEMS(), + $path, + array( + 'item' => $i, + 'property' => $k, + 'additionalItems' => $schema->additionalItems + ) + ); } } else { // Should be valid against an empty schema diff --git a/src/JsonSchema/Constraints/ConstraintInterface.php b/src/JsonSchema/Constraints/ConstraintInterface.php index 442268e6..6007c099 100644 --- a/src/JsonSchema/Constraints/ConstraintInterface.php +++ b/src/JsonSchema/Constraints/ConstraintInterface.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -35,12 +36,11 @@ public function addErrors(array $errors); /** * adds an error * - * @param JsonPointer|null $path - * @param string $message - * @param string $constraint the constraint/rule that is broken, e.g.: 'minLength' - * @param array $more more array elements to add to the error + * @param ConstraintError $constraint the constraint/rule that is broken, e.g.: ConstraintErrors::LENGTH_MIN() + * @param JsonPointer |null $path + * @param array $more more array elements to add to the error */ - public function addError(JsonPointer $path = null, $message, $constraint='', array $more = null); + public function addError(ConstraintError $constraint, JsonPointer $path = null, array $more = array()); /** * checks if the validator has not raised errors diff --git a/src/JsonSchema/Constraints/EnumConstraint.php b/src/JsonSchema/Constraints/EnumConstraint.php index 0fd2b6a0..5e401228 100644 --- a/src/JsonSchema/Constraints/EnumConstraint.php +++ b/src/JsonSchema/Constraints/EnumConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -49,6 +50,6 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = } } - $this->addError($path, 'Does not have a value in the enumeration ' . json_encode($schema->enum), 'enum', array('enum' => $schema->enum)); + $this->addError(ConstraintError::ENUM(), $path, array('enum' => $schema->enum)); } } diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index ad192b5b..5fbd2c40 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Rfc3339; @@ -33,49 +34,67 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = switch ($schema->format) { case 'date': if (!$date = $this->validateDateTime($element, 'Y-m-d')) { - $this->addError($path, sprintf('Invalid date %s, expected format YYYY-MM-DD', json_encode($element)), 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_DATE(), $path, array( + 'date' => $element, + 'format' => $schema->format + ) + ); } break; case 'time': if (!$this->validateDateTime($element, 'H:i:s')) { - $this->addError($path, sprintf('Invalid time %s, expected format hh:mm:ss', json_encode($element)), 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_TIME(), $path, array( + 'time' => json_encode($element), + 'format' => $schema->format, + ) + ); } break; case 'date-time': if (null === Rfc3339::createFromString($element)) { - $this->addError($path, sprintf('Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', json_encode($element)), 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, array( + 'dateTime' => json_encode($element), + 'format' => $schema->format + ) + ); } break; case 'utc-millisec': if (!$this->validateDateTime($element, 'U')) { - $this->addError($path, sprintf('Invalid time %s, expected integer of milliseconds since Epoch', json_encode($element)), 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, array( + 'value' => $element, + 'format' => $schema->format)); } break; case 'regex': if (!$this->validateRegex($element)) { - $this->addError($path, 'Invalid regex format ' . $element, 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_REGEX(), $path, array( + 'value' => $element, + 'format' => $schema->format + ) + ); } break; case 'color': if (!$this->validateColor($element)) { - $this->addError($path, 'Invalid color', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_COLOR(), $path, array('format' => $schema->format)); } break; case 'style': if (!$this->validateStyle($element)) { - $this->addError($path, 'Invalid style', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_STYLE(), $path, array('format' => $schema->format)); } break; case 'phone': if (!$this->validatePhone($element)) { - $this->addError($path, 'Invalid phone number', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_PHONE(), $path, array('format' => $schema->format)); } break; @@ -99,34 +118,34 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = $validURL = null; } if ($validURL === null) { - $this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_URL(), $path, array('format' => $schema->format)); } } break; case 'email': if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)) { - $this->addError($path, 'Invalid email', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_EMAIL(), $path, array('format' => $schema->format)); } break; case 'ip-address': case 'ipv4': if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { - $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_IP(), $path, array('format' => $schema->format)); } break; case 'ipv6': if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { - $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_IP(), $path, array('format' => $schema->format)); } break; case 'host-name': case 'hostname': if (!$this->validateHostname($element)) { - $this->addError($path, 'Invalid hostname', 'format', array('format' => $schema->format)); + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, array('format' => $schema->format)); } break; diff --git a/src/JsonSchema/Constraints/NumberConstraint.php b/src/JsonSchema/Constraints/NumberConstraint.php index 5a809774..aeea855d 100644 --- a/src/JsonSchema/Constraints/NumberConstraint.php +++ b/src/JsonSchema/Constraints/NumberConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -28,40 +29,40 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = if (isset($schema->exclusiveMinimum)) { if (isset($schema->minimum)) { if ($schema->exclusiveMinimum && $element <= $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'exclusiveMinimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, array('minimum' => $schema->minimum)); } elseif ($element < $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::MINIMUM(), $path, array('minimum' => $schema->minimum)); } } else { - $this->addError($path, 'Use of exclusiveMinimum requires presence of minimum', 'missingMinimum'); + $this->addError(ConstraintError::MISSING_MINIMUM(), $path); } } elseif (isset($schema->minimum) && $element < $schema->minimum) { - $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); + $this->addError(ConstraintError::MINIMUM(), $path, array('minimum' => $schema->minimum)); } // Verify maximum if (isset($schema->exclusiveMaximum)) { if (isset($schema->maximum)) { if ($schema->exclusiveMaximum && $element >= $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'exclusiveMaximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, array('maximum' => $schema->maximum)); } elseif ($element > $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::MAXIMUM(), $path, array('maximum' => $schema->maximum)); } } else { - $this->addError($path, 'Use of exclusiveMaximum requires presence of maximum', 'missingMaximum'); + $this->addError(ConstraintError::MISSING_MAXIMUM(), $path); } } elseif (isset($schema->maximum) && $element > $schema->maximum) { - $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); + $this->addError(ConstraintError::MAXIMUM(), $path, array('maximum' => $schema->maximum)); } // Verify divisibleBy - Draft v3 if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) { - $this->addError($path, 'Is not divisible by ' . $schema->divisibleBy, 'divisibleBy', array('divisibleBy' => $schema->divisibleBy)); + $this->addError(ConstraintError::DIVISIBLE_BY(), $path, array('divisibleBy' => $schema->divisibleBy)); } // Verify multipleOf - Draft v4 if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) { - $this->addError($path, 'Must be a multiple of ' . $schema->multipleOf, 'multipleOf', array('multipleOf' => $schema->multipleOf)); + $this->addError(ConstraintError::MULTIPLE_OF(), $path, array('multipleOf' => $schema->multipleOf)); } $this->checkFormat($element, $schema, $path, $i); diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 5ea94f7d..d360a659 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -57,7 +58,7 @@ public function validatePatternProperties($element, JsonPointer $path = null, $p // Validate the pattern before using it to test for matches if (@preg_match($delimiter . $pregex . $delimiter . 'u', '') === false) { - $this->addError($path, 'The pattern "' . $pregex . '" is invalid', 'pregex', array('pregex' => $pregex)); + $this->addError(ConstraintError::PREGEX_INVALID(), $path, array('pregex' => $pregex)); continue; } foreach ($element as $i => $value) { @@ -89,7 +90,7 @@ public function validateElement($element, $matches, $objectDefinition = null, Js // no additional properties allowed if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { - $this->addError($path, 'The property ' . $i . ' is not defined and the definition does not allow additional properties', 'additionalProp'); + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, array('property' => $i)); } // additional properties defined @@ -104,7 +105,10 @@ public function validateElement($element, $matches, $objectDefinition = null, Js // property requires presence of another $require = $this->getProperty($definition, 'requires'); if ($require && !$this->getProperty($element, $require)) { - $this->addError($path, 'The presence of the property ' . $i . ' requires that ' . $require . ' also be present', 'requires'); + $this->addError(ConstraintError::REQUIRES(), $path, array( + 'property' => $i, + 'requiredProperty' => $require + )); } $property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined')); @@ -168,13 +172,13 @@ protected function validateMinMaxConstraint($element, $objectDefinition, JsonPoi // Verify minimum number of properties if (isset($objectDefinition->minProperties) && !is_object($objectDefinition->minProperties)) { if ($this->getTypeCheck()->propertyCount($element) < $objectDefinition->minProperties) { - $this->addError($path, 'Must contain a minimum of ' . $objectDefinition->minProperties . ' properties', 'minProperties', array('minProperties' => $objectDefinition->minProperties)); + $this->addError(ConstraintError::PROPERTIES_MIN(), $path, array('minProperties' => $objectDefinition->minProperties)); } } // Verify maximum number of properties if (isset($objectDefinition->maxProperties) && !is_object($objectDefinition->maxProperties)) { if ($this->getTypeCheck()->propertyCount($element) > $objectDefinition->maxProperties) { - $this->addError($path, 'Must contain no more than ' . $objectDefinition->maxProperties . ' properties', 'maxProperties', array('maxProperties' => $objectDefinition->maxProperties)); + $this->addError(ConstraintError::PROPERTIES_MAX(), $path, array('maxProperties' => $objectDefinition->maxProperties)); } } } diff --git a/src/JsonSchema/Constraints/StringConstraint.php b/src/JsonSchema/Constraints/StringConstraint.php index 5b15de7a..4790a040 100644 --- a/src/JsonSchema/Constraints/StringConstraint.php +++ b/src/JsonSchema/Constraints/StringConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; /** @@ -26,21 +27,21 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = { // Verify maxLength if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) { - $this->addError($path, 'Must be at most ' . $schema->maxLength . ' characters long', 'maxLength', array( + $this->addError(ConstraintError::LENGTH_MAX(), $path, array( 'maxLength' => $schema->maxLength, )); } //verify minLength if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) { - $this->addError($path, 'Must be at least ' . $schema->minLength . ' characters long', 'minLength', array( + $this->addError(ConstraintError::LENGTH_MIN(), $path, array( 'minLength' => $schema->minLength, )); } // Verify a regex pattern if (isset($schema->pattern) && !preg_match('#' . str_replace('#', '\\#', $schema->pattern) . '#u', $element)) { - $this->addError($path, 'Does not match the regex pattern ' . $schema->pattern, 'pattern', array( + $this->addError(ConstraintError::PATTERN(), $path, array( 'pattern' => $schema->pattern, )); } diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index 0ef32843..a1db1d3a 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; use UnexpectedValueException as StandardUnexpectedValueException; @@ -60,8 +61,10 @@ public function check(&$value = null, $schema = null, JsonPointer $path = null, $this->validateTypeNameWording($type); $wording[] = self::$wording[$type]; } - $this->addError($path, ucwords(gettype($value)) . ' value found, but ' . - $this->implodeWith($wording, ', ', 'or') . ' is required', 'type'); + $this->addError(ConstraintError::TYPE(), $path, array( + 'expected' => gettype($value), + 'found' => $this->implodeWith($wording, ', ', 'or') + )); } } diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 147e5bc3..264a8d36 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; use JsonSchema\Uri\UriResolver; @@ -160,16 +161,17 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer foreach ($schema->required as $required) { if (!$this->getTypeCheck()->propertyExists($value, $required)) { $this->addError( - $this->incrementPath($path ?: new JsonPointer(''), $required), - 'The property ' . $required . ' is required', - 'required' + ConstraintError::REQUIRED(), + $this->incrementPath($path ?: new JsonPointer(''), $required), array( + 'property' => $required + ) ); } } } elseif (isset($schema->required) && !is_array($schema->required)) { // Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true} if ($schema->required && $value instanceof self) { - $this->addError($path, 'Is missing and it is required', 'required'); + $this->addError(ConstraintError::REQUIRED_D3(), $path); } } } @@ -189,7 +191,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // if no new errors were raised it must be a disallowed value if (count($this->getErrors()) == count($initErrors)) { - $this->addError($path, 'Disallowed value was matched', 'disallow'); + $this->addError(ConstraintError::DISALLOW(), $path); } else { $this->errors = $initErrors; } @@ -201,7 +203,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // if no new errors were raised then the instance validated against the "not" schema if (count($this->getErrors()) == count($initErrors)) { - $this->addError($path, 'Matched a schema which it should not', 'not'); + $this->addError(ConstraintError::NOT(), $path); } else { $this->errors = $initErrors; } @@ -236,7 +238,7 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); } if (!$isValid) { - $this->addError($path, 'Failed to match all schemas', 'allOf'); + $this->addError(ConstraintError::ALL_OF(), $path); } } @@ -251,7 +253,7 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i } } if (!$isValid) { - $this->addError($path, 'Failed to match at least one schema', 'anyOf'); + $this->addError(ConstraintError::ANY_OF(), $path); } else { $this->errors = $startErrors; } @@ -271,7 +273,7 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i } if ($matchedSchemas !== 1) { $this->addErrors(array_merge($allErrors, $startErrors)); - $this->addError($path, 'Failed to match exactly one schema', 'oneOf'); + $this->addError(ConstraintError::ONE_OF(), $path); } else { $this->errors = $startErrors; } @@ -293,13 +295,19 @@ protected function validateDependencies($value, $dependencies, JsonPointer $path if (is_string($dependency)) { // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { - $this->addError($path, "$key depends on $dependency and $dependency is missing", 'dependencies'); + $this->addError(ConstraintError::DEPENDENCIES(), $path, array( + 'key' => $key, + 'dependency' => $dependency + )); } } elseif (is_array($dependency)) { // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} foreach ($dependency as $d) { if (!$this->getTypeCheck()->propertyExists($value, $d)) { - $this->addError($path, "$key depends on $d and $d is missing", 'dependencies'); + $this->addError(ConstraintError::DEPENDENCIES(), $path, array( + 'key' => $key, + 'dependency' => $dependency + )); } } } elseif (is_object($dependency)) { diff --git a/tests/Constraints/AdditionalPropertiesTest.php b/tests/Constraints/AdditionalPropertiesTest.php index 4d68654b..5ecfa0a7 100644 --- a/tests/Constraints/AdditionalPropertiesTest.php +++ b/tests/Constraints/AdditionalPropertiesTest.php @@ -36,7 +36,12 @@ public function getInvalidTests() 'property' => '', 'pointer' => '', 'message' => 'The property additionalProp is not defined and the definition does not allow additional properties', - 'constraint' => 'additionalProp', + 'constraint' => array( + 'name' => 'additionalProp', + 'params' => array( + 'property' => 'additionalProp' + ) + ) ) ) ), diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index c36ba29e..721195f5 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -75,19 +75,34 @@ public function getInvalidTests() 'property' => 'prop2', 'pointer' => '/prop2', 'message' => 'Array value found, but a string is required', - 'constraint' => 'type', + 'constraint' => array( + 'name' => 'type', + 'params' => array( + 'expected' => 'array', + 'found' => 'a string' + ) + ) ), array( 'property' => 'prop2', 'pointer' => '/prop2', 'message' => 'Array value found, but a number is required', - 'constraint' => 'type', + 'constraint' => array( + 'name' => 'type', + 'params' => array( + 'expected' => 'array', + 'found' => 'a number' + ) + ) ), array( 'property' => 'prop2', 'pointer' => '/prop2', 'message' => 'Failed to match exactly one schema', - 'constraint' => 'oneOf', + 'constraint' => array( + 'name' => 'oneOf', + 'params' => array() + ) ), ), ), diff --git a/tests/Constraints/PointerTest.php b/tests/Constraints/PointerTest.php index ca378e3d..95c4c7b8 100644 --- a/tests/Constraints/PointerTest.php +++ b/tests/Constraints/PointerTest.php @@ -88,25 +88,45 @@ public function testVariousPointers() 'property' => 'prop1', 'pointer' => '/prop1', 'message' => 'The property prop1 is required', - 'constraint' => 'required' + 'constraint' => array( + 'name' => 'required', + 'params' => array( + 'property' => 'prop1' + ) + ) ), array( 'property' => 'prop2.prop2.1', 'pointer' => '/prop2/prop2.1', 'message' => 'The property prop2.1 is required', - 'constraint' => 'required' + 'constraint' => array( + 'name' => 'required', + 'params' => array( + 'property' => 'prop2.1' + ) + ) ), array( 'property' => 'prop3.prop3/1.prop3/1.1', 'pointer' => '/prop3/prop3~11/prop3~11.1', 'message' => 'The property prop3/1.1 is required', - 'constraint' => 'required' + 'constraint' => array( + 'name' => 'required', + 'params' => array( + 'property' => 'prop3/1.1' + ) + ) ), array( 'property' => 'prop4[0].prop4-child', 'pointer' => '/prop4/0/prop4-child', 'message' => 'The property prop4-child is required', - 'constraint' => 'required' + 'constraint' => array( + 'name' => 'required', + 'params' => array( + 'property' => 'prop4-child' + ) + ) ) ), $validator->getErrors() From a4da6ba7a7735595d4a19e1723813f6ad2a2ce0b Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:02:43 +1300 Subject: [PATCH 03/20] Add use line for InvalidArgumentException (#370) Fixes issue #369 --- src/JsonSchema/ConstraintError.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index d7b73346..8dcd2d50 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -2,6 +2,8 @@ namespace JsonSchema; +use JsonSchema\Exception\InvalidArgumentException; + class ConstraintError extends \MabeEnum\Enum { const ADDITIONAL_ITEMS = 'additionalItems'; From 14a04dc1b4060ed40f1f5b1b3dc3f6647cbcb7ab Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:03:20 +1300 Subject: [PATCH 04/20] Add use line for InvalidArgumentException & adjust scope (#372) Fixes issue #371 --- src/JsonSchema/Entity/JsonPointer.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/JsonSchema/Entity/JsonPointer.php b/src/JsonSchema/Entity/JsonPointer.php index 31c753ba..8bb71ecf 100644 --- a/src/JsonSchema/Entity/JsonPointer.php +++ b/src/JsonSchema/Entity/JsonPointer.php @@ -9,6 +9,8 @@ namespace JsonSchema\Entity; +use JsonSchema\Exception\InvalidArgumentException; + /** * @package JsonSchema\Entity * @@ -25,12 +27,12 @@ class JsonPointer /** * @param string $value * - * @throws \InvalidArgumentException when $value is not a string + * @throws InvalidArgumentException when $value is not a string */ public function __construct($value) { if (!is_string($value)) { - throw new \InvalidArgumentException('Ref value must be a string'); + throw new InvalidArgumentException('Ref value must be a string'); } $splitRef = explode('#', $value, 2); From 5de03d43afd0d410bdee7e12216bbc5ef179351d Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:05:20 +1300 Subject: [PATCH 05/20] Revert "An email is a string, not much else." (#373) This reverts commit 73ef463f5ee1d50db9f01069c5a060a3d354888a. 'email' is only a valid type attribute in draft-03 (later versions of the spec explicitly limit the value of type to the core primitive types), and this code doesn't validate email addresses anyway. IMO we should be validating it properly or not at all, and noting this went away after draft-03 my opinion is on the not-at-all side of the fence. Note that 'email' is *never* defined as a spec type, in any version - it just slips in under the radar of the draft-03 language which allows users to put arbitrary things in the type field. --- src/JsonSchema/Constraints/TypeConstraint.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index a1db1d3a..096f5485 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -206,10 +206,6 @@ protected function validateType(&$value, $type) return is_string($value); } - if ('email' === $type) { - return is_string($value); - } - if ('null' === $type) { return is_null($value); } From b1ee0e8fd71b31f75ef288eab0810c680dbfff60 Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:06:44 +1300 Subject: [PATCH 06/20] [BUGFIX] Add provided schema under a dummy / internal URI (fixes #376) (#378) * Add provided schema under a dummy / internal URI (fixes #376) In order to resolve internal $ref references within a user-provided schema, SchemaStorage needs to know about the schema. As user-supplied schemas do not have an associated URI, use a dummy / internal one instead. * Remove dangling use * Change URI to class constant on SchemaStorage --- src/JsonSchema/SchemaStorage.php | 7 ++++++- src/JsonSchema/Validator.php | 4 ++++ tests/SchemaStorageTest.php | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index 6450572e..55d7985a 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -10,6 +10,8 @@ class SchemaStorage implements SchemaStorageInterface { + const INTERNAL_PROVIDED_SCHEMA_URI = 'internal://provided-schema'; + protected $uriRetriever; protected $uriResolver; protected $schemas = array(); @@ -43,7 +45,10 @@ public function getUriResolver() */ public function addSchema($id, $schema = null) { - if (is_null($schema)) { + if (is_null($schema) && $id !== self::INTERNAL_PROVIDED_SCHEMA_URI) { + // if the schema was user-provided to Validator and is still null, then assume this is + // what the user intended, as there's no way for us to retrieve anything else. User-supplied + // schemas do not have an associated URI when passed via Validator::validate(). $schema = $this->uriRetriever->retrieve($id); } $objectIterator = new ObjectIterator($schema); diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index e2a919bc..9f44e512 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -12,6 +12,7 @@ use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Constraints\Constraint; use JsonSchema\Exception\InvalidConfigException; +use JsonSchema\SchemaStorage; /** * A JsonSchema Constraint @@ -41,6 +42,9 @@ public function validate(&$value, $schema = null, $checkMode = null) $this->factory->setConfig($checkMode); } + // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution + $this->factory->getSchemaStorage()->addSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema); + $validator = $this->factory->createInstanceFor('schema'); $validator->check($value, $schema); diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index c3388bf4..92e1d5c3 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -11,6 +11,7 @@ use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriRetriever; +use JsonSchema\Validator; use Prophecy\Argument; class SchemaStorageTest extends \PHPUnit_Framework_TestCase @@ -31,6 +32,15 @@ public function testResolveRef() ); } + public function testResolveTopRef() + { + $input = json_decode('{"propertyOne":"notANumber"}'); + $schema = json_decode('{"$ref":"#/definition","definition":{"properties":{"propertyOne":{"type":"number"}}}}'); + $v = new Validator(); + $v->validate($input, $schema); + $this->assertFalse($v->isValid()); + } + /** * @depends testResolveRef */ From d3f7740e6a82e7e05241716f902b22c6d448d9da Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Thu, 16 Mar 2017 21:09:47 -0700 Subject: [PATCH 07/20] add quiet option (#382) * add quiet option * use verbose instead of quiet * add quiet option * always output dump-schema * always output dump-schema-url * fix typo and ws --- bin/validate-json | 124 ++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/bin/validate-json b/bin/validate-json index e9c18095..421ebcde 100755 --- a/bin/validate-json +++ b/bin/validate-json @@ -17,7 +17,6 @@ function __autoload($className) { $className = ltrim($className, '\\'); $fileName = ''; - $namespace = ''; if ($lastNsPos = strrpos($className, '\\')) { $namespace = substr($className, 0, $lastNsPos); $className = substr($className, $lastNsPos + 1); @@ -29,6 +28,49 @@ function __autoload($className) } } +// support running this tool from git checkout +if (is_dir(__DIR__ . '/../src/JsonSchema')) { + set_include_path(__DIR__ . '/../src' . PATH_SEPARATOR . get_include_path()); +} + +$arOptions = array(); +$arArgs = array(); +array_shift($argv);//script itself +foreach ($argv as $arg) { + if ($arg{0} == '-') { + $arOptions[$arg] = true; + } else { + $arArgs[] = $arg; + } +} + +if (count($arArgs) == 0 + || isset($arOptions['--help']) || isset($arOptions['-h']) +) { + echo <<getMessage() . "\n"; + output("Error loading JSON schema file\n"); + output($urlSchema . "\n"); + output($e->getMessage() . "\n"); exit(2); } $refResolver = new JsonSchema\SchemaStorage($retriever, $resolver); @@ -221,17 +233,19 @@ try { $validator->check($data, $schema); if ($validator->isValid()) { - echo "OK. The supplied JSON validates against the schema.\n"; + if(isset($arOptions['--verbose'])) { + output("OK. The supplied JSON validates against the schema.\n"); + } } else { - echo "JSON does not validate. Violations:\n"; + output("JSON does not validate. Violations:\n"); foreach ($validator->getErrors() as $error) { - echo sprintf("[%s] %s\n", $error['property'], $error['message']); + output(sprintf("[%s] %s\n", $error['property'], $error['message'])); } exit(23); } } catch (Exception $e) { - echo "JSON does not validate. Error:\n"; - echo $e->getMessage() . "\n"; - echo "Error code: " . $e->getCode() . "\n"; + output("JSON does not validate. Error:\n"); + output($e->getMessage() . "\n"); + output("Error code: " . $e->getCode() . "\n"); exit(24); } From b8d26116b6fa48cca3280a608b42a8a6d9d2121b Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:10:40 +1300 Subject: [PATCH 08/20] Add option to disable validation of "format" constraint (#383) --- README.md | 1 + src/JsonSchema/Constraints/Constraint.php | 1 + src/JsonSchema/Constraints/FormatConstraint.php | 2 +- tests/Constraints/FormatTest.php | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 376c2e27..4de425f5 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `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_EXCEPTIONS` | Throw an exception immediately if validation fails | +| `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` will modify your original data. diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 7fa0a99a..2283ac12 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -30,6 +30,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_COERCE_TYPES = 0x00000004; const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; + const CHECK_MODE_DISABLE_FORMAT = 0x00000020; /** * Bubble down the path diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index 5fbd2c40..87c1dc57 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -27,7 +27,7 @@ class FormatConstraint extends Constraint */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { - if (!isset($schema->format)) { + if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { return; } diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index 73de5784..5cca9c01 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -9,6 +9,8 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\Constraint; +use JsonSchema\Constraints\Factory; use JsonSchema\Constraints\FormatConstraint; class FormatTest extends BaseTestCase @@ -76,6 +78,21 @@ public function testInvalidFormat($string, $format) $this->assertEquals(1, count($validator->getErrors()), 'Expected 1 error'); } + /** + * @dataProvider getInvalidFormats + */ + public function testDisabledFormat($string, $format) + { + $factory = new Factory(); + $validator = new FormatConstraint($factory); + $schema = new \stdClass(); + $schema->format = $format; + $factory->addConfig(Constraint::CHECK_MODE_DISABLE_FORMAT); + + $validator->check($string, $schema); + $this->assertEmpty($validator->getErrors()); + } + public function getValidFormats() { return array( From 705dcbd23d659f192b040dc2b4ec934e340face1 Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:21:32 +1300 Subject: [PATCH 09/20] Add more unit tests (#366) * Add test coverage for coercion API * Complete test coverage for SchemaStorage * Add test coverage for ObjectIterator * Add tests for ConstraintError * Add exception test for JsonPointer * MabeEnum\Enum appears to use singletons - add testing const * Don't check this line for coverage mbstring is on all test platforms, so this line will never be reached. * Add test for TypeConstraint::validateTypeNameWording() * Add test for exception on TypeConstraint::validateType() * PHPunit doesn't like an explanation with its @codeCoverageIgnore... * Add various tests for UriRetriever * Add tests for FileGetContents * Add tests for JsonSchema\Uri\Retrievers\Curl * Add missing bad-syntax test file * Restrict ignore to the exception line only * Fix exception scope --- src/JsonSchema/ConstraintError.php | 2 + .../Constraints/StringConstraint.php | 3 +- src/JsonSchema/Uri/Retrievers/Curl.php | 4 +- .../Uri/Retrievers/FileGetContents.php | 6 +- tests/ConstraintErrorTest.php | 32 +++++++ tests/Constraints/CoerciveTest.php | 9 ++ tests/Constraints/TypeTest.php | 27 ++++++ tests/Entity/JsonPointerTest.php | 9 ++ tests/Iterators/ObjectIteratorTest.php | 89 +++++++++++++++++++ tests/SchemaStorageTest.php | 14 +++ tests/Uri/Retrievers/CurlTest.php | 57 ++++++++++++ tests/Uri/Retrievers/FileGetContentsTest.php | 79 ++++++++++++---- tests/Uri/UriRetrieverTest.php | 84 +++++++++++++++++ tests/fixtures/bad-syntax.json | 1 + 14 files changed, 396 insertions(+), 20 deletions(-) create mode 100644 tests/ConstraintErrorTest.php create mode 100644 tests/Iterators/ObjectIteratorTest.php create mode 100644 tests/Uri/Retrievers/CurlTest.php create mode 100644 tests/fixtures/bad-syntax.json diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index 8dcd2d50..2af542d8 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -33,6 +33,7 @@ class ConstraintError extends \MabeEnum\Enum const MAXIMUM = 'maximum'; const MIN_ITEMS = 'minItems'; const MINIMUM = 'minimum'; + const MISSING_ERROR = 'missingError'; const MISSING_MAXIMUM = 'missingMaximum'; const MISSING_MINIMUM = 'missingMinimum'; const MAX_ITEMS = 'maxItems'; @@ -83,6 +84,7 @@ public function getMessage() self::MINIMUM => 'Must have a minimum value greater than or equal to %d', self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum', + /*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/ self::MULTIPLE_OF => 'Must be a multiple of %d', self::NOT => 'Matched a schema which it should not', self::ONE_OF => 'Failed to match exactly one schema', diff --git a/src/JsonSchema/Constraints/StringConstraint.php b/src/JsonSchema/Constraints/StringConstraint.php index 4790a040..b3bdfbf7 100644 --- a/src/JsonSchema/Constraints/StringConstraint.php +++ b/src/JsonSchema/Constraints/StringConstraint.php @@ -55,6 +55,7 @@ private function strlen($string) return mb_strlen($string, mb_detect_encoding($string)); } - return strlen($string); + // mbstring is present on all test platforms, so strlen() can be ignored for coverage + return strlen($string); // @codeCoverageIgnore } } diff --git a/src/JsonSchema/Uri/Retrievers/Curl.php b/src/JsonSchema/Uri/Retrievers/Curl.php index a4125aa6..81c86037 100644 --- a/src/JsonSchema/Uri/Retrievers/Curl.php +++ b/src/JsonSchema/Uri/Retrievers/Curl.php @@ -9,6 +9,7 @@ namespace JsonSchema\Uri\Retrievers; +use JsonSchema\Exception\RuntimeException; use JsonSchema\Validator; /** @@ -23,7 +24,8 @@ class Curl extends AbstractRetriever public function __construct() { if (!function_exists('curl_init')) { - throw new \RuntimeException('cURL not installed'); + // Cannot test this, because curl_init is present on all test platforms plus mock + throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore } } diff --git a/src/JsonSchema/Uri/Retrievers/FileGetContents.php b/src/JsonSchema/Uri/Retrievers/FileGetContents.php index 7f0c399a..7019814f 100644 --- a/src/JsonSchema/Uri/Retrievers/FileGetContents.php +++ b/src/JsonSchema/Uri/Retrievers/FileGetContents.php @@ -50,8 +50,10 @@ public function retrieve($uri) $this->messageBody = $response; if (!empty($http_response_header)) { - $this->fetchContentType($http_response_header); - } else { + // $http_response_header cannot be tested, because it's defined in the method's local scope + // See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info. + $this->fetchContentType($http_response_header); // @codeCoverageIgnore + } else { // @codeCoverageIgnore // Could be a "file://" url or something else - fake up the response $this->contentType = null; } diff --git a/tests/ConstraintErrorTest.php b/tests/ConstraintErrorTest.php new file mode 100644 index 00000000..5efb7b66 --- /dev/null +++ b/tests/ConstraintErrorTest.php @@ -0,0 +1,32 @@ +assertEquals('Failed to match all schemas', $e->getMessage()); + } + + public function testGetInvalidMessage() + { + $e = ConstraintError::MISSING_ERROR(); + + $this->setExpectedException( + '\JsonSchema\Exception\InvalidArgumentException', + 'Missing error message for missingError' + ); + $e->getMessage(); + } +} diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index cb5c5518..36c0cf6b 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -116,6 +116,15 @@ public function testInvalidCoerceCasesUsingAssoc($input, $schema, $errors = arra $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); } + public function testCoerceAPI() + { + $input = json_decode('{"propertyOne": "10"}'); + $schema = json_decode('{"properties":{"propertyOne":{"type":"number"}}}'); + $v = new Validator(); + $v->coerce($input, $schema); + $this->assertEquals('{"propertyOne":10}', json_encode($input)); + } + public function getValidCoerceTests() { return array( diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index df8d6dd1..24138478 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -92,4 +92,31 @@ private function assertTypeConstraintError($expected, TypeConstraint $actual) $this->assertEquals($expected, $actualMessage); // first equal for the diff $this->assertSame($expected, $actualMessage); // the same for the strictness } + + public function testValidateTypeNameWording() + { + $t = new TypeConstraint(); + $r = new \ReflectionObject($t); + $m = $r->getMethod('validateTypeNameWording'); + $m->setAccessible(true); + + $this->setExpectedException( + '\UnexpectedValueException', + "No wording for 'notAValidTypeName' available, expected wordings are: [an integer, a number, a boolean, an object, an array, a string, a null]" + ); + $m->invoke($t, 'notAValidTypeName'); + } + + public function testValidateTypeException() + { + $t = new TypeConstraint(); + $data = new \StdClass(); + $schema = json_decode('{"type": "notAValidTypeName"}'); + + $this->setExpectedException( + 'JsonSchema\Exception\InvalidArgumentException', + 'object is an invalid type for notAValidTypeName' + ); + $t->check($data, $schema); + } } diff --git a/tests/Entity/JsonPointerTest.php b/tests/Entity/JsonPointerTest.php index 6a5ff4bf..65859895 100644 --- a/tests/Entity/JsonPointerTest.php +++ b/tests/Entity/JsonPointerTest.php @@ -109,4 +109,13 @@ public function testJsonPointerWithPropertyPaths() $this->assertEquals(array('~definitions/general', '%custom%'), $modified->getPropertyPaths()); $this->assertEquals('#/~0definitions~1general/%25custom%25', $modified->getPropertyPathAsString()); } + + public function testCreateWithInvalidValue() + { + $this->setExpectedException( + '\JsonSchema\Exception\InvalidArgumentException', + 'Ref value must be a string' + ); + new JsonPointer(null); + } } diff --git a/tests/Iterators/ObjectIteratorTest.php b/tests/Iterators/ObjectIteratorTest.php new file mode 100644 index 00000000..703df833 --- /dev/null +++ b/tests/Iterators/ObjectIteratorTest.php @@ -0,0 +1,89 @@ +testObject = (object) array( + 'subOne' => (object) array( + 'propertyOne' => 'valueOne', + 'propertyTwo' => 'valueTwo', + 'propertyThree' => 'valueThree' + ), + 'subTwo' => (object) array( + 'propertyFour' => 'valueFour', + 'subThree' => (object) array( + 'propertyFive' => 'valueFive', + 'propertySix' => 'valueSix' + ) + ), + 'propertySeven' => 'valueSeven' + ); + } + + public function testCreate() + { + $i = new ObjectIterator($this->testObject); + + $this->assertInstanceOf('\JsonSchema\Iterator\ObjectIterator', $i); + } + + public function testInitialState() + { + $i = new ObjectIterator($this->testObject); + + $this->assertEquals($this->testObject, $i->current()); + } + + public function testCount() + { + $i = new ObjectIterator($this->testObject); + + $this->assertEquals(4, $i->count()); + } + + public function testKey() + { + $i = new ObjectIterator($this->testObject); + + while ($i->key() != 2) { + $i->next(); + } + + $this->assertEquals($this->testObject->subTwo->subThree, $i->current()); + } + + public function testAlwaysObjects() + { + $i= new ObjectIterator($this->testObject); + + foreach ($i as $item) { + $this->assertInstanceOf('\StdClass', $item); + } + } + + public function testReachesAllProperties() + { + $i = new ObjectIterator($this->testObject); + + $count = 0; + foreach ($i as $item) { + $count += count(get_object_vars($item)); + } + + $this->assertEquals(10, $count); + } +} diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index 92e1d5c3..cf20ff7b 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -264,4 +264,18 @@ private function getInvalidSchema() ) ); } + + public function testGetUriRetriever() + { + $s = new SchemaStorage(); + $s->addSchema('http://json-schema.org/draft-04/schema#'); + $this->assertInstanceOf('\JsonSchema\Uri\UriRetriever', $s->getUriRetriever()); + } + + public function testGetUriResolver() + { + $s = new SchemaStorage(); + $s->addSchema('http://json-schema.org/draft-04/schema#'); + $this->assertInstanceOf('\JsonSchema\Uri\UriResolver', $s->getUriResolver()); + } } diff --git a/tests/Uri/Retrievers/CurlTest.php b/tests/Uri/Retrievers/CurlTest.php new file mode 100644 index 00000000..f833b590 --- /dev/null +++ b/tests/Uri/Retrievers/CurlTest.php @@ -0,0 +1,57 @@ +retrieve(realpath(__DIR__ . '/../../fixtures/foobar.json')); + } + + public function testRetrieveNonexistantFile() + { + $c = new Curl(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found' + ); + $c->retrieve(__DIR__ . '/notARealFile'); + } + + public function testNoContentType() + { + $c = new Curl(); + $c->retrieve(realpath(__DIR__ . '/../../fixtures') . '/foobar-noheader.json'); + } + } +} + +namespace JsonSchema\Uri\Retrievers +{ + function curl_exec($curl) + { + $uri = curl_getinfo($curl, \CURLINFO_EFFECTIVE_URL); + + if ($uri === realpath(__DIR__ . '/../../fixtures/foobar.json')) { + // return file with headers + $headers = implode("\n", array( + 'Content-Type: application/json' + )); + + return sprintf("%s\r\n\r\n%s", $headers, file_get_contents($uri)); + } elseif ($uri === realpath(__DIR__ . '/../../fixtures') . '/foobar-noheader.json') { + // return file without headers + $uri = realpath(__DIR__ . '/../../fixtures/foobar.json'); + + return "\r\n\r\n" . file_get_contents($uri); + } + + // fallback to real curl_exec + return \curl_exec($curl); + } +} diff --git a/tests/Uri/Retrievers/FileGetContentsTest.php b/tests/Uri/Retrievers/FileGetContentsTest.php index 7b67facb..d9b06263 100644 --- a/tests/Uri/Retrievers/FileGetContentsTest.php +++ b/tests/Uri/Retrievers/FileGetContentsTest.php @@ -1,27 +1,74 @@ retrieve(__DIR__ . '/Fixture/missing.json'); + /** + * @expectedException \JsonSchema\Exception\ResourceNotFoundException + */ + public function testFetchMissingFile() + { + $res = new FileGetContents(); + $res->retrieve(__DIR__ . '/Fixture/missing.json'); + } + + public function testFetchFile() + { + $res = new FileGetContents(); + $result = $res->retrieve(__DIR__ . '/../Fixture/child.json'); + $this->assertNotEmpty($result); + } + + public function testFalseReturn() + { + $res = new FileGetContents(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found at http://example.com/false' + ); + $res->retrieve('http://example.com/false'); + } + + public function testFetchDirectory() + { + $res = new FileGetContents(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found at file:///this/is/a/directory/' + ); + $res->retrieve('file:///this/is/a/directory/'); + } + + public function testContentType() + { + $res = new FileGetContents(); + + $reflector = new \ReflectionObject($res); + $fetchContentType = $reflector->getMethod('fetchContentType'); + $fetchContentType->setAccessible(true); + + $this->assertTrue($fetchContentType->invoke($res, array('Content-Type: application/json'))); + $this->assertFalse($fetchContentType->invoke($res, array('X-Some-Header: whateverValue'))); + } } +} - public function testFetchFile() +namespace JsonSchema\Uri\Retrievers +{ + function file_get_contents($uri) { - $res = new FileGetContents(); - $result = $res->retrieve(__DIR__ . '/../Fixture/child.json'); - $this->assertNotEmpty($result); + switch ($uri) { + case 'http://example.com/false': return false; + case 'file:///this/is/a/directory/': return ''; + default: return \file_get_contents($uri); + } } } diff --git a/tests/Uri/UriRetrieverTest.php b/tests/Uri/UriRetrieverTest.php index 01df161b..f5db5ca1 100644 --- a/tests/Uri/UriRetrieverTest.php +++ b/tests/Uri/UriRetrieverTest.php @@ -328,4 +328,88 @@ public function testRetrieveSchemaFromPackage() // check that the schema was loaded & processed correctly $this->assertEquals('454f423bd7edddf0bc77af4130ed9161', md5(json_encode($schema))); } + + public function testJsonSchemaOrgMediaTypeHack() + { + $mock = $this->getMock('JsonSchema\Uri\UriRetriever', array('getContentType')); + $mock->method('getContentType')->willReturn('Application/X-Fake-Type'); + $retriever = new UriRetriever(); + + $this->assertTrue($retriever->confirmMediaType($mock, 'http://json-schema.org/')); + } + + public function testSchemaCache() + { + $retriever = new UriRetriever(); + $reflector = new \ReflectionObject($retriever); + + // inject a schema cache value + $schemaCache = $reflector->getProperty('schemaCache'); + $schemaCache->setAccessible(true); + $schemaCache->setValue($retriever, array('local://test/uri' => 'testSchemaValue')); + + // retrieve from schema cache + $loadSchema = $reflector->getMethod('loadSchema'); + $loadSchema->setAccessible(true); + $this->assertEquals( + 'testSchemaValue', + $loadSchema->invoke($retriever, 'local://test/uri') + ); + } + + public function testLoadSchemaJSONDecodingException() + { + $retriever = new UriRetriever(); + + $this->setExpectedException( + 'JsonSchema\Exception\JsonDecodingException', + 'JSON syntax is malformed' + ); + $schema = $retriever->retrieve('package://tests/fixtures/bad-syntax.json'); + } + + public function testGenerateURI() + { + $retriever = new UriRetriever(); + $components = array( + 'scheme' => 'scheme', + 'authority' => 'authority', + 'path' => '/path', + 'query' => '?query', + 'fragment' => '#fragment' + ); + $this->assertEquals('scheme://authority/path?query#fragment', $retriever->generate($components)); + } + + public function testResolveHTTP() + { + $retriever = new UriRetriever(); + $this->assertEquals( + 'http://example.com/schema', + $retriever->resolve('http://example.com/schema') + ); + } + + public function combinedURITests() + { + return array( + array('blue', 'http://example.com/red', 'http://example.com/blue'), + array('blue', 'http://example.com/', 'http://example.com/blue'), + ); + } + + /** + * @dataProvider combinedURITests + */ + public function testResolveCombinedURI($uri, $baseURI, $combinedURI) + { + $retriever = new UriRetriever(); + $this->assertEquals($combinedURI, $retriever->resolve($uri, $baseURI)); + } + + public function testIsValidURI() + { + $retriever = new UriRetriever(); + $this->assertTrue($retriever->isValid('http://example.com/schema')); + } } diff --git a/tests/fixtures/bad-syntax.json b/tests/fixtures/bad-syntax.json new file mode 100644 index 00000000..98232c64 --- /dev/null +++ b/tests/fixtures/bad-syntax.json @@ -0,0 +1 @@ +{ From cf8e886e8d752f7773ab7ad93056895591096a6b Mon Sep 17 00:00:00 2001 From: Erayd Date: Fri, 17 Mar 2017 17:39:44 +1300 Subject: [PATCH 10/20] Reset errors prior to validation (#386) --- src/JsonSchema/Validator.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index 9f44e512..d479103c 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -37,6 +37,10 @@ class Validator extends BaseConstraint */ public function validate(&$value, $schema = null, $checkMode = null) { + // reset errors prior to validation + $this->reset(); + + // set checkMode $initialCheckMode = $this->factory->getConfig(); if ($checkMode !== null) { $this->factory->setConfig($checkMode); From 5bc0d34d0b782ba97db4b49127cc052daa289a02 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 02:22:25 +1300 Subject: [PATCH 11/20] Allow the schema to be an associative array (#389) * Allow the schema to be an associative array Implements #388. * Use json_decode(json_encode()) for array -> object cast * Skip exception check on PHP versions < 5.5.0 * Skip test on HHVM, as it's happy to encode resources --- src/JsonSchema/Constraints/BaseConstraint.php | 23 ++++++++ src/JsonSchema/Validator.php | 5 ++ tests/ValidatorTest.php | 59 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 tests/ValidatorTest.php diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index bc608f6d..951f2fd2 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -10,7 +10,9 @@ namespace JsonSchema\Constraints; use JsonSchema\ConstraintError; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; +use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\ValidationException; /** @@ -89,4 +91,25 @@ public function reset() { $this->errors = array(); } + + /** + * Recursively cast an associative array to an object + * + * @param array $array + * + * @return object + */ + public static function arrayToObjectRecursive($array) + { + $json = json_encode($array); + if (json_last_error() !== \JSON_ERROR_NONE) { + $message = 'Unable to encode schema array as JSON'; + if (version_compare(phpversion(), '5.5.0', '>=')) { + $message .= ': ' . json_last_error_msg(); + } + throw new InvalidArgumentException($message); + } + + return json_decode($json); + } } diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index d479103c..0a9a8418 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -40,6 +40,11 @@ public function validate(&$value, $schema = null, $checkMode = null) // reset errors prior to validation $this->reset(); + // make sure $schema is an object + if (is_array($schema)) { + $schema = self::arrayToObjectRecursive($schema); + } + // set checkMode $initialCheckMode = $this->factory->getConfig(); if ($checkMode !== null) { diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php new file mode 100644 index 00000000..73688537 --- /dev/null +++ b/tests/ValidatorTest.php @@ -0,0 +1,59 @@ +validate($data, $schema); + + $this->assertFalse($validator->isValid(), 'Validation succeeded, but should have failed.'); + } + + public function testBadAssocSchemaInput() + { + if (version_compare(phpversion(), '5.5.0', '<')) { + $this->markTestSkipped('PHP versions < 5.5.0 trigger an error on json_encode issues'); + } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM has no problem with encoding resources'); + } + $schema = array('propertyOne' => fopen('php://stdout', 'w')); + $data = json_decode('{"propertyOne":[42]}', true); + + $validator = new Validator(); + + $this->setExpectedException('\JsonSchema\Exception\InvalidArgumentException'); + $validator->validate($data, $schema); + } + + public function testCheck() + { + $schema = json_decode('{"type":"string"}'); + $data = json_decode('42'); + + $validator = new Validator(); + $validator->check($data, $schema); + + $this->assertFalse($validator->isValid(), 'Validation succeeded, but should have failed.'); + } + + public function testCoerce() + { + $schema = json_decode('{"type":"integer"}'); + $data = json_decode('"42"'); + + $validator = new Validator(); + $validator->coerce($data, $schema); + + $this->assertTrue($validator->isValid(), 'Validation failed, but should have succeeded.'); + } +} From 1567cb6fb19cdc6889e4fa20dcbb64b2b0950308 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 02:23:12 +1300 Subject: [PATCH 12/20] Enable FILTER_FLAG_EMAIL_UNICODE for email format if present (#398) --- src/JsonSchema/Constraints/FormatConstraint.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index 87c1dc57..f03e6520 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -124,7 +124,12 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = break; case 'email': - if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)) { + $filterFlags = FILTER_NULL_ON_FAILURE; + if (defined('FILTER_FLAG_EMAIL_UNICODE')) { + // Only available from PHP >= 7.1.0, so ignore it for coverage checks + $filterFlags |= constant('FILTER_FLAG_EMAIL_UNICODE'); // @codeCoverageIgnore + } + if (null === filter_var($element, FILTER_VALIDATE_EMAIL, $filterFlags)) { $this->addError(ConstraintError::FORMAT_EMAIL(), $path, array('format' => $schema->format)); } break; From c44609a536731ff978ab9186c8f26facb30ee1c6 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 02:23:48 +1300 Subject: [PATCH 13/20] Don't throw exceptions until after checking anyOf / oneOf (#394) Fixes #393 --- .../Constraints/UndefinedConstraint.php | 27 ++++++++---- tests/Constraints/OfPropertiesTest.php | 43 +++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 264a8d36..a03291da 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -12,6 +12,7 @@ use JsonSchema\ConstraintError; use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; +use JsonSchema\Exception\ValidationException; use JsonSchema\Uri\UriResolver; /** @@ -245,11 +246,16 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i if (isset($schema->anyOf)) { $isValid = false; $startErrors = $this->getErrors(); + $caughtException = null; foreach ($schema->anyOf as $anyOf) { $initErrors = $this->getErrors(); - $this->checkUndefined($value, $anyOf, $path, $i); - if ($isValid = (count($this->getErrors()) == count($initErrors))) { - break; + try { + $this->checkUndefined($value, $anyOf, $path, $i); + if ($isValid = (count($this->getErrors()) == count($initErrors))) { + break; + } + } catch (ValidationException $e) { + $isValid = false; } } if (!$isValid) { @@ -264,12 +270,17 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i $matchedSchemas = 0; $startErrors = $this->getErrors(); foreach ($schema->oneOf as $oneOf) { - $this->errors = array(); - $this->checkUndefined($value, $oneOf, $path, $i); - if (count($this->getErrors()) == 0) { - $matchedSchemas++; + try { + $this->errors = array(); + $this->checkUndefined($value, $oneOf, $path, $i); + if (count($this->getErrors()) == 0) { + $matchedSchemas++; + } + $allErrors = array_merge($allErrors, array_values($this->getErrors())); + } catch (ValidationException $e) { + // deliberately do nothing here - validation failed, but we want to check + // other schema options in the OneOf field. } - $allErrors = array_merge($allErrors, array_values($this->getErrors())); } if ($matchedSchemas !== 1) { $this->addErrors(array_merge($allErrors, $startErrors)); diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index 721195f5..f4fb8968 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -8,6 +8,9 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\Constraint; +use JsonSchema\Validator; + /** * Class OfPropertiesTest */ @@ -223,4 +226,44 @@ public function getInvalidTests() ) ); } + + public function testNoPrematureAnyOfException() + { + $schema = json_decode('{ + "type": "object", + "properties": { + "propertyOne": { + "anyOf": [ + {"type": "number"}, + {"type": "string"} + ] + } + } + }'); + $data = json_decode('{"propertyOne":"ABC"}'); + + $v = new Validator(); + $v->validate($data, $schema, Constraint::CHECK_MODE_EXCEPTIONS); + $this->assertTrue($v->isValid()); + } + + public function testNoPrematureOneOfException() + { + $schema = json_decode('{ + "type": "object", + "properties": { + "propertyOne": { + "oneOf": [ + {"type": "number"}, + {"type": "string"} + ] + } + } + }'); + $data = json_decode('{"propertyOne":"ABC"}'); + + $v = new Validator(); + $v->validate($data, $schema, Constraint::CHECK_MODE_EXCEPTIONS); + $this->assertTrue($v->isValid()); + } } From af143723998fdce27b15358521b394e254200681 Mon Sep 17 00:00:00 2001 From: Max Loeb Date: Tue, 21 Mar 2017 06:25:53 -0700 Subject: [PATCH 14/20] add enum wrapper (#375) --- src/JsonSchema/ConstraintError.php | 2 +- src/JsonSchema/Enum.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/JsonSchema/Enum.php diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index 2af542d8..f1588d12 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -4,7 +4,7 @@ use JsonSchema\Exception\InvalidArgumentException; -class ConstraintError extends \MabeEnum\Enum +class ConstraintError extends Enum { const ADDITIONAL_ITEMS = 'additionalItems'; const ADDITIONAL_PROPERTIES = 'additionalProp'; diff --git a/src/JsonSchema/Enum.php b/src/JsonSchema/Enum.php new file mode 100644 index 00000000..c52f8daa --- /dev/null +++ b/src/JsonSchema/Enum.php @@ -0,0 +1,7 @@ + Date: Wed, 22 Mar 2017 02:27:05 +1300 Subject: [PATCH 15/20] Fix infinite recursion on some schemas when setting defaults (#359) (#365) * Don't try to fetch files that don't exist Throws an exception when the ref can't be resolved to a useful file URI, rather than waiting for something further down the line to fail after the fact. * Refactor defaults code to use LooseTypeCheck where appropriate * Test for not treating non-containers like arrays * Update comments * Rename variable for clarity * Add CHECK_MODE_ONLY_REQUIRED_DEFAULTS If CHECK_MODE_ONLY_REQUIRED_DEFAULTS is set, then only apply defaults if they are marked as required. * Workaround for $this scope issue on PHP-5.3 * Fix infinite recursion via $ref when applying defaults * Add missing second test for array case * Add test for setting a default value for null * Also fix infinite recursion via $ref for array defaults * Move nested closure into separate method * $parentSchema will always be set when $name is, so don't check it * Handle nulls properly - fixes issue #377 --- README.md | 1 + src/JsonSchema/Constraints/Constraint.php | 9 +- .../Constraints/ObjectConstraint.php | 17 +- .../Constraints/UndefinedConstraint.php | 145 +++++++++++++----- src/JsonSchema/Entity/JsonPointer.php | 23 +++ src/JsonSchema/SchemaStorage.php | 12 +- tests/Constraints/DefaultPropertiesTest.php | 138 +++++++++++++---- tests/SchemaStorageTest.php | 11 ++ 8 files changed, 274 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 4de425f5..46adffbe 100644 --- a/README.md +++ b/README.md @@ -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 | | `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 2283ac12..28c8d44c 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -31,6 +31,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; /** * Bubble down the path @@ -78,10 +79,10 @@ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, * @param mixed $i * @param mixed $patternProperties */ - protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null) + protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $appliedDefaults = array()) { $validator = $this->factory->createInstanceFor('object'); - $validator->check($value, $schema, $path, $i, $patternProperties); + $validator->check($value, $schema, $path, $i, $patternProperties, $appliedDefaults); $this->addErrors($validator->getErrors()); } @@ -110,11 +111,11 @@ protected function checkType(&$value, $schema = null, JsonPointer $path = null, * @param JsonPointer|null $path * @param mixed $i */ - protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null) + protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { $validator = $this->factory->createInstanceFor('undefined'); - $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i); + $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); $this->addErrors($validator->getErrors()); } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index d360a659..3c345619 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -20,15 +20,22 @@ */ class ObjectConstraint extends Constraint { + /** + * @var array List of properties to which a default value has been applied + */ + protected $appliedDefaults = array(); + /** * {@inheritdoc} */ - public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) + public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) { if ($element instanceof UndefinedConstraint) { return; } + $this->appliedDefaults = $appliedDefaults; + $matches = array(); if ($patternProperties) { $matches = $this->validatePatternProperties($element, $path, $patternProperties); @@ -64,7 +71,7 @@ public function validatePatternProperties($element, JsonPointer $path = null, $p foreach ($element as $i => $value) { if (preg_match($delimiter . $pregex . $delimiter . 'u', $i)) { $matches[] = $i; - $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i); + $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); } } } @@ -96,9 +103,9 @@ public function validateElement($element, $matches, $objectDefinition = null, Js // additional properties defined if (!in_array($i, $matches) && $additionalProp && !$definition) { if ($additionalProp === true) { - $this->checkUndefined($value, null, $path, $i); + $this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults)); } else { - $this->checkUndefined($value, $additionalProp, $path, $i); + $this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults)); } } @@ -135,7 +142,7 @@ public function validateDefinition(&$element, $objectDefinition = null, JsonPoin if (is_object($definition)) { // Undefined constraint will check for is_object() and quit if is not - so why pass it? - $this->checkUndefined($property, $definition, $path, $i); + $this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults)); } } } diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index a03291da..ed7d113f 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -23,16 +23,24 @@ */ class UndefinedConstraint extends Constraint { + /** + * @var array List of properties to which a default value has been applied + */ + protected $appliedDefaults = array(); + /** * {@inheritdoc} */ - public function check(&$value, $schema = null, JsonPointer $path = null, $i = null) + public function check(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { if (is_null($schema) || !is_object($schema)) { return; } $path = $this->incrementPath($path ?: new JsonPointer(''), $i); + if ($fromDefault) { + $path->setFromDefault(); + } // check special properties $this->validateCommonProperties($value, $schema, $path, $i); @@ -68,7 +76,8 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, $path, isset($schema->additionalProperties) ? $schema->additionalProperties : null, - isset($schema->patternProperties) ? $schema->patternProperties : null + isset($schema->patternProperties) ? $schema->patternProperties : null, + $this->appliedDefaults ); } @@ -113,46 +122,8 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } // Apply default values from schema - if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { - if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) { - // $value is an object, so apply default properties if defined - foreach ($schema->properties as $currentProperty => $propertyDefinition) { - if (!$this->getTypeCheck()->propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) { - if (is_object($propertyDefinition->default)) { - $this->getTypeCheck()->propertySet($value, $currentProperty, clone $propertyDefinition->default); - } else { - $this->getTypeCheck()->propertySet($value, $currentProperty, $propertyDefinition->default); - } - } - } - } elseif ($this->getTypeCheck()->isArray($value)) { - if (isset($schema->properties)) { - // $value is an array, but default properties are defined, so treat as assoc - foreach ($schema->properties as $currentProperty => $propertyDefinition) { - if (!isset($value[$currentProperty]) && isset($propertyDefinition->default)) { - if (is_object($propertyDefinition->default)) { - $value[$currentProperty] = clone $propertyDefinition->default; - } else { - $value[$currentProperty] = $propertyDefinition->default; - } - } - } - } elseif (isset($schema->items)) { - // $value is an array, and default items are defined - treat as plain array - foreach ($schema->items as $currentProperty => $itemDefinition) { - if (!isset($value[$currentProperty]) && isset($itemDefinition->default)) { - if (is_object($itemDefinition->default)) { - $value[$currentProperty] = clone $itemDefinition->default; - } else { - $value[$currentProperty] = $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; - } + if (!$path->fromDefault()) { + $this->applyDefaultValues($value, $schema, $path); } // Verify required values @@ -216,6 +187,96 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } + /** + * Check whether a default should be applied for this value + * + * @param mixed $schema + * @param mixed $parentSchema + * @param bool $requiredOnly + * + * @return bool + */ + private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) + { + // required-only mode is off + if (!$requiredOnly) { + return true; + } + // draft-04 required is set + if ( + $name !== null + && isset($parentSchema->required) + && is_array($parentSchema->required) + && in_array($name, $parentSchema->required) + ) { + return true; + } + // draft-03 required is set + if (isset($schema->required) && !is_array($schema->required) && $schema->required) { + return true; + } + // default case + return false; + } + + /** + * Apply default values + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + */ + protected function applyDefaultValues(&$value, $schema, $path) + { + // only apply defaults if feature is enabled + if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { + return; + } + + // apply defaults if appropriate + $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_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) + && property_exists($propertyDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) + ) { + // assign default value + if (is_object($propertyDefinition->default)) { + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); + } else { + LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); + } + $this->appliedDefaults[] = $currentProperty; + } + } + } 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 ( + !array_key_exists($currentItem, $value) + && property_exists($itemDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { + if (is_object($itemDefinition->default)) { + $value[$currentItem] = clone $itemDefinition->default; + } else { + $value[$currentItem] = $itemDefinition->default; + } + } + $path->setFromDefault(); + } + } elseif ( + $value instanceof self + && property_exists($schema, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { + // $value is a leaf, not a container - apply the default directly + $value = is_object($schema->default) ? clone $schema->default : $schema->default; + $path->setFromDefault(); + } + } + /** * Validate allOf, anyOf, and oneOf properties * diff --git a/src/JsonSchema/Entity/JsonPointer.php b/src/JsonSchema/Entity/JsonPointer.php index 8bb71ecf..fcaf5b8d 100644 --- a/src/JsonSchema/Entity/JsonPointer.php +++ b/src/JsonSchema/Entity/JsonPointer.php @@ -24,6 +24,11 @@ class JsonPointer /** @var string[] */ private $propertyPaths = array(); + /** + * @var bool Whether the value at this path was set from a schema default + */ + private $fromDefault = false; + /** * @param string $value * @@ -135,4 +140,22 @@ public function __toString() { return $this->getFilename() . $this->getPropertyPathAsString(); } + + /** + * Mark the value at this path as being set from a schema default + */ + public function setFromDefault() + { + $this->fromDefault = true; + } + + /** + * Check whether the value at this path was set from a schema default + * + * @return bool + */ + public function fromDefault() + { + return $this->fromDefault; + } } diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index 55d7985a..3ee081e3 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -79,8 +79,18 @@ public function getSchema($id) public function resolveRef($ref) { $jsonPointer = new JsonPointer($ref); - $refSchema = $this->getSchema($jsonPointer->getFilename()); + // resolve filename for pointer + $fileName = $jsonPointer->getFilename(); + if (!strlen($fileName)) { + throw new UnresolvableJsonPointerException(sprintf( + "Could not resolve fragment '%s': no file is defined", + $jsonPointer->getPropertyPathAsString() + )); + } + + // get & process the schema + $refSchema = $this->getSchema($fileName); foreach ($jsonPointer->getPropertyPaths() as $path) { if (is_object($refSchema) && property_exists($refSchema, $path)) { $refSchema = $this->resolveRefSchema($refSchema->{$path}); diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 6687e7c2..a3b9c4e2 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -19,83 +19,143 @@ class DefaultPropertiesTest extends VeryBaseTestCase public function getValidTests() { return array( - array(// default value for entire object + /* + // This test case was intended to check whether a default value can be applied for the + // entire object, however testing this case is impossible, because there is no way to + // distinguish between a deliberate top-level NULL and a top level that contains nothing. + // As such, the assumption is that a top-level NULL is deliberate, and should not be + // altered by replacing it with a default value. + array(// #0 default value for entire object '', '{"default":"valueOne"}', '"valueOne"' ), - array(// default value in an empty object + */ + array(// #0 default value in an empty object '{}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"valueOne"}' ), - array(// default value for top-level property + array(// #1 default value for top-level property '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// default value for sub-property + array(// #2 default value for sub-property '{"propertyOne":{}}', '{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}', '{"propertyOne":{"propertyTwo":"valueTwo"}}' ), - array(// default value for sub-property with sibling + array(// #3 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(// #4 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(// #5 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(// #6 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(// #7 default value for an already set property '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(//default item value for an array + array(// #8 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(// #9 default item value for an empty array '[]', '{"type":"array","items":[{"type":"string","default":"valueOne"}]}', '["valueOne"]' ), - array(//property without a default available + array(// #10 property without a default available '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"type":"string"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(// default property value is an object + array(// #11 default property value is an object '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":{}}}}', '{"propertyOne":"valueOne","propertyTwo":{}}' ), - array(// default item value is an object + array(// #12 default item value is an object '[]', '{"type":"array","items":[{"default":{}}]}', '[{}]' - ) + ), + array(// #13 only set required values (draft-04) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo"} + }, + "required": ["propertyTwo"] + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #14 only set required values (draft-03) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo", "required": true} + } + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #15 infinite recursion via $ref (object) + '{}', + '{"properties":{"propertyOne": {"$ref": "#","default": {}}}}', + '{"propertyOne":{}}' + ), + array(// #16 infinite recursion via $ref (array) + '[]', + '{"items":[{"$ref":"#","default":[]}]}', + '[[]]' + ), + array(// #17 default top value does not overwrite defined null + 'null', + '{"default":"valueOne"}', + 'null' + ), + array(// #18 default property value does not overwrite defined null + '{"propertyOne":null}', + '{"properties":{"propertyOne":{"default":"valueOne"}}}', + '{"propertyOne":null}' + ), + array(// #19 default value in an object is null + '{}', + '{"properties":{"propertyOne":{"default":null}}}', + '{"propertyOne":null}' + ), + array(// #20 default value in an array is null + '[]', + '{"items":[{"default":null}]}', + '[null]' + ), ); } /** * @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); @@ -103,11 +163,14 @@ public function testValidCases($input, $schema, $expectOutput = null, $validator $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; + + $schemaStorage = new SchemaStorage(); + $schemaStorage->addSchema('local://testSchema', json_decode($schema)); + $factory = new Factory($schemaStorage); + $validator = new Validator($factory); + + $validator->validate($inputDecoded, json_decode('{"$ref": "local://testSchema"}'), $checkMode); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); @@ -119,28 +182,28 @@ 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() { - $input = json_decode(''); - $schema = json_decode('{"default":{"propertyOne":"valueOne"}}'); + $input = json_decode('{}'); + $schema = json_decode('{"properties":{"propertyOne":{"default":"valueOne"}}}'); $validator = new Validator(); $validator->validate($input, $schema, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); @@ -148,6 +211,21 @@ public function testNoModificationViaReferences() $this->assertEquals('{"propertyOne":"valueOne"}', json_encode($input)); $input->propertyOne = 'valueTwo'; - $this->assertEquals('valueOne', $schema->default->propertyOne); + $this->assertEquals('valueOne', $schema->properties->propertyOne->default); + } + + public function testLeaveBasicTypesAlone() + { + $input = json_decode('"ThisIsAString"'); + $schema = json_decode('{"properties": {"propertyOne": {"default": "valueOne"}}}'); + + $validator = new Validator(); + $validator->validate($input, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); + + $this->assertEquals('"ThisIsAString"', json_encode($input)); + + $schema = json_decode('{"items":[{"type":"string","default":"valueOne"}]}'); + $validator->validate($input, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); + $this->assertEquals('"ThisIsAString"', json_encode($input)); } } diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index cf20ff7b..294f0f95 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -119,6 +119,17 @@ public function testUnresolvableJsonPointExceptionShouldBeThrown() $schemaStorage->resolveRef("$mainSchemaPath#/definitions/car"); } + public function testResolveRefWithNoAssociatedFileName() + { + $this->setExpectedException( + 'JsonSchema\Exception\UnresolvableJsonPointerException', + "Could not resolve fragment '#': no file is defined" + ); + + $schemaStorage = new SchemaStorage(); + $schemaStorage->resolveRef('#'); + } + /** * @return object */ From e8259a0246806d697ead47d5dc27bea2e3861e07 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 07:27:47 +1300 Subject: [PATCH 16/20] Add option to also validate the schema (#357) --- README.md | 1 + schema-validation/json-schema-draft-03.json | 193 +++++++++++++++ schema-validation/json-schema-draft-04.json | 221 ++++++++++++++++++ src/JsonSchema/ConstraintError.php | 2 + src/JsonSchema/Constraints/BaseConstraint.php | 48 +++- src/JsonSchema/Constraints/Constraint.php | 1 + src/JsonSchema/Constraints/Factory.php | 26 +++ .../Constraints/SchemaConstraint.php | 57 ++++- .../Exception/InvalidSchemaException.php | 17 ++ src/JsonSchema/Validator.php | 7 + .../Constraints/AdditionalPropertiesTest.php | 7 +- tests/Constraints/ArraysTest.php | 2 + tests/Constraints/BaseTestCase.php | 47 +++- tests/Constraints/BasicTypesTest.php | 3 + tests/Constraints/CoerciveTest.php | 3 + tests/Constraints/DependenciesTest.php | 3 + tests/Constraints/DisallowTest.php | 8 + tests/Constraints/DivisibleByTest.php | 2 + tests/Constraints/EnumTest.php | 3 + tests/Constraints/ExtendsTest.php | 3 + tests/Constraints/FormatTest.php | 2 + tests/Constraints/LongArraysTest.php | 2 + tests/Constraints/MinItemsMaxItemsTest.php | 2 + .../MinLengthMaxLengthMultiByteTest.php | 2 + tests/Constraints/MinLengthMaxLengthTest.php | 2 + tests/Constraints/MinMaxPropertiesTest.php | 2 + tests/Constraints/MinimumMaximumTest.php | 2 + tests/Constraints/NotTest.php | 2 + .../Constraints/NumberAndIntegerTypesTest.php | 2 + tests/Constraints/OfPropertiesTest.php | 11 +- tests/Constraints/PatternPropertiesTest.php | 2 + tests/Constraints/PatternTest.php | 2 + tests/Constraints/PointerTest.php | 14 +- tests/Constraints/ReadOnlyTest.php | 2 + tests/Constraints/RequireTest.php | 2 + tests/Constraints/RequiredPropertyTest.php | 6 + tests/Constraints/SchemaValidationTest.php | 124 ++++++++++ tests/Constraints/SelfDefinedSchemaTest.php | 4 + tests/Constraints/TupleTypingTest.php | 2 + tests/Constraints/UnionTypesTest.php | 2 + tests/Constraints/UnionWithNullValueTest.php | 2 + tests/Constraints/UniqueItemsTest.php | 2 + .../WrongMessagesFailingTestCaseTest.php | 2 + tests/Drafts/Draft3Test.php | 3 + tests/Drafts/Draft4Test.php | 3 + 45 files changed, 836 insertions(+), 19 deletions(-) create mode 100644 schema-validation/json-schema-draft-03.json create mode 100644 schema-validation/json-schema-draft-04.json create mode 100644 src/JsonSchema/Exception/InvalidSchemaException.php create mode 100644 tests/Constraints/SchemaValidationTest.php diff --git a/README.md b/README.md index 46adffbe..8df14db7 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `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 | | `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | +| `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document | Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` will modify your original data. diff --git a/schema-validation/json-schema-draft-03.json b/schema-validation/json-schema-draft-03.json new file mode 100644 index 00000000..dcf07342 --- /dev/null +++ b/schema-validation/json-schema-draft-03.json @@ -0,0 +1,193 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "http://json-schema.org/draft-03/schema#", + "type": "object", + "properties": { + "type": { + "type": [ + "string", + "array" + ], + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "uniqueItems": true, + "default": "any" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "additionalProperties": { + "type": [ + { + "$ref": "#" + }, + "boolean" + ], + "default": {} + }, + "items": { + "type": [ + { + "$ref": "#" + }, + "array" + ], + "items": { + "$ref": "#" + }, + "default": {} + }, + "additionalItems": { + "type": [ + { + "$ref": "#" + }, + "boolean" + ], + "default": {} + }, + "required": { + "type": "boolean", + "default": false + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "array", + { + "$ref": "#" + } + ], + "items": { + "type": "string" + } + }, + "default": {} + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "maxLength": { + "type": "integer" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "default": { + "type": "any" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "divisibleBy": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 1 + }, + "disallow": { + "type": [ + "string", + "array" + ], + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "uniqueItems": true + }, + "extends": { + "type": [ + { + "$ref": "#" + }, + "array" + ], + "items": { + "$ref": "#" + }, + "default": {} + }, + "id": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + } + }, + "dependencies": { + "exclusiveMinimum": "minimum", + "exclusiveMaximum": "maximum" + }, + "default": {} +} \ No newline at end of file diff --git a/schema-validation/json-schema-draft-04.json b/schema-validation/json-schema-draft-04.json new file mode 100644 index 00000000..d13c1cf2 --- /dev/null +++ b/schema-validation/json-schema-draft-04.json @@ -0,0 +1,221 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/positiveInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "$ref": "#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": {} + }, + "maxItems": { + "$ref": "#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "$ref": "#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + } + }, + "dependencies": { + "exclusiveMaximum": [ + "maximum" + ], + "exclusiveMinimum": [ + "minimum" + ] + }, + "default": {} +} diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index f1588d12..d4dda01c 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -28,6 +28,7 @@ class ConstraintError extends Enum const FORMAT_STYLE = 'styleFormat'; const FORMAT_TIME = 'timeFormat'; const FORMAT_URL = 'urlFormat'; + const INVALID_SCHEMA = 'invalidSchema'; const LENGTH_MAX = 'maxLength'; const LENGTH_MIN = 'minLength'; const MAXIMUM = 'maximum'; @@ -77,6 +78,7 @@ public function getMessage() self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', self::FORMAT_URL => 'Invalid URL format', self::LENGTH_MAX => 'Must be at most %d characters long', + self::INVALID_SCHEMA => 'Schema is not valid', self::LENGTH_MIN => 'Must be at least %d characters long', self::MAX_ITEMS => 'There must be a maximum of %d items in the array', self::MAXIMUM => 'Must have a maximum value less than or equal to %d', diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index 951f2fd2..a473edac 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -14,6 +14,7 @@ use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\ValidationException; +use JsonSchema\Validator; /** * A more basic constraint definition - used for the public @@ -26,6 +27,11 @@ class BaseConstraint */ protected $errors = array(); + /** + * @var int All error types which have occurred + */ + protected $errorMask = Validator::ERROR_NONE; + /** * @var Factory */ @@ -56,7 +62,8 @@ public function addError(ConstraintError $constraint, JsonPointer $path = null, 'constraint' => array( 'name' => $name, 'params' => $more - ) + ), + 'context' => $this->factory->getErrorContext(), ); if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { @@ -64,18 +71,42 @@ public function addError(ConstraintError $constraint, JsonPointer $path = null, } $this->errors[] = $error; + $this->errorMask |= $error['context']; } public function addErrors(array $errors) { if ($errors) { $this->errors = array_merge($this->errors, $errors); + $errorMask = &$this->errorMask; + array_walk($errors, function ($error) use (&$errorMask) { + if (isset($error['context'])) { + $errorMask |= $error['context']; + } + }); } } - public function getErrors() + public function getErrors($errorContext = Validator::ERROR_ALL) + { + if ($errorContext === Validator::ERROR_ALL) { + return $this->errors; + } + + return array_filter($this->errors, function ($error) use ($errorContext) { + if ($errorContext & $error['context']) { + return true; + } + }); + } + + public function numErrors($errorContext = Validator::ERROR_ALL) { - return $this->errors; + if ($errorContext === Validator::ERROR_ALL) { + return count($this->errors); + } + + return count($this->getErrors($errorContext)); } public function isValid() @@ -90,6 +121,17 @@ public function isValid() public function reset() { $this->errors = array(); + $this->errorMask = Validator::ERROR_NONE; + } + + /** + * Get the error mask + * + * @return int + */ + public function getErrorMask() + { + return $this->errorMask; } /** diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 28c8d44c..b7f3bb42 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -32,6 +32,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; + const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; /** * Bubble down the path diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 8c24873f..c9296385 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -16,6 +16,7 @@ use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; +use JsonSchema\Validator; /** * Factory for centralize constraint initialization. @@ -42,6 +43,11 @@ class Factory */ private $typeCheck = array(); + /** + * @var int Validation context + */ + protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; + /** * @var array */ @@ -193,4 +199,24 @@ public function createInstanceFor($constraintName) return clone $this->instanceCache[$constraintName]; } + + /** + * Get the error context + * + * @return string + */ + public function getErrorContext() + { + return $this->errorContext; + } + + /** + * Set the error context + * + * @param string $validationContext + */ + public function setErrorContext($errorContext) + { + $this->errorContext = $errorContext; + } } diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index c33fe8ca..fad577b7 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -9,8 +9,13 @@ namespace JsonSchema\Constraints; +use JsonSchema\ConstraintError; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Exception\InvalidSchemaException; +use JsonSchema\Exception\RuntimeException; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; /** * The SchemaConstraint Constraints, validates an element against a given schema @@ -20,6 +25,8 @@ */ class SchemaConstraint extends Constraint { + const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; + /** * {@inheritdoc} */ @@ -27,16 +34,62 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = { if ($schema !== null) { // passed schema - $this->checkUndefined($element, $schema, $path, $i); + $validationSchema = $schema; } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); if (is_array($inlineSchema)) { $inlineSchema = json_decode(json_encode($inlineSchema)); } // inline schema - $this->checkUndefined($element, $inlineSchema, $path, $i); + $validationSchema = $inlineSchema; } else { throw new InvalidArgumentException('no schema found to verify against'); } + + // validate schema against whatever is defined in $validationSchema->$schema. If no + // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). + if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { + if (!$this->getTypeCheck()->isObject($validationSchema)) { + throw new RuntimeException('Cannot validate the schema of a non-object'); + } + if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { + $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); + } else { + $schemaSpec = self::DEFAULT_SCHEMA_SPEC; + } + + // get the spec schema + $schemaStorage = $this->factory->getSchemaStorage(); + if (!$this->getTypeCheck()->isObject($schemaSpec)) { + $schemaSpec = $schemaStorage->getSchema($schemaSpec); + } + + // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA + $initialErrorCount = $this->numErrors(); + $initialConfig = $this->factory->getConfig(); + $initialContext = $this->factory->getErrorContext(); + $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); + $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); + $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); + + // validate schema + try { + $this->check($validationSchema, $schemaSpec); + } catch (\Exception $e) { + if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { + throw new InvalidSchemaException('Schema did not pass validation', 0, $e); + } + } + if ($this->numErrors() > $initialErrorCount) { + $this->addError(ConstraintError::INVALID_SCHEMA(), $path); + } + + // restore the initial config + $this->factory->setConfig($initialConfig); + $this->factory->setErrorContext($initialContext); + } + + // validate element against $validationSchema + $this->checkUndefined($element, $validationSchema, $path, $i); } } diff --git a/src/JsonSchema/Exception/InvalidSchemaException.php b/src/JsonSchema/Exception/InvalidSchemaException.php new file mode 100644 index 00000000..c1209958 --- /dev/null +++ b/src/JsonSchema/Exception/InvalidSchemaException.php @@ -0,0 +1,17 @@ +factory->setConfig($initialCheckMode); $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); + + return $validator->getErrorMask(); } /** diff --git a/tests/Constraints/AdditionalPropertiesTest.php b/tests/Constraints/AdditionalPropertiesTest.php index 5ecfa0a7..e064312d 100644 --- a/tests/Constraints/AdditionalPropertiesTest.php +++ b/tests/Constraints/AdditionalPropertiesTest.php @@ -9,8 +9,12 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Validator; + class AdditionalPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( @@ -41,7 +45,8 @@ public function getInvalidTests() 'params' => array( 'property' => 'additionalProp' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ) ), diff --git a/tests/Constraints/ArraysTest.php b/tests/Constraints/ArraysTest.php index 5498e35e..dac14358 100644 --- a/tests/Constraints/ArraysTest.php +++ b/tests/Constraints/ArraysTest.php @@ -11,6 +11,8 @@ class ArraysTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index 75010e33..50efbd82 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -20,19 +20,31 @@ */ abstract class BaseTestCase extends VeryBaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = false; + /** * @dataProvider getInvalidTests */ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_NORMAL : $checkMode; + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -46,16 +58,25 @@ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_TYPE_CAST : $checkMode; - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input, true); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -68,12 +89,19 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra */ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -83,18 +111,25 @@ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_M */ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST) { - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schema = json_decode($schema); $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver()); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $value = json_decode($input, true); $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $validator->validate($value, $schema); + $errorMask = $validator->validate($value, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } diff --git a/tests/Constraints/BasicTypesTest.php b/tests/Constraints/BasicTypesTest.php index 7daa43ad..0e88ef42 100644 --- a/tests/Constraints/BasicTypesTest.php +++ b/tests/Constraints/BasicTypesTest.php @@ -11,6 +11,9 @@ class BasicTypesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index 36c0cf6b..e4dd173d 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -17,6 +17,9 @@ class CoerciveTest extends BasicTypesTest { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * @dataProvider getValidCoerceTests */ diff --git a/tests/Constraints/DependenciesTest.php b/tests/Constraints/DependenciesTest.php index 2e508218..f7f9d532 100644 --- a/tests/Constraints/DependenciesTest.php +++ b/tests/Constraints/DependenciesTest.php @@ -11,6 +11,9 @@ class DependenciesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DisallowTest.php b/tests/Constraints/DisallowTest.php index 07446d9d..f00d2fab 100644 --- a/tests/Constraints/DisallowTest.php +++ b/tests/Constraints/DisallowTest.php @@ -11,6 +11,14 @@ class DisallowTest extends BaseTestCase { + // schemas in these tests look like draft-03, but the 'disallow' patterns provided are in + // violation of the spec - 'disallow' as defined in draft-03 accepts the same values as the + // 'type' option, and cannot take arbitrary patterns. The implementation in this library is + // probably deliberate, but noting that it's invalid, schema validation has been disabled + // for these tests. The 'disallow' option was removed permanently in draft-04. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DivisibleByTest.php b/tests/Constraints/DivisibleByTest.php index 8a010965..b88a87a1 100644 --- a/tests/Constraints/DivisibleByTest.php +++ b/tests/Constraints/DivisibleByTest.php @@ -11,6 +11,8 @@ class DivisibleByTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/EnumTest.php b/tests/Constraints/EnumTest.php index 0ca5b9e2..723321d0 100644 --- a/tests/Constraints/EnumTest.php +++ b/tests/Constraints/EnumTest.php @@ -11,6 +11,9 @@ class EnumTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/ExtendsTest.php b/tests/Constraints/ExtendsTest.php index 289484f3..5df1fa27 100644 --- a/tests/Constraints/ExtendsTest.php +++ b/tests/Constraints/ExtendsTest.php @@ -11,6 +11,9 @@ class ExtendsTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index 5cca9c01..ad7075d9 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -15,6 +15,8 @@ class FormatTest extends BaseTestCase { + protected $validateSchema = true; + public function setUp() { date_default_timezone_set('UTC'); diff --git a/tests/Constraints/LongArraysTest.php b/tests/Constraints/LongArraysTest.php index 849c0371..2757b964 100644 --- a/tests/Constraints/LongArraysTest.php +++ b/tests/Constraints/LongArraysTest.php @@ -15,6 +15,8 @@ class LongArraysTest extends VeryBaseTestCase { + protected $validateSchema = true; + public function testLongStringArray() { $schema = diff --git a/tests/Constraints/MinItemsMaxItemsTest.php b/tests/Constraints/MinItemsMaxItemsTest.php index 1b477845..62fbaa9a 100644 --- a/tests/Constraints/MinItemsMaxItemsTest.php +++ b/tests/Constraints/MinItemsMaxItemsTest.php @@ -11,6 +11,8 @@ class MinItemsMaxItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php index ab110a40..b19ec4f7 100644 --- a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php +++ b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthMultiByteTest extends BaseTestCase { + protected $validateSchema = true; + protected function setUp() { if (!extension_loaded('mbstring')) { diff --git a/tests/Constraints/MinLengthMaxLengthTest.php b/tests/Constraints/MinLengthMaxLengthTest.php index 0e09a7a3..8dfa7158 100644 --- a/tests/Constraints/MinLengthMaxLengthTest.php +++ b/tests/Constraints/MinLengthMaxLengthTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinMaxPropertiesTest.php b/tests/Constraints/MinMaxPropertiesTest.php index 8c3a641d..e4fd5a1a 100644 --- a/tests/Constraints/MinMaxPropertiesTest.php +++ b/tests/Constraints/MinMaxPropertiesTest.php @@ -11,6 +11,8 @@ class MinMaxPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Constraints/MinimumMaximumTest.php b/tests/Constraints/MinimumMaximumTest.php index c25a7c29..508c0253 100644 --- a/tests/Constraints/MinimumMaximumTest.php +++ b/tests/Constraints/MinimumMaximumTest.php @@ -11,6 +11,8 @@ class MinimumMaximumTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NotTest.php b/tests/Constraints/NotTest.php index 27f02225..3a950f57 100644 --- a/tests/Constraints/NotTest.php +++ b/tests/Constraints/NotTest.php @@ -11,6 +11,8 @@ class NotTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php index 91e1c7cb..6c7277b9 100644 --- a/tests/Constraints/NumberAndIntegerTypesTest.php +++ b/tests/Constraints/NumberAndIntegerTypesTest.php @@ -11,6 +11,8 @@ class NumberAndIntegerTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index f4fb8968..192369c6 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -16,6 +16,8 @@ */ class OfPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getValidTests() { return array( @@ -84,7 +86,8 @@ public function getInvalidTests() 'expected' => 'array', 'found' => 'a string' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', @@ -96,7 +99,8 @@ public function getInvalidTests() 'expected' => 'array', 'found' => 'a number' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', @@ -105,7 +109,8 @@ public function getInvalidTests() 'constraint' => array( 'name' => 'oneOf', 'params' => array() - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), ), ), diff --git a/tests/Constraints/PatternPropertiesTest.php b/tests/Constraints/PatternPropertiesTest.php index a04e45b9..8dede058 100644 --- a/tests/Constraints/PatternPropertiesTest.php +++ b/tests/Constraints/PatternPropertiesTest.php @@ -11,6 +11,8 @@ class PatternPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PatternTest.php b/tests/Constraints/PatternTest.php index 0f69b9ad..c017600c 100644 --- a/tests/Constraints/PatternTest.php +++ b/tests/Constraints/PatternTest.php @@ -11,6 +11,8 @@ class PatternTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PointerTest.php b/tests/Constraints/PointerTest.php index 95c4c7b8..87ab0136 100644 --- a/tests/Constraints/PointerTest.php +++ b/tests/Constraints/PointerTest.php @@ -13,6 +13,8 @@ class PointerTest extends \PHPUnit_Framework_TestCase { + protected $validateSchema = true; + public function testVariousPointers() { $schema = array( @@ -93,7 +95,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2.prop2.1', @@ -104,7 +107,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop2.1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop3.prop3/1.prop3/1.1', @@ -115,7 +119,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop3/1.1' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop4[0].prop4-child', @@ -126,7 +131,8 @@ public function testVariousPointers() 'params' => array( 'property' => 'prop4-child' ) - ) + ), + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ), $validator->getErrors() diff --git a/tests/Constraints/ReadOnlyTest.php b/tests/Constraints/ReadOnlyTest.php index 7a3e8678..23434406 100644 --- a/tests/Constraints/ReadOnlyTest.php +++ b/tests/Constraints/ReadOnlyTest.php @@ -11,6 +11,8 @@ class ReadOnlyTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { //is readonly really required? diff --git a/tests/Constraints/RequireTest.php b/tests/Constraints/RequireTest.php index c10f8a7b..efb6f63e 100644 --- a/tests/Constraints/RequireTest.php +++ b/tests/Constraints/RequireTest.php @@ -11,6 +11,8 @@ class RequireTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/RequiredPropertyTest.php b/tests/Constraints/RequiredPropertyTest.php index 10d42f57..31545a64 100644 --- a/tests/Constraints/RequiredPropertyTest.php +++ b/tests/Constraints/RequiredPropertyTest.php @@ -14,6 +14,12 @@ class RequiredPropertyTest extends BaseTestCase { + // Most tests are draft-03 compliant, but some tests are draft-04, or mix draft-03 and + // draft-04 syntax within the same schema. Unfortunately, draft-03 and draft-04 required + // definitions are incompatible, so disabling schema validation for these tests. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function testErrorPropertyIsPopulatedForRequiredIfMissingInInput() { $validator = new UndefinedConstraint(); diff --git a/tests/Constraints/SchemaValidationTest.php b/tests/Constraints/SchemaValidationTest.php new file mode 100644 index 00000000..356637ae --- /dev/null +++ b/tests/Constraints/SchemaValidationTest.php @@ -0,0 +1,124 @@ +validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + + $this->assertTrue((bool) (Validator::ERROR_SCHEMA_VALIDATION & $errorMask)); + $this->assertGreaterThan(0, $v->numErrors(Validator::ERROR_SCHEMA_VALIDATION)); + $this->assertEquals(0, $v->numErrors(Validator::ERROR_DOCUMENT_VALIDATION)); + + $this->assertFalse($v->isValid(), 'Validation succeeded for an invalid test case'); + foreach ($v->getErrors() as $error) { + $this->assertEquals(Validator::ERROR_SCHEMA_VALIDATION, $error['context']); + } + } + + /** + * @dataProvider getValidTests + */ + public function testValidCases($schema) + { + $input = json_decode('{"propertyOne":"valueOne"}'); + $schema = json_decode($schema); + + $v = new Validator(); + $errorMask = $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + $this->assertEquals(0, $errorMask); + + if (!$v->isValid()) { + var_dump($v->getErrors(Validator::ERROR_SCHEMA_VALIDATION)); + } + $this->assertTrue($v->isValid(), 'Validation failed on a valid test case'); + } + + public function testNonObjectSchema() + { + $this->setExpectedException( + '\JsonSchema\Exception\RuntimeException', + 'Cannot validate the schema of a non-object' + ); + $this->testValidCases('"notAnObject"'); + } + + public function testInvalidSchemaException() + { + $this->setExpectedException( + '\JsonSchema\Exception\InvalidSchemaException', + 'Schema did not pass validation' + ); + + $input = json_decode('{}'); + $schema = json_decode('{"properties":{"propertyOne":{"type":"string","required":true}}}'); + + $v = new Validator(); + $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA | Constraint::CHECK_MODE_EXCEPTIONS); + } +} diff --git a/tests/Constraints/SelfDefinedSchemaTest.php b/tests/Constraints/SelfDefinedSchemaTest.php index d2cce50e..e7d3d70b 100644 --- a/tests/Constraints/SelfDefinedSchemaTest.php +++ b/tests/Constraints/SelfDefinedSchemaTest.php @@ -13,12 +13,15 @@ class SelfDefinedSchemaTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" @@ -44,6 +47,7 @@ public function getValidTests() array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" diff --git a/tests/Constraints/TupleTypingTest.php b/tests/Constraints/TupleTypingTest.php index ceab8ec6..08fedc0a 100644 --- a/tests/Constraints/TupleTypingTest.php +++ b/tests/Constraints/TupleTypingTest.php @@ -11,6 +11,8 @@ class TupleTypingTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UnionTypesTest.php b/tests/Constraints/UnionTypesTest.php index 01e49c4e..42676308 100644 --- a/tests/Constraints/UnionTypesTest.php +++ b/tests/Constraints/UnionTypesTest.php @@ -11,6 +11,8 @@ class UnionTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UnionWithNullValueTest.php b/tests/Constraints/UnionWithNullValueTest.php index a077cfdf..60301f2e 100644 --- a/tests/Constraints/UnionWithNullValueTest.php +++ b/tests/Constraints/UnionWithNullValueTest.php @@ -11,6 +11,8 @@ class UnionWithNullValueTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UniqueItemsTest.php b/tests/Constraints/UniqueItemsTest.php index 4abac569..099b407c 100644 --- a/tests/Constraints/UniqueItemsTest.php +++ b/tests/Constraints/UniqueItemsTest.php @@ -11,6 +11,8 @@ class UniqueItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/WrongMessagesFailingTestCaseTest.php b/tests/Constraints/WrongMessagesFailingTestCaseTest.php index ca620420..80a14421 100644 --- a/tests/Constraints/WrongMessagesFailingTestCaseTest.php +++ b/tests/Constraints/WrongMessagesFailingTestCaseTest.php @@ -11,6 +11,8 @@ class WrongMessagesFailingTestCaseTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php index 4a744441..1942a3b1 100644 --- a/tests/Drafts/Draft3Test.php +++ b/tests/Drafts/Draft3Test.php @@ -16,6 +16,9 @@ */ class Draft3Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Drafts/Draft4Test.php b/tests/Drafts/Draft4Test.php index a4508b0f..54eee4c4 100644 --- a/tests/Drafts/Draft4Test.php +++ b/tests/Drafts/Draft4Test.php @@ -14,6 +14,9 @@ */ class Draft4Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */ From f3bfb478c7d99963fd0a3f8d10dc437153419225 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 08:54:29 +1300 Subject: [PATCH 17/20] Improvements to type coercion (#384) * Improve performance - don't loop over everything if already valid * Don't coerce already-valid types (fixes #379) * Add remaining coercion cases & rewrite tests * Add all remaining coercion cases from ajv matrix * Rewrite the coercion tests to tidy things up a bit * Add CHECK_MODE_EARLY_COERCE If set, falls back to the old behavior of coercing to the first compatible type, regardless of whether another already-valid type might be available. * Add multiple-type test that requires coercion * \JSON_PRETTY_PRINT doesn't exist in PHP-5.3, so work around this * Various PR cleanup stuff * Fix whitespace * Turn $early into $extraFlags * Change "string" to "ABC" in string test * Update README.md description of CHECK_MODE_EARLY_COERCE * Move loop after complex tests definition * Move test #39 to grid #15 --- README.md | 9 +- src/JsonSchema/Constraints/Constraint.php | 1 + src/JsonSchema/Constraints/TypeConstraint.php | 130 +++++- tests/Constraints/CoerciveTest.php | 402 +++++++----------- tests/Constraints/OfPropertiesTest.php | 8 +- 5 files changed, 286 insertions(+), 264 deletions(-) diff --git a/README.md b/README.md index 8df14db7..aabb2eb6 100644 --- a/README.md +++ b/README.md @@ -186,14 +186,19 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default | | `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_EARLY_COERCE` | Apply type coercion as soon as 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 | | `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | | `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document | -Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` -will modify your original data. +Please note that using `CHECK_MODE_COERCE_TYPES` or `CHECK_MODE_APPLY_DEFAULTS` will modify your +original data. + +`CHECK_MODE_EARLY_COERCE` has no effect unless used in combination with `CHECK_MODE_COERCE_TYPES`. If +enabled, the validator will use (and coerce) the first compatible type it encounters, even if the +schema defines another type that matches directly and does not require coercion. ## Running the tests diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index b7f3bb42..05e3efa2 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -31,6 +31,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + const CHECK_MODE_EARLY_COERCE = 0x00000040; const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; diff --git a/src/JsonSchema/Constraints/TypeConstraint.php b/src/JsonSchema/Constraints/TypeConstraint.php index 096f5485..5bfe08a9 100644 --- a/src/JsonSchema/Constraints/TypeConstraint.php +++ b/src/JsonSchema/Constraints/TypeConstraint.php @@ -44,16 +44,24 @@ public function check(&$value = null, $schema = null, JsonPointer $path = null, { $type = isset($schema->type) ? $schema->type : null; $isValid = false; + $coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES); + $earlyCoerce = $this->factory->getConfig(self::CHECK_MODE_EARLY_COERCE); $wording = array(); if (is_array($type)) { - $this->validateTypesArray($value, $type, $wording, $isValid, $path); + $this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $this->validateTypesArray($value, $type, $wording, $isValid, $path, true); + } } elseif (is_object($type)) { $this->checkUndefined($value, $type, $path); return; } else { - $isValid = $this->validateType($value, $type); + $isValid = $this->validateType($value, $type, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $isValid = $this->validateType($value, $type, true); + } } if ($isValid === false) { @@ -62,8 +70,8 @@ public function check(&$value = null, $schema = null, JsonPointer $path = null, $wording[] = self::$wording[$type]; } $this->addError(ConstraintError::TYPE(), $path, array( - 'expected' => gettype($value), - 'found' => $this->implodeWith($wording, ', ', 'or') + 'found' => gettype($value), + 'expected' => $this->implodeWith($wording, ', ', 'or') )); } } @@ -79,9 +87,14 @@ public function check(&$value = null, $schema = null, JsonPointer $path = null, * @param bool $isValid The current validation value * @param $path */ - protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path) + protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path, $coerce = false) { foreach ($type as $tp) { + // already valid, so no need to waste cycles looping over everything + if ($isValid) { + return; + } + // $tp can be an object, if it's a schema instead of a simple type, validate it // with a new type constraint if (is_object($tp)) { @@ -98,7 +111,7 @@ protected function validateTypesArray(&$value, array $type, &$validTypesWording, $this->validateTypeNameWording($tp); $validTypesWording[] = self::$wording[$tp]; if (!$isValid) { - $isValid = $this->validateType($value, $tp); + $isValid = $this->validateType($value, $tp, $coerce); } } } @@ -157,7 +170,7 @@ protected function validateTypeNameWording($type) * * @return bool */ - protected function validateType(&$value, $type) + protected function validateType(&$value, $type, $coerce = false) { //mostly the case for inline schema if (!$type) { @@ -173,11 +186,13 @@ protected function validateType(&$value, $type) } if ('array' === $type) { + if ($coerce) { + $value = $this->toArray($value); + } + return $this->getTypeCheck()->isArray($value); } - $coerce = $this->factory->getConfig(Constraint::CHECK_MODE_COERCE_TYPES); - if ('integer' === $type) { if ($coerce) { $value = $this->toInteger($value); @@ -203,10 +218,18 @@ protected function validateType(&$value, $type) } if ('string' === $type) { + if ($coerce) { + $value = $this->toString($value); + } + return is_string($value); } if ('null' === $type) { + if ($coerce) { + $value = $this->toNull($value); + } + return is_null($value); } @@ -222,19 +245,21 @@ protected function validateType(&$value, $type) */ protected function toBoolean($value) { - if ($value === 'true') { + if ($value === 1 || $value === 'true') { return true; } - - if ($value === 'false') { + if (is_null($value) || $value === 0 || $value === 'false') { return false; } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toBoolean(reset($value)); + } return $value; } /** - * Converts a numeric string to a number. For example, "4" becomes 4. + * Converts a value to a number. For example, "4.5" becomes 4.5. * * @param mixed $value the value to convert to a number * @@ -245,14 +270,89 @@ protected function toNumber($value) if (is_numeric($value)) { return $value + 0; // cast to number } + if (is_bool($value) || is_null($value)) { + return (int) $value; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNumber(reset($value)); + } return $value; } + /** + * Converts a value to an integer. For example, "4" becomes 4. + * + * @param mixed $value + * + * @return int|mixed + */ protected function toInteger($value) { - if (is_numeric($value) && (int) $value == $value) { - return (int) $value; // cast to number + $numberValue = $this->toNumber($value); + if (is_numeric($numberValue) && (int) $numberValue == $numberValue) { + return (int) $numberValue; // cast to number + } + + return $value; + } + + /** + * Converts a value to an array containing that value. For example, [4] becomes 4. + * + * @param mixed $value + * + * @return array|mixed + */ + protected function toArray($value) + { + if (is_scalar($value) || is_null($value)) { + return array($value); + } + + return $value; + } + + /** + * Convert a value to a string representation of that value. For example, null becomes "". + * + * @param mixed $value + * + * @return string|mixed + */ + protected function toString($value) + { + if (is_numeric($value)) { + return "$value"; + } + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } + if (is_null($value)) { + return ''; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toString(reset($value)); + } + } + + /** + * Convert a value to a null. For example, 0 becomes null. + * + * @param mixed $value + * + * @return null|mixed + */ + protected function toNull($value) + { + if ($value === 0 || $value === false || $value === '') { + return null; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNull(reset($value)); } return $value; diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index e4dd173d..9a910c8a 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -11,112 +11,191 @@ use JsonSchema\Constraints\Constraint; use JsonSchema\Constraints\Factory; -use JsonSchema\SchemaStorage; -use JsonSchema\Uri\UriResolver; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Validator; -class CoerciveTest extends BasicTypesTest +class CoerciveTest extends VeryBaseTestCase { - protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; - protected $validateSchema = true; + protected $factory = null; - /** - * @dataProvider getValidCoerceTests - */ - public function testValidCoerceCasesUsingAssoc($input, $schema) + public function setUp() { - $checkMode = Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE_TYPES; - - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - - $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - - $value = json_decode($input, true); - - $validator->validate($value, $schema, $checkMode); - $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + $this->factory = new Factory(); + $this->factory->setConfig(Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE_TYPES); } - /** - * @dataProvider getValidCoerceTests - */ - public function testValidCoerceCases($input, $schema, $errors = array()) + public function dataCoerceCases() { - $checkMode = Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE_TYPES; + // check type conversions + $types = array( + // toType + 'string' => array( + // fromType fromValue toValue valid Test Number + array('string', '"ABC"', 'ABC', true), // #0 + array('integer', '45', '45', true), // #1 + array('boolean', 'true', 'true', true), // #2 + array('boolean', 'false', 'false', true), // #3 + array('NULL', 'null', '', true), // #4 + array('array', '[45]', '45', true), // #5 + array('object', '{"a":"b"}', null, false), // #6 + array('array', '[{"a":"b"}]', null, false), // #7 + ), + 'integer' => array( + array('string', '"45"', 45, true), // #8 + array('integer', '45', 45, true), // #9 + array('boolean', 'true', 1, true), // #10 + array('boolean', 'false', 0, true), // #11 + array('NULL', 'null', 0, true), // #12 + array('array', '["-45"]', -45, true), // #13 + array('object', '{"a":"b"}', null, false), // #14 + array('array', '["ABC"]', null, false), // #15 + ), + 'boolean' => array( + array('string', '"true"', true, true), // #16 + array('integer', '1', true, true), // #17 + array('boolean', 'true', true, true), // #18 + array('NULL', 'null', false, true), // #19 + array('array', '["true"]', true, true), // #20 + array('object', '{"a":"b"}', null, false), // #21 + array('string', '""', null, false), // #22 + array('string', '"ABC"', null, false), // #23 + array('integer', '2', null, false), // #24 + ), + 'NULL' => array( + array('string', '""', null, true), // #25 + array('integer', '0', null, true), // #26 + array('boolean', 'false', null, true), // #27 + array('NULL', 'null', null, true), // #28 + array('array', '[0]', null, true), // #29 + array('object', '{"a":"b"}', null, false), // #30 + array('string', '"null"', null, false), // #31 + array('integer', '-1', null, false), // #32 + ), + 'array' => array( + array('string', '"ABC"', array('ABC'), true), // #33 + array('integer', '45', array(45), true), // #34 + array('boolean', 'true', array(true), true), // #35 + array('NULL', 'null', array(null), true), // #36 + array('array', '["ABC"]', array('ABC'), true), // #37 + array('object', '{"a":"b"}', null, false), // #38 + ), + ); - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + // #39 check post-coercion validation (to array) + $tests[] = array( + '{"properties":{"propertyOne":{"type":"array","items":[{"type":"number"}]}}}', + '{"propertyOne":"ABC"}', + 'string', null, null, false + ); - $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $value = json_decode($input); + // #40 check multiple types (first valid) + $tests[] = array( + '{"properties":{"propertyOne":{"type":["number", "string"]}}}', + '{"propertyOne":42}', + 'integer', 'integer', 42, true + ); - $this->assertTrue(gettype($value->number) == 'string'); - $this->assertTrue(gettype($value->integer) == 'string'); - $this->assertTrue(gettype($value->boolean) == 'string'); + // #41 check multiple types (last valid) + $tests[] = array( + '{"properties":{"propertyOne":{"type":["number", "string"]}}}', + '{"propertyOne":"42"}', + 'string', 'string', '42', true + ); - $validator->validate($value, $schema, $checkMode); + // #42 check the meaning of life + $tests[] = array( + '{"properties":{"propertyOne":{"type":"any"}}}', + '{"propertyOne":"42"}', + 'string', 'string', '42', true + ); - $this->assertTrue(gettype($value->number) == 'double'); - $this->assertTrue(gettype($value->integer) == 'integer'); - $this->assertTrue(gettype($value->negativeInteger) == 'integer'); - $this->assertTrue(gettype($value->boolean) == 'boolean'); + // #43 check turple coercion + $tests[] = array( + '{"properties":{"propertyOne":{"type":"array","items":[{"type":"number"},{"type":"string"}]}}}', + '{"propertyOne":["42", 42]}', + 'array', 'array', array(42, '42'), true + ); - $this->assertTrue($value->number === 1.5); - $this->assertTrue($value->integer === 1); - $this->assertTrue($value->negativeInteger === -2); - $this->assertTrue($value->boolean === true); + // #44 check early coercion + $tests[] = array( + '{"properties":{"propertyOne":{"type":["object", "number", "string"]}}}', + '{"propertyOne":"42"}', + 'string', 'integer', 42, true, Constraint::CHECK_MODE_EARLY_COERCE + ); - $this->assertTrue(gettype($value->multitype1) == 'boolean'); - $this->assertTrue(gettype($value->multitype2) == 'double'); - $this->assertTrue(gettype($value->multitype3) == 'integer'); + // #45 check multiple types (none valid) + $tests[] = array( + '{"properties":{"propertyOne":{"type":["number", "boolean"]}}}', + '{"propertyOne":"42"}', + 'string', 'integer', 42, true + ); - $this->assertTrue($value->number === 1.5); - $this->assertTrue($value->integer === 1); - $this->assertTrue($value->negativeInteger === -2); - $this->assertTrue($value->boolean === true); + $tests = array(); + foreach ($types as $toType => $testCases) { + foreach ($testCases as $testCase) { + $tests[] = array( + sprintf('{"properties":{"propertyOne":{"type":"%s"}}}', strtolower($toType)), + sprintf('{"propertyOne":%s}', $testCase[1]), + $testCase[0], + $toType, + $testCase[2], + $testCase[3] + ); + } + } - $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + return $tests; } - /** - * @dataProvider getInvalidCoerceTests - */ - public function testInvalidCoerceCases($input, $schema, $errors = array()) + /** @dataProvider dataCoerceCases **/ + public function testCoerceCases($schema, $data, $startType, $endType, $endValue, $valid, $extraFlags = 0, $assoc = false) { - $checkMode = Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE_TYPES; + $validator = new Validator($this->factory); - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + $schema = json_decode($schema); + $data = json_decode($data, $assoc); - $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $value = json_decode($input); - $validator->validate($value, $schema, $checkMode); - - if (array() !== $errors) { - $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); + // check initial type + $type = gettype(LooseTypeCheck::propertyGet($data, 'propertyOne')); + if ($assoc && $type == 'array' && $startType == 'object') { + $type = 'object'; + } + $this->assertEquals($startType, $type, "Incorrect type '$type': expected '$startType'"); + + $validator->validate($data, $schema, $this->factory->getConfig() | $extraFlags); + + // check validity + if ($valid) { + $prettyPrint = defined('\JSON_PRETTY_PRINT') ? constant('\JSON_PRETTY_PRINT') : 0; + $this->assertTrue( + $validator->isValid(), + 'Validation failed: ' . json_encode($validator->getErrors(), $prettyPrint) + ); + + // check end type + $type = gettype(LooseTypeCheck::propertyGet($data, 'propertyOne')); + $this->assertEquals($endType, $type, "Incorrect type '$type': expected '$endType'"); + + // check end value + $value = LooseTypeCheck::propertyGet($data, 'propertyOne'); + $this->assertTrue( + $value === $endValue, + sprintf( + "Incorrect value '%s': expected '%s'", + is_scalar($value) ? $value : gettype($value), + is_scalar($endValue) ? $endValue : gettype($endValue) + ) + ); + } else { + $this->assertFalse($validator->isValid(), 'Validation succeeded, but should have failed'); + $this->assertEquals(1, count($validator->getErrors())); } - $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); } - /** - * @dataProvider getInvalidCoerceTests - */ - public function testInvalidCoerceCasesUsingAssoc($input, $schema, $errors = array()) + /** @dataProvider dataCoerceCases **/ + public function testCoerceCasesUsingAssoc($schema, $data, $startType, $endType, $endValue, $valid, $early = false) { - $checkMode = Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_COERCE_TYPES; - - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); - - $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $value = json_decode($input, true); - $validator->validate($value, $schema, $checkMode); - - if (array() !== $errors) { - $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); - } - $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); + $this->testCoerceCases($schema, $data, $startType, $endType, $endValue, $valid, $early, true); } public function testCoerceAPI() @@ -127,167 +206,4 @@ public function testCoerceAPI() $v->coerce($input, $schema); $this->assertEquals('{"propertyOne":10}', json_encode($input)); } - - public function getValidCoerceTests() - { - return array( - array( - '{ - "string":"string test", - "number":"1.5", - "integer":"1", - "negativeInteger":"-2", - "boolean":"true", - "object":{}, - "array":[], - "null":null, - "any": "string", - "allOf": "1", - "multitype1": "false", - "multitype2": "1.2", - "multitype3": "7", - "arrayOfIntegers":["-1","0","1"], - "tupleTyping":["1","2.2","true"], - "any1": 2.6, - "any2": 4, - "any3": false, - "any4": {}, - "any5": [], - "any6": null - }', - '{ - "type":"object", - "properties":{ - "string":{"type":"string"}, - "number":{"type":"number"}, - "integer":{"type":"integer"}, - "negativeInteger":{"type":"integer"}, - "boolean":{"type":"boolean"}, - "object":{"type":"object"}, - "array":{"type":"array"}, - "null":{"type":"null"}, - "any": {"type":"any"}, - "allOf" : {"allOf":[{ - "type" : "string" - },{ - "type" : "integer" - }]}, - "multitype1": {"type":["boolean","integer","number"]}, - "multitype2": {"type":["boolean","integer","number"]}, - "multitype3": {"type":["boolean","integer","number"]}, - "arrayOfIntegers":{ - "items":{ - "type":"integer" - } - }, - "tupleTyping":{ - "type":"array", - "items":[ - {"type":"integer"}, - {"type":"number"} - ], - "additionalItems":{"type":"boolean"} - }, - "any1": {"type":"any"}, - "any2": {"type":"any"}, - "any3": {"type":"any"}, - "any4": {"type":"any"}, - "any5": {"type":"any"}, - "any6": {"type":"any"} - }, - "additionalProperties":false - }', - ), - ); - } - - public function getInvalidCoerceTests() - { - return array( - array( - '{ - "string":null - }', - '{ - "type":"object", - "properties": { - "string":{"type":"string"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "number":"five" - }', - '{ - "type":"object", - "properties": { - "number":{"type":"number"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "integer":"5.2" - }', - '{ - "type":"object", - "properties": { - "integer":{"type":"integer"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "boolean":"0" - }', - '{ - "type":"object", - "properties": { - "boolean":{"type":"boolean"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "object":null - }', - '{ - "type":"object", - "properties": { - "object":{"type":"object"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "array":null - }', - '{ - "type":"object", - "properties": { - "array":{"type":"array"} - }, - "additionalProperties":false - }', - ), - array( - '{ - "null":1 - }', - '{ - "type":"object", - "properties": { - "null":{"type":"null"} - }, - "additionalProperties":false - }', - ), - ); - } } diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index 192369c6..ff8bded3 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -83,8 +83,8 @@ public function getInvalidTests() 'constraint' => array( 'name' => 'type', 'params' => array( - 'expected' => 'array', - 'found' => 'a string' + 'expected' => 'a string', + 'found' => 'array' ) ), 'context' => Validator::ERROR_DOCUMENT_VALIDATION @@ -96,8 +96,8 @@ public function getInvalidTests() 'constraint' => array( 'name' => 'type', 'params' => array( - 'expected' => 'array', - 'found' => 'a number' + 'expected' => 'a number', + 'found' => 'array' ) ), 'context' => Validator::ERROR_DOCUMENT_VALIDATION From 2553ebd78c547c0dadeb4dc37ee5eee96c168a58 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 22 Mar 2017 08:55:08 +1300 Subject: [PATCH 18/20] Remove stale files from #357 (obviated by #362) (#400) --- schema-validation/json-schema-draft-03.json | 193 ----------------- schema-validation/json-schema-draft-04.json | 221 -------------------- 2 files changed, 414 deletions(-) delete mode 100644 schema-validation/json-schema-draft-03.json delete mode 100644 schema-validation/json-schema-draft-04.json diff --git a/schema-validation/json-schema-draft-03.json b/schema-validation/json-schema-draft-03.json deleted file mode 100644 index dcf07342..00000000 --- a/schema-validation/json-schema-draft-03.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "id": "http://json-schema.org/draft-03/schema#", - "type": "object", - "properties": { - "type": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true, - "default": "any" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "additionalProperties": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "items": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "additionalItems": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "required": { - "type": "boolean", - "default": false - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "array", - { - "$ref": "#" - } - ], - "items": { - "type": "string" - } - }, - "default": {} - }, - "minimum": { - "type": "number" - }, - "maximum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minItems": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxItems": { - "type": "integer", - "minimum": 0 - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "minLength": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxLength": { - "type": "integer" - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "default": { - "type": "any" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "format": { - "type": "string" - }, - "divisibleBy": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 1 - }, - "disallow": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true - }, - "extends": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "id": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - } - }, - "dependencies": { - "exclusiveMinimum": "minimum", - "exclusiveMaximum": "maximum" - }, - "default": {} -} \ No newline at end of file diff --git a/schema-validation/json-schema-draft-04.json b/schema-validation/json-schema-draft-04.json deleted file mode 100644 index d13c1cf2..00000000 --- a/schema-validation/json-schema-draft-04.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#" - } - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/positiveInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": { - "$ref": "#/definitions/positiveInteger" - }, - "minLength": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/schemaArray" - } - ], - "default": {} - }, - "maxItems": { - "$ref": "#/definitions/positiveInteger" - }, - "minItems": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": { - "$ref": "#/definitions/positiveInteger" - }, - "minProperties": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/stringArray" - }, - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/stringArray" - } - ] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "allOf": { - "$ref": "#/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schemaArray" - }, - "not": { - "$ref": "#" - } - }, - "dependencies": { - "exclusiveMaximum": [ - "maximum" - ], - "exclusiveMinimum": [ - "minimum" - ] - }, - "default": {} -} From 7ec8f1bedec59ffa2a897c63dfdf7ab81df7ccc0 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 12 Apr 2017 08:46:00 +1200 Subject: [PATCH 19/20] Split $objectDefinition into $schema and $properties (#411) Object validation attempts to use a single variable to store both the object definition, and its properties. This causes validation to be incomplete where "properties" is not set, but "additionalProperties" is. This commit fixes both bugs in issue #353. --- src/JsonSchema/Constraints/Constraint.php | 8 ++-- .../Constraints/ObjectConstraint.php | 44 ++++++++++--------- .../Constraints/UndefinedConstraint.php | 3 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 05e3efa2..e0e044bc 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -78,13 +78,15 @@ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path - * @param mixed $i + * @param mixed $properties + * @param mixed $additionalProperties * @param mixed $patternProperties */ - protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $appliedDefaults = array()) + protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $properties = null, + $additionalProperties = null, $patternProperties = null, $appliedDefaults = array()) { $validator = $this->factory->createInstanceFor('object'); - $validator->check($value, $schema, $path, $i, $patternProperties, $appliedDefaults); + $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); $this->addErrors($validator->getErrors()); } diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 3c345619..dd1c02b9 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -28,7 +28,8 @@ class ObjectConstraint extends Constraint /** * {@inheritdoc} */ - public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) + public function check(&$element, $schema = null, JsonPointer $path = null, $properties = null, + $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) { if ($element instanceof UndefinedConstraint) { return; @@ -38,16 +39,17 @@ public function check(&$element, $definition = null, JsonPointer $path = null, $ $matches = array(); if ($patternProperties) { + // validate the element pattern properties $matches = $this->validatePatternProperties($element, $path, $patternProperties); } - if ($definition) { - // validate the definition properties - $this->validateDefinition($element, $definition, $path); + if ($properties) { + // validate the element properties + $this->validateProperties($element, $properties, $path); } - // additional the element properties - $this->validateElement($element, $matches, $definition, $path, $additionalProp); + // validate additional element properties & constraints + $this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp); } public function validatePatternProperties($element, JsonPointer $path = null, $patternProperties) @@ -82,18 +84,20 @@ public function validatePatternProperties($element, JsonPointer $path = null, $p /** * Validates the element properties * - * @param \stdClass $element Element to validate - * @param array $matches Matches from patternProperties (if any) - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param JsonPointer|null $path Path to test? - * @param mixed $additionalProp Additional properties + * @param \StdClass $element Element to validate + * @param array $matches Matches from patternProperties (if any) + * @param \StdClass $schema ObjectConstraint definition + * @param JsonPointer|null $path Current test path + * @param \StdClass $properties Properties + * @param mixed $additionalProp Additional properties */ - public function validateElement($element, $matches, $objectDefinition = null, JsonPointer $path = null, $additionalProp = null) + public function validateElement($element, $matches, $schema = null, JsonPointer $path = null, + $properties = null, $additionalProp = null) { - $this->validateMinMaxConstraint($element, $objectDefinition, $path); + $this->validateMinMaxConstraint($element, $schema, $path); foreach ($element as $i => $value) { - $definition = $this->getProperty($objectDefinition, $i); + $definition = $this->getProperty($properties, $i); // no additional properties allowed if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { @@ -128,17 +132,17 @@ public function validateElement($element, $matches, $objectDefinition = null, Js /** * Validates the definition properties * - * @param \stdClass $element Element to validate - * @param \stdClass $objectDefinition ObjectConstraint definition - * @param JsonPointer|null $path Path? + * @param \stdClass $element Element to validate + * @param \stdClass $properties Property definitions + * @param JsonPointer|null $path Path? */ - public function validateDefinition(&$element, $objectDefinition = null, JsonPointer $path = null) + public function validateProperties(&$element, $properties = null, JsonPointer $path = null) { $undefinedConstraint = $this->factory->createInstanceFor('undefined'); - foreach ($objectDefinition as $i => $value) { + foreach ($properties as $i => $value) { $property = &$this->getProperty($element, $i, $undefinedConstraint); - $definition = $this->getProperty($objectDefinition, $i); + $definition = $this->getProperty($properties, $i); if (is_object($definition)) { // Undefined constraint will check for is_object() and quit if is not - so why pass it? diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index ed7d113f..db4c5886 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -73,8 +73,9 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, - isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, + $schema, $path, + isset($schema->properties) ? $schema->properties : null, isset($schema->additionalProperties) ? $schema->additionalProperties : null, isset($schema->patternProperties) ? $schema->patternProperties : null, $this->appliedDefaults From 72fc5cbaec9258c8928b40b5f64bccc369613376 Mon Sep 17 00:00:00 2001 From: Florent Olivaud Date: Fri, 24 Mar 2017 11:27:45 +0100 Subject: [PATCH 20/20] Testcase for minProperties with properties defined + Fix Test --- tests/Constraints/MinMaxPropertiesTest.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/Constraints/MinMaxPropertiesTest.php b/tests/Constraints/MinMaxPropertiesTest.php index e4fd5a1a..2063122c 100644 --- a/tests/Constraints/MinMaxPropertiesTest.php +++ b/tests/Constraints/MinMaxPropertiesTest.php @@ -74,7 +74,7 @@ public function getInvalidTests() return array( array( '{ - "value": 1 + "value": {} }', '{ "type": "object", @@ -83,9 +83,27 @@ public function getInvalidTests() } }' ), + array( + '{}', + '{ + "type": "object", + "properties": { + "propertyOne": { + "type": "string" + }, + "propertyTwo": { + "type": "string" + } + }, + "minProperties": 1 + }' + ), array( '{ - "value": 1 + "value": { + "propertyOne": "valueOne", + "propertyTwo": "valueTwo" + } }', '{ "type": "object",