From b7c6ad7693fed4a750a2e818568e8c5be9597c48 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 23 Feb 2017 02:26:21 +1300 Subject: [PATCH] Add option to also validate the schema per issue #353 --- schema-validation/json-schema-draft-03.json | 193 +++++++++++++++ schema-validation/json-schema-draft-04.json | 221 ++++++++++++++++++ src/JsonSchema/Constraints/Constraint.php | 1 + .../Constraints/SchemaConstraint.php | 44 +++- tests/Constraints/BaseTestCase.php | 31 ++- tests/Constraints/SchemaValidationTest.php | 73 ++++++ 6 files changed, 559 insertions(+), 4 deletions(-) create mode 100644 schema-validation/json-schema-draft-03.json create mode 100644 schema-validation/json-schema-draft-04.json create mode 100644 tests/Constraints/SchemaValidationTest.php 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/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 7fa0a99a..834123b3 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_VALIDATE_SCHEMA = 0x00000020; /** * Bubble down the path diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index c33fe8ca..fcf548fe 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -11,6 +11,9 @@ use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Exception\RuntimeException; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; /** * The SchemaConstraint Constraints, validates an element against a given schema @@ -27,16 +30,53 @@ 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 ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { + if (!$this->getTypeCheck()->isObject($validationSchema)) { + throw new RuntimeException('cannot validate non-object schema'); + } + if (!$this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { + throw new RuntimeException('$schema is not set'); + } + + // preload standard schema specs + $preload = array( + 'http://json-schema.org/draft-03/schema' => 'json-schema-draft-03.json', + 'http://json-schema.org/draft-03/schema#' => 'json-schema-draft-03.json', + 'http://json-schema.org/draft-04/schema' => 'json-schema-draft-04.json', + 'http://json-schema.org/draft-04/schema#' => 'json-schema-draft-04.json' + ); + $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); + $schemaStorage = $this->factory->getSchemaStorage(); + foreach ($preload as $schemaID => $schemaFile) { + $schemaStorage->addSchema( + $schemaID, + json_decode(file_get_contents(__DIR__ . "/../../../schema-validation/$schemaFile")) + ); + } + + // validate schema + $validator = new Validator(new Factory($schemaStorage, null, self::CHECK_MODE_TYPE_CAST)); + $validator->validate($validationSchema, $schemaStorage->getSchema($schemaSpec)); + if (!$validator->isValid()) { + $this->addErrors($validator->getErrors()); + $this->addError($path, 'schema is not valid', 'schema'); + } + } + + // validate element against $validationSchema + $this->checkUndefined($element, $validationSchema, $path, $i); } } diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index 75010e33..cf19303b 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -20,15 +20,24 @@ */ 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); @@ -46,12 +55,18 @@ 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); @@ -68,8 +83,14 @@ 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); @@ -83,13 +104,19 @@ 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)); diff --git a/tests/Constraints/SchemaValidationTest.php b/tests/Constraints/SchemaValidationTest.php new file mode 100644 index 00000000..6df61e01 --- /dev/null +++ b/tests/Constraints/SchemaValidationTest.php @@ -0,0 +1,73 @@ +setExpectedException('JsonSchema\Exception\InvalidArgumentException'); + parent::testValidCases('{}', 'notARealSchema'); + } + + public function testNonObjectSchema() + { + $this->setExpectedException('JsonSchema\Exception\RuntimeException'); + parent::testValidCases('{"$schema": "notAnObject"}', ''); + } + + public function testMissingSchemaSpec() + { + $this->setExpectedException('JsonSchema\Exception\RuntimeException'); + parent::testValidCases('{"$schema":{}}', ''); + } +}