diff --git a/src/Context.php b/src/Context.php index 66720b9..1e83c51 100644 --- a/src/Context.php +++ b/src/Context.php @@ -48,6 +48,9 @@ class Context extends MagicMap public $version = Schema::VERSION_AUTO; public $exportedDefinitions = []; + + public $isRef = false; + /** * @param boolean $skipValidation * @return Context diff --git a/src/Schema.php b/src/Schema.php index 3372961..9b982e5 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -706,6 +706,9 @@ private function processObject($data, Context $options, $path, $result = null) try { $refResult = $this->process($data, $options, $path . '->$ref:' . $refString, $result); if ($refResult instanceof ObjectItemContract) { + if ($refResult->getFromRefs()) { + $refResult = clone $refResult; // @todo check performance, consider option + } $refResult->setFromRef($refString); } $ref->setImported($refResult); @@ -1025,24 +1028,39 @@ public function process($data, Context $options, $path = '#', $result = null) if ('#' === $path) { $injectDefinitions = new ScopeExit(function () use ($result, $options) { foreach ($options->exportedDefinitions as $ref => $data) { - JsonPointer::add($result, JsonPointer::splitPath($ref), $data, - JsonPointer::SKIP_IF_ISSET + JsonPointer::RECURSIVE_KEY_CREATION); + if ($data !== null) { + JsonPointer::add($result, JsonPointer::splitPath($ref), $data, + /*JsonPointer::SKIP_IF_ISSET + */ + JsonPointer::RECURSIVE_KEY_CREATION); + } } }); } - if ('#' !== $path && $ref = $data->getFromRef()) { - if ($ref[0] === '#') { - if (isset($options->exportedDefinitions[$ref])) { - $result->{self::PROP_REF} = $ref; - return $result; - } elseif (!array_key_exists($ref, $options->exportedDefinitions)) { + if ($options->isRef) { + $options->isRef = false; + } else { + if ('#' !== $path && $refs = $data->getFromRefs()) { + $ref = $refs[0]; + if (!array_key_exists($ref, $options->exportedDefinitions) && strpos($ref, '://') === false) { $exported = null; $options->exportedDefinitions[$ref] = &$exported; + $options->isRef = true; $exported = $this->process($data, $options, $ref); - $result->{self::PROP_REF} = $ref; - return $result; + unset($exported); + } + + for ($i = 1; $i < count($refs); $i++) { + $ref = $refs[$i]; + if (!array_key_exists($ref, $options->exportedDefinitions) && strpos($ref, '://') === false) { + $exported = new \stdClass(); + $exported->{self::PROP_REF} = $refs[$i - 1]; + $options->exportedDefinitions[$ref] = $exported; + } } + + $result->{self::PROP_REF} = $refs[count($refs) - 1]; + return $result; } } diff --git a/src/Structure/ObjectItem.php b/src/Structure/ObjectItem.php index 65fd8df..8f65a4a 100644 --- a/src/Structure/ObjectItem.php +++ b/src/Structure/ObjectItem.php @@ -2,6 +2,15 @@ namespace Swaggest\JsonSchema\Structure; +/** + * @method getNestedObject($className); + * @method setNestedProperty($propertyName, $value, Egg $nestedEgg); + * @method addAdditionalPropertyName($name); + * @method setDocumentPath($path); + * @method setFromRef($ref); + * @method string|null getFromRef(); + * @method string[]|null getFromRefs(); + */ class ObjectItem implements ObjectItemContract { use ObjectItemTrait; diff --git a/src/Structure/ObjectItemContract.php b/src/Structure/ObjectItemContract.php index cd290d2..b8d3da2 100644 --- a/src/Structure/ObjectItemContract.php +++ b/src/Structure/ObjectItemContract.php @@ -10,5 +10,15 @@ public function setNestedProperty($propertyName, $value, Egg $nestedEgg); public function addAdditionalPropertyName($name); public function setDocumentPath($path); public function setFromRef($ref); + + /** + * @return string + * @deprecated + */ public function getFromRef(); + + /** + * @return string[]|null + */ + public function getFromRefs(); } \ No newline at end of file diff --git a/src/Structure/ObjectItemTrait.php b/src/Structure/ObjectItemTrait.php index baeb04e..3949547 100644 --- a/src/Structure/ObjectItemTrait.php +++ b/src/Structure/ObjectItemTrait.php @@ -104,6 +104,10 @@ public function setDocumentPath($path) } /** + * @see ObjectItemContract::getFromRef + * @deprecated use ObjectItemContract::getFromRefs + * @see ObjectItemContract::getFromRefs + * @todo remove * @return string */ public function getFromRef() @@ -111,6 +115,20 @@ public function getFromRef() return null === $this->__fromRef ? null : $this->__fromRef[0]; } + /** + * @see ObjectItemContract::getFromRef + * @return string + */ + public function getFromRefs() + { + return $this->__fromRef; + } + + /** + * @see ObjectItemContract::setFromRef + * @param string $ref + * @return $this + */ public function setFromRef($ref) { if (null === $this->__fromRef) { diff --git a/tests/src/Helper/DeepRefAnotherTitle.php b/tests/src/Helper/DeepRefAnotherTitle.php new file mode 100644 index 0000000..f5b328d --- /dev/null +++ b/tests/src/Helper/DeepRefAnotherTitle.php @@ -0,0 +1,22 @@ +setFromRef('http://json-schema.org/draft-04/schema#/properties/title'); + $ownerSchema->setFromRef('#/definitions/anotherTitle'); + } + +} \ No newline at end of file diff --git a/tests/src/Helper/DeepRefProperty.php b/tests/src/Helper/DeepRefProperty.php new file mode 100644 index 0000000..ab80cce --- /dev/null +++ b/tests/src/Helper/DeepRefProperty.php @@ -0,0 +1,25 @@ +type = Schema::OBJECT; + $ownerSchema->setFromRef('#/definitions/lvlA'); + $ownerSchema->setFromRef('#/definitions/lvlB'); + $ownerSchema->setFromRef('#/definitions/lvlC'); + $ownerSchema->setFromRef('#/definitions/lvlD'); + } + + +} \ No newline at end of file diff --git a/tests/src/Helper/DeepRefRoot.php b/tests/src/Helper/DeepRefRoot.php new file mode 100644 index 0000000..6e07ac7 --- /dev/null +++ b/tests/src/Helper/DeepRefRoot.php @@ -0,0 +1,37 @@ +prop = DeepRefProperty::schema(); + + $properties->directTitle = new Schema(); + $properties->directTitle->ref = 'http://json-schema.org/draft-04/schema#/properties/title'; + + $properties->intermediateTitle = DeepRefTitle::schema(); + + $properties->anotherTitle = DeepRefAnotherTitle::schema(); + + $ownerSchema->type = Schema::STRING; + } +} \ No newline at end of file diff --git a/tests/src/Helper/DeepRefTitle.php b/tests/src/Helper/DeepRefTitle.php new file mode 100644 index 0000000..07dae30 --- /dev/null +++ b/tests/src/Helper/DeepRefTitle.php @@ -0,0 +1,21 @@ +setFromRef('http://json-schema.org/draft-04/schema#/properties/title'); + $ownerSchema->setFromRef('#/definitions/title'); + } +} \ No newline at end of file diff --git a/tests/src/PHPUnit/ClassStructure/ExportSchemaTest.php b/tests/src/PHPUnit/ClassStructure/ExportSchemaTest.php index 6e81d81..ded76fb 100644 --- a/tests/src/PHPUnit/ClassStructure/ExportSchemaTest.php +++ b/tests/src/PHPUnit/ClassStructure/ExportSchemaTest.php @@ -5,6 +5,7 @@ use Swaggest\JsonSchema\Schema; use Swaggest\JsonSchema\Tests\Helper\DbId; +use Swaggest\JsonSchema\Tests\Helper\DeepRefRoot; class ExportSchemaTest extends \PHPUnit_Framework_TestCase { @@ -34,4 +35,99 @@ public function testSchemaExport() $this->assertSame($expected, json_encode($schemaData, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES)); } + public function testDeepRef() + { + $schema = DeepRefRoot::schema(); + $schemaData = Schema::export($schema); + + $expected = <<<'JSON' +{ + "properties": { + "prop": { + "$ref": "#/definitions/lvlD" + }, + "directTitle": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "intermediateTitle": { + "$ref": "#/definitions/title" + }, + "anotherTitle": { + "$ref": "#/definitions/anotherTitle" + } + }, + "type": "string", + "definitions": { + "lvlA": { + "type": "object" + }, + "lvlB": { + "$ref": "#/definitions/lvlA" + }, + "lvlC": { + "$ref": "#/definitions/lvlB" + }, + "lvlD": { + "$ref": "#/definitions/lvlC" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "anotherTitle": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + } + } +} +JSON; + + $this->assertSame($expected, json_encode($schemaData, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES)); + + + } + + public function testDeepRefSchema() + { + $schemaJson = <<<'JSON' +{ + "definitions": { + "lvl1": { + "$ref": "#/definitions/lvl2" + }, + "lvl2": { + "$ref": "#/definitions/lvl3" + }, + "lvl3": { + "$ref": "#/definitions/lvl4" + }, + "lvl4": { + "type": "integer" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "anotherTitle": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + } + }, + "properties": { + "prop": { + "$ref": "#/definitions/lvl1" + }, + "intermediateTitle": { + "$ref": "#/definitions/title" + }, + "anotherTitle": { + "$ref": "#/definitions/anotherTitle" + } + }, + "type": "object" +} +JSON; + $schemaData = json_decode($schemaJson); + + $schema = Schema::import($schemaData); + $exported = Schema::export($schema); + $this->assertSame($schemaJson, json_encode($exported, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES)); + } + } \ No newline at end of file