diff --git a/CHANGELOG.md b/CHANGELOG.md index 406d6a4a..24170612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed - Fix objects are non-unique despite key order ([#819](https://github.com/jsonrainbow/json-schema/pull/819)) +- Id's not being resolved and id property affects sibling ref which it should not do ([#828](https://github.com/jsonrainbow/json-schema/pull/828)) ### Changed - Added extra breaking change to UPDATE-6.0.md regarding BaseConstraint::addError signature change ([#823](https://github.com/jsonrainbow/json-schema/pull/823)) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 42bea6be..19dba14a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -646,22 +646,17 @@ parameters: path: src/JsonSchema/SchemaStorage.php - - message: "#^Argument of an invalid type object supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/JsonSchema/SchemaStorage.php - - - - message: "#^Call to function is_array\\(\\) with object will always evaluate to false\\.$#" + message: "#^Argument of an invalid type array\\|stdClass supplied for foreach, only iterables are supported\\.$#" count: 1 path: src/JsonSchema/SchemaStorage.php - - message: "#^Method JsonSchema\\\\SchemaStorage\\:\\:addSchema\\(\\) has no return type specified\\.$#" + message: "#^Argument of an invalid type object supplied for foreach, only iterables are supported\\.$#" count: 1 path: src/JsonSchema/SchemaStorage.php - - message: "#^Method JsonSchema\\\\SchemaStorage\\:\\:expandRefs\\(\\) has no return type specified\\.$#" + message: "#^Call to function is_array\\(\\) with object will always evaluate to false\\.$#" count: 1 path: src/JsonSchema/SchemaStorage.php @@ -695,11 +690,6 @@ parameters: count: 1 path: src/JsonSchema/SchemaStorage.php - - - message: "#^Method JsonSchema\\\\SchemaStorageInterface\\:\\:addSchema\\(\\) has no return type specified\\.$#" - count: 1 - path: src/JsonSchema/SchemaStorageInterface.php - - message: "#^Method JsonSchema\\\\Uri\\\\Retrievers\\\\Curl\\:\\:fetchMessageBody\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index 34f0daae..e7e48ef0 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -45,7 +45,7 @@ public function getUriResolver() /** * {@inheritdoc} */ - public function addSchema($id, $schema = null) + public function addSchema(string $id, $schema = null): void { 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 @@ -62,14 +62,16 @@ public function addSchema($id, $schema = null) // workaround for bug in draft-03 & draft-04 meta-schemas (id & $ref defined with incorrect format) // see https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/177#issuecomment-293051367 if (is_object($schema) && property_exists($schema, 'id')) { - if ($schema->id == 'http://json-schema.org/draft-04/schema#') { + if ($schema->id === 'http://json-schema.org/draft-04/schema#') { $schema->properties->id->format = 'uri-reference'; - } elseif ($schema->id == 'http://json-schema.org/draft-03/schema#') { + } elseif ($schema->id === 'http://json-schema.org/draft-03/schema#') { $schema->properties->id->format = 'uri-reference'; $schema->properties->{'$ref'}->format = 'uri-reference'; } } + $this->scanForSubschemas($schema, $id); + // resolve references $this->expandRefs($schema, $id); @@ -79,39 +81,44 @@ public function addSchema($id, $schema = null) /** * Recursively resolve all references against the provided base * - * @param mixed $schema - * @param string $base + * @param mixed $schema */ - private function expandRefs(&$schema, $base = null) + private function expandRefs(&$schema, ?string $parentId = null): void { if (!is_object($schema)) { if (is_array($schema)) { foreach ($schema as &$member) { - $this->expandRefs($member, $base); + $this->expandRefs($member, $parentId); } } return; } - if (property_exists($schema, 'id') && is_string($schema->id) && $base != $schema->id) { - $base = $this->uriResolver->resolve($schema->id, $base); - } - if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { - $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $base)); + $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $parentId)); $schema->{'$ref'} = (string) $refPointer; } - foreach ($schema as &$member) { - $this->expandRefs($member, $base); + foreach ($schema as $propertyName => &$member) { + if (in_array($propertyName, ['enum', 'const'])) { + // Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445 + continue; + } + + $childId = $parentId; + if (property_exists($schema, 'id') && is_string($schema->id) && $childId !== $schema->id) { + $childId = $this->uriResolver->resolve($schema->id, $childId); + } + + $this->expandRefs($member, $childId); } } /** * {@inheritdoc} */ - public function getSchema($id) + public function getSchema(string $id) { if (!array_key_exists($id, $this->schemas)) { $this->addSchema($id); @@ -123,7 +130,7 @@ public function getSchema($id) /** * {@inheritdoc} */ - public function resolveRef($ref, $resolveStack = []) + public function resolveRef(string $ref, $resolveStack = []) { $jsonPointer = new JsonPointer($ref); @@ -174,4 +181,32 @@ public function resolveRefSchema($refSchema, $resolveStack = []) return $refSchema; } + + /** + * @param mixed $schema + */ + private function scanForSubschemas($schema, string $parentId): void + { + if (!$schema instanceof \stdClass && !is_array($schema)) { + return; + } + + foreach ($schema as $propertyName => $potentialSubSchema) { + if (!is_object($potentialSubSchema)) { + continue; + } + + if (property_exists($potentialSubSchema, 'id') && is_string($potentialSubSchema->id) && property_exists($potentialSubSchema, 'type')) { + // Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471 + if (in_array($propertyName, ['enum', 'const'])) { + continue; + } + + // Found sub schema + $this->addSchema($this->uriResolver->resolve($potentialSubSchema->id, $parentId), $potentialSubSchema); + } + + $this->scanForSubschemas($potentialSubSchema, $parentId); + } + } } diff --git a/src/JsonSchema/SchemaStorageInterface.php b/src/JsonSchema/SchemaStorageInterface.php index eca44283..f625cdd2 100644 --- a/src/JsonSchema/SchemaStorageInterface.php +++ b/src/JsonSchema/SchemaStorageInterface.php @@ -9,28 +9,23 @@ interface SchemaStorageInterface /** * Adds schema with given identifier * - * @param string $id * @param object $schema */ - public function addSchema($id, $schema = null); + public function addSchema(string $id, $schema = null): void; /** * Returns schema for given identifier, or null if it does not exist * - * @param string $id - * * @return object */ - public function getSchema($id); + public function getSchema(string $id); /** * Returns schema for given reference with all sub-references resolved * - * @param string $ref - * * @return object */ - public function resolveRef($ref); + public function resolveRef(string $ref); /** * Returns schema referenced by '$ref' property diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php index 42982062..91c5f6da 100644 --- a/tests/Drafts/Draft3Test.php +++ b/tests/Drafts/Draft3Test.php @@ -9,6 +9,10 @@ namespace JsonSchema\Tests\Drafts; +use JsonSchema\Constraints\Factory; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; + /** * @package JsonSchema\Tests\Drafts */ @@ -17,6 +21,56 @@ class Draft3Test extends BaseDraftTestCase protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; protected $validateSchema = true; + /** + * This test is a copy of https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/tests/draft3/ref.json#L203-L225 + * + * @todo cleanup when #821 gets merged + * + * @param mixed $data + * @dataProvider refPreventsASiblingIdFromChangingTheBaseUriProvider + */ + public function testRefPreventsASiblingIdFromChangingTheBaseUriProvider($data, bool $expectedResult): void + { + $schema = json_decode(<<<'JSON' + { + "id": "http://localhost:1234/sibling_id/base/", + "definitions": { + "foo": { + "id": "http://localhost:1234/sibling_id/foo.json", + "type": "string" + }, + "base_foo": { + "$comment": "this canonical uri is http://localhost:1234/sibling_id/base/foo.json", + "id": "foo.json", + "type": "number" + } + }, + "extends": [ + { + "$comment": "$ref resolves to http://localhost:1234/sibling_id/base/foo.json, not http://localhost:1234/sibling_id/foo.json", + "id": "http://localhost:1234/sibling_id/", + "$ref": "foo.json" + } + ] + } +JSON + , false); + + $schemaStorage = new SchemaStorage(); + $schemaStorage->addSchema(property_exists($schema, 'id') ? $schema->id : 'internal://mySchema', $schema); + $validator = new Validator(new Factory($schemaStorage)); + $validator->validate($data, $schema); + + self::assertEquals($expectedResult, $validator->isValid()); + } + + public function refPreventsASiblingIdFromChangingTheBaseUriProvider(): \Generator + { + yield '$ref resolves to /definitions/base_foo, data does not validate' => ['data' => 'a', 'valid' => false]; + yield '$ref resolves to /definitions/base_foo, data validate' => ['data' => 1, 'valid' => true]; + } + + /** * {@inheritdoc} */