diff --git a/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php b/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php index bd6efac6..79e0ee23 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php @@ -66,6 +66,10 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n $this->checkForKeyword('pattern', $value, $schema, $path, $i); } + private const KEYWORD_SCHEMA_PROPERTIES = [ + 'ref' => ['$ref'], + ]; + /** * @param mixed $value * @param mixed $schema @@ -73,9 +77,15 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n */ protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void { - $validator = $this->factory->createInstanceFor($keyword); - $validator->check($value, $schema, $path, $i); + foreach (self::KEYWORD_SCHEMA_PROPERTIES[$keyword] ?? [$keyword] as $property) { + if (property_exists($schema, $property)) { + $validator = $this->factory->createInstanceFor($keyword); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); - $this->addErrors($validator->getErrors()); + return; + } + } } } diff --git a/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php b/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php index 084ce96c..d1cac103 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php @@ -68,6 +68,12 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n $this->checkForKeyword('content', $value, $schema, $path, $i); } + private const KEYWORD_SCHEMA_PROPERTIES = [ + 'ref' => ['$ref'], + 'ifThenElse' => ['if'], + 'content' => ['contentMediaType', 'contentEncoding'], + ]; + /** * @param mixed $value * @param mixed $schema @@ -75,9 +81,15 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n */ protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void { - $validator = $this->factory->createInstanceFor($keyword); - $validator->check($value, $schema, $path, $i); + foreach (self::KEYWORD_SCHEMA_PROPERTIES[$keyword] ?? [$keyword] as $property) { + if (property_exists($schema, $property)) { + $validator = $this->factory->createInstanceFor($keyword); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); - $this->addErrors($validator->getErrors()); + return; + } + } } } diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php index 0b003b54..2167387e 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php @@ -69,6 +69,12 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n $this->checkForKeyword('content', $value, $schema, $path, $i); } + private const KEYWORD_SCHEMA_PROPERTIES = [ + 'ref' => ['$ref'], + 'ifThenElse' => ['if'], + 'content' => ['contentMediaType', 'contentEncoding'], + ]; + /** * @param mixed $value * @param mixed $schema @@ -76,9 +82,15 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n */ protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void { - $validator = $this->factory->createInstanceFor($keyword); - $validator->check($value, $schema, $path, $i); + foreach (self::KEYWORD_SCHEMA_PROPERTIES[$keyword] ?? [$keyword] as $property) { + if (property_exists($schema, $property)) { + $validator = $this->factory->createInstanceFor($keyword); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); - $this->addErrors($validator->getErrors()); + return; + } + } } } diff --git a/tests/Constraints/Drafts/Draft06/Draft06ConstraintTest.php b/tests/Constraints/Drafts/Draft06/Draft06ConstraintTest.php new file mode 100644 index 00000000..ed6d9842 --- /dev/null +++ b/tests/Constraints/Drafts/Draft06/Draft06ConstraintTest.php @@ -0,0 +1,79 @@ +injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame([], $calledKeywords); + } + + public function testOnlyKeywordsPresentInSchemaAreInstantiated(): void + { + $constraint = new Draft06Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"type": "object", "required": ["id"]}'); + $value = json_decode('{"id": 1}'); + $constraint->check($value, $schema); + + sort($calledKeywords); + $this->assertSame(['required', 'type'], $calledKeywords); + } + + public function testRefKeywordIsInstantiatedWhenDollarRefPropertyIsPresent(): void + { + $constraint = new Draft06Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"$ref": "#/definitions/foo"}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame(['ref'], $calledKeywords); + } + + private function createSpyFactory(array &$calledKeywords): Factory + { + $constraintStub = $this->createMock(ConstraintInterface::class); + $constraintStub->method('getErrors')->willReturn([]); + + $factory = $this->createMock(Factory::class); + $factory->method('createInstanceFor') + ->willReturnCallback(static function (string $keyword) use (&$calledKeywords, $constraintStub) { + $calledKeywords[] = $keyword; + + return $constraintStub; + }); + + return $factory; + } + + private function injectFactory(Draft06Constraint $constraint, Factory $factory): void + { + $property = new \ReflectionProperty(BaseConstraint::class, 'factory'); + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + $property->setValue($constraint, $factory); + } +} diff --git a/tests/Constraints/Drafts/Draft07/Draft07ConstraintTest.php b/tests/Constraints/Drafts/Draft07/Draft07ConstraintTest.php new file mode 100644 index 00000000..a2ccb048 --- /dev/null +++ b/tests/Constraints/Drafts/Draft07/Draft07ConstraintTest.php @@ -0,0 +1,114 @@ +injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame([], $calledKeywords); + } + + public function testOnlyKeywordsPresentInSchemaAreInstantiated(): void + { + $constraint = new Draft07Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"type": "object", "required": ["id"]}'); + $value = json_decode('{"id": 1}'); + $constraint->check($value, $schema); + + sort($calledKeywords); + $this->assertSame(['required', 'type'], $calledKeywords); + } + + public function testRefKeywordIsInstantiatedWhenDollarRefPropertyIsPresent(): void + { + $constraint = new Draft07Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"$ref": "#/definitions/foo"}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame(['ref'], $calledKeywords); + } + + public function testIfThenElseKeywordIsInstantiatedWhenIfPropertyIsPresent(): void + { + $constraint = new Draft07Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"if": {"type": "string"}}'); + $value = json_decode('"hello"'); + $constraint->check($value, $schema); + + $this->assertSame(['ifThenElse'], $calledKeywords); + } + + /** + * @dataProvider contentPropertyProvider + */ + public function testContentKeywordIsInstantiatedWhenEitherContentPropertyIsPresent(string $property): void + { + $constraint = new Draft07Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode(sprintf('{"%s": "text/plain"}', $property)); + $value = json_decode('"hello"'); + $constraint->check($value, $schema); + + $this->assertSame(['content'], $calledKeywords); + } + + public static function contentPropertyProvider(): \Generator + { + yield 'contentMediaType' => ['contentMediaType']; + yield 'contentEncoding' => ['contentEncoding']; + } + + private function createSpyFactory(array &$calledKeywords): Factory + { + $constraintStub = $this->createMock(ConstraintInterface::class); + $constraintStub->method('getErrors')->willReturn([]); + + $factory = $this->createMock(Factory::class); + $factory->method('createInstanceFor') + ->willReturnCallback(static function (string $keyword) use (&$calledKeywords, $constraintStub) { + $calledKeywords[] = $keyword; + + return $constraintStub; + }); + + return $factory; + } + + private function injectFactory(Draft07Constraint $constraint, Factory $factory): void + { + $property = new \ReflectionProperty(BaseConstraint::class, 'factory'); + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + $property->setValue($constraint, $factory); + } +} diff --git a/tests/Constraints/Drafts/Draft2019/Draft2019ConstraintTest.php b/tests/Constraints/Drafts/Draft2019/Draft2019ConstraintTest.php new file mode 100644 index 00000000..4d11573e --- /dev/null +++ b/tests/Constraints/Drafts/Draft2019/Draft2019ConstraintTest.php @@ -0,0 +1,127 @@ +injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame([], $calledKeywords); + } + + public function testOnlyKeywordsPresentInSchemaAreInstantiated(): void + { + $constraint = new Draft2019Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"type": "object", "required": ["id"]}'); + $value = json_decode('{"id": 1}'); + $constraint->check($value, $schema); + + sort($calledKeywords); + $this->assertSame(['required', 'type'], $calledKeywords); + } + + public function testRefKeywordIsInstantiatedWhenDollarRefPropertyIsPresent(): void + { + $constraint = new Draft2019Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"$ref": "#/definitions/foo"}'); + $value = json_decode('{}'); + $constraint->check($value, $schema); + + $this->assertSame(['ref'], $calledKeywords); + } + + public function testIfThenElseKeywordIsInstantiatedWhenIfPropertyIsPresent(): void + { + $constraint = new Draft2019Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"if": {"type": "string"}}'); + $value = json_decode('"hello"'); + $constraint->check($value, $schema); + + $this->assertSame(['ifThenElse'], $calledKeywords); + } + + /** + * @dataProvider contentPropertyProvider + */ + public function testContentKeywordIsInstantiatedWhenEitherContentPropertyIsPresent(string $property): void + { + $constraint = new Draft2019Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode(sprintf('{"%s": "text/plain"}', $property)); + $value = json_decode('"hello"'); + $constraint->check($value, $schema); + + $this->assertSame(['content'], $calledKeywords); + } + + public static function contentPropertyProvider(): \Generator + { + yield 'contentMediaType' => ['contentMediaType']; + yield 'contentEncoding' => ['contentEncoding']; + } + + public function testDependentSchemasAndDependentRequiredAreInstantiatedIndependently(): void + { + $constraint = new Draft2019Constraint(); + $calledKeywords = []; + $this->injectFactory($constraint, $this->createSpyFactory($calledKeywords)); + + $schema = json_decode('{"dependentRequired": {"a": ["b"]}}'); + $value = json_decode('{"a": 1, "b": 2}'); + $constraint->check($value, $schema); + + $this->assertSame(['dependentRequired'], $calledKeywords); + } + + private function createSpyFactory(array &$calledKeywords): Factory + { + $constraintStub = $this->createMock(ConstraintInterface::class); + $constraintStub->method('getErrors')->willReturn([]); + + $factory = $this->createMock(Factory::class); + $factory->method('createInstanceFor') + ->willReturnCallback(static function (string $keyword) use (&$calledKeywords, $constraintStub) { + $calledKeywords[] = $keyword; + + return $constraintStub; + }); + + return $factory; + } + + private function injectFactory(Draft2019Constraint $constraint, Factory $factory): void + { + $property = new \ReflectionProperty(BaseConstraint::class, 'factory'); + if (PHP_VERSION_ID < 80100) { + $property->setAccessible(true); + } + $property->setValue($constraint, $factory); + } +}