Skip to content

Commit

Permalink
Added support for $ref in PathItem Objects
Browse files Browse the repository at this point in the history
fixes #17
  • Loading branch information
cebe committed Jun 28, 2019
1 parent 855aab5 commit 6288f41
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 7 deletions.
4 changes: 2 additions & 2 deletions src/ReferenceContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ReferenceContext
* @param string $uri the URI to the base object.
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
*/
public function __construct(SpecObjectInterface $base, string $uri)
public function __construct(?SpecObjectInterface $base, string $uri)
{
$this->_baseSpec = $base;
$this->_uri = $this->normalizeUri($uri);
Expand All @@ -60,7 +60,7 @@ private function normalizeUri($uri)
/**
* @return mixed
*/
public function getBaseSpec(): SpecObjectInterface
public function getBaseSpec(): ?SpecObjectInterface
{
return $this->_baseSpec;
}
Expand Down
103 changes: 103 additions & 0 deletions src/spec/PathItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

namespace cebe\openapi\spec;

use cebe\openapi\ReferenceContext;
use cebe\openapi\SpecBaseObject;
use cebe\openapi\SpecObjectInterface;
use cebe\openapi\json\JsonPointer;

/**
* Describes the operations available on a single path.
Expand All @@ -33,6 +36,12 @@
*/
class PathItem extends SpecBaseObject
{
/**
* @var Reference|null
*/
private $_ref;


/**
* @return array array of attributes available in this object.
*/
Expand All @@ -54,6 +63,37 @@ protected function attributes(): array
];
}

/**
* Create an object from spec data.
* @param array $data spec data read from YAML or JSON
* @throws TypeErrorException in case invalid data is supplied.
*/
public function __construct(array $data)
{
if (isset($data['$ref'])) {
// Allows for an external definition of this path item.
// $ref in a Path Item Object is not a Reference.
// https://github.com/OAI/OpenAPI-Specification/issues/1038
$this->_ref = new Reference(['$ref' => $data['$ref']], PathItem::class);
unset($data['$ref']);
}

parent::__construct($data);
}

/**
* @return mixed returns the serializable data of this object for converting it
* to JSON or YAML.
*/
public function getSerializableData()
{
$data = parent::getSerializableData();
if ($this->_ref instanceof Reference) {
$data->{'$ref'} = $this->_ref->getReference();
}
return $data;
}

/**
* Perform validation on this object, check data against OpenAPI Specification rules.
*/
Expand All @@ -76,4 +116,67 @@ public function getOperations()
}
return $operations;
}

/**
* Allows for an external definition of this path item. The referenced structure MUST be in the format of a
* PathItem Object. The properties of the referenced structure are merged with the local Path Item Object.
* If the same property exists in both, the referenced structure and the local one, this is a conflict.
* In this case the behavior is *undefined*.
* @return Reference|null
*/
public function getReference(): ?Reference
{
return $this->_ref;
}

/**
* Set context for all Reference Objects in this object.
*/
public function setReferenceContext(ReferenceContext $context)
{
if ($this->_ref instanceof Reference) {
$this->_ref->setContext($context);
}
parent::setReferenceContext($context);
}

/**
* Resolves all Reference Objects in this object and replaces them with their resolution.
* @throws exceptions\UnresolvableReferenceException in case resolving a reference fails.
*/
public function resolveReferences(ReferenceContext $context = null)
{
if ($this->_ref instanceof Reference) {
$pathItem = $this->_ref->resolve($context);
$this->_ref = null;
// The properties of the referenced structure are merged with the local Path Item Object.
foreach(self::attributes() as $attribute => $type) {
if (!isset($pathItem->$attribute)) {
continue;
}
// If the same property exists in both, the referenced structure and the local one, this is a conflict.
if (isset($this->$attribute) && !empty($this->$attribute)) {
$this->addError("Conflicting properties, property '$attribute' exists in local PathItem and also in the referenced one.");
}
$this->$attribute = $pathItem->$attribute;
}
}
parent::resolveReferences($context);
}

/**
* Provide context information to the object.
*
* Context information contains a reference to the base object where it is contained in
* as well as a JSON pointer to its position.
* @param SpecObjectInterface $baseDocument
* @param JsonPointer $jsonPointer
*/
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
{
parent::setDocumentContext($baseDocument, $jsonPointer);
if ($this->_ref instanceof Reference) {
$this->_ref->setDocumentContext($baseDocument, $jsonPointer->append('$ref'));
}
}
}
28 changes: 23 additions & 5 deletions src/spec/Reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function __construct(array $data, string $to = null)
$this->_to = $to;
$this->_ref = $data['$ref'];
try {
$this->_jsonReference = JsonReference::createFromReference($data['$ref']);
$this->_jsonReference = JsonReference::createFromReference($this->_ref);
} catch (InvalidJsonPointerSyntaxException $e) {
$this->_errors[] = 'Reference: value of $ref is not a valid JSON pointer: ' . $e->getMessage();
}
Expand Down Expand Up @@ -158,21 +158,39 @@ public function resolve(ReferenceContext $context = null)
$jsonReference = $this->_jsonReference;
try {
if ($jsonReference->getDocumentUri() === '') {
// TODO type error if resolved object does not match $this->_to ?
return $jsonReference->getJsonPointer()->evaluate($context->getBaseSpec());
// resolve in current document
$baseSpec = $context->getBaseSpec();
if ($baseSpec !== null) {
// TODO type error if resolved object does not match $this->_to ?
return $jsonReference->getJsonPointer()->evaluate($baseSpec);
} else {
// if current document was loaded via reference, it may be null,
// so we load current document by URI instead.
$jsonReference = JsonReference::createFromUri($context->getUri(), $jsonReference->getJsonPointer());
}
}

// resolve in external document
$file = $context->resolveRelativeUri($jsonReference->getDocumentUri());
// TODO could be a good idea to cache loaded files in current context to avoid loading the same files over and over again
$referencedDocument = $this->fetchReferencedFile($file);
$referencedData = $jsonReference->getJsonPointer()->evaluate($referencedDocument);

if ($referencedData === null) {
return null;
}
/** @var $referencedObject SpecObjectInterface */
$referencedObject = new $this->_to($referencedData);
if ($jsonReference->getJsonPointer()->getPointer() === '') {
$referencedObject->setReferenceContext(new ReferenceContext($referencedObject, $file));
if ($referencedObject instanceof DocumentContextInterface) {
$referencedObject->setDocumentContext($referencedObject, $jsonReference->getJsonPointer());
}
} else {
// TODO resolving references recursively does not work as we do not know the base type of the file at this point
// $referencedObject->resolveReferences(new ReferenceContext($referencedObject, $file));
// resolving references recursively does not work the same if we have not referenced
// the whole document. We do not know the base type of the file at this point,
// so base document must be null.
$referencedObject->setReferenceContext(new ReferenceContext(null, $file));
}

return $referencedObject;
Expand Down
51 changes: 51 additions & 0 deletions tests/spec/PathTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use cebe\openapi\spec\Operation;
use cebe\openapi\spec\PathItem;
use cebe\openapi\spec\Paths;
use cebe\openapi\spec\Reference;

/**
* @covers \cebe\openapi\spec\Paths
Expand Down Expand Up @@ -88,4 +89,54 @@ public function testInvalidPath()
], $paths->getErrors());
$this->assertFalse($result);
}

public function testPathItemReference()
{
$file = __DIR__ . '/data/paths/openapi.yaml';
/** @var $openapi OpenApi */
$openapi = Reader::readFromYamlFile($file, \cebe\openapi\spec\OpenApi::class, false);

$result = $openapi->validate();
$this->assertEquals([], $openapi->getErrors(), print_r($openapi->getErrors(), true));
$this->assertTrue($result);

$this->assertInstanceOf(Paths::class, $openapi->paths);
$this->assertInstanceOf(PathItem::class, $fooPath = $openapi->paths['/foo']);
$this->assertInstanceOf(PathItem::class, $barPath = $openapi->paths['/bar']);

$this->assertEmpty($fooPath->getOperations());
$this->assertEmpty($barPath->getOperations());

$this->assertInstanceOf(\cebe\openapi\spec\Reference::class, $fooPath->getReference());
$this->assertInstanceOf(\cebe\openapi\spec\Reference::class, $barPath->getReference());

$this->assertNull($fooPath->getReference()->resolve());
$this->assertInstanceOf(PathItem::class, $ReferencedBarPath = $barPath->getReference()->resolve());

$this->assertCount(1, $ReferencedBarPath->getOperations());
$this->assertInstanceOf(Operation::class, $ReferencedBarPath->get);
$this->assertEquals('getBar', $ReferencedBarPath->get->operationId);

$this->assertInstanceOf(Reference::class, $ReferencedBarPath->get->responses['200']);
$this->assertInstanceOf(Reference::class, $ReferencedBarPath->get->responses['404']);

/** @var $openapi OpenApi */
$openapi = Reader::readFromYamlFile($file, \cebe\openapi\spec\OpenApi::class, true);

$result = $openapi->validate();
$this->assertEquals([], $openapi->getErrors(), print_r($openapi->getErrors(), true));
$this->assertTrue($result);

$this->assertInstanceOf(Paths::class, $openapi->paths);
$this->assertInstanceOf(PathItem::class, $fooPath = $openapi->paths['/foo']);
$this->assertInstanceOf(PathItem::class, $barPath = $openapi->paths['/bar']);

$this->assertEmpty($fooPath->getOperations());
$this->assertCount(1, $barPath->getOperations());
$this->assertInstanceOf(Operation::class, $barPath->get);
$this->assertEquals('getBar', $barPath->get->operationId);

$this->assertEquals('A bar', $barPath->get->responses['200']->description);
$this->assertEquals('non-existing resource', $barPath->get->responses['404']->description);
}
}
17 changes: 17 additions & 0 deletions tests/spec/data/paths/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# https://github.com/OAI/OpenAPI-Specification/issues/1961#issuecomment-506026142
openapi: 3.0.2
info:
title: My API
version: 1.0.0
paths:
/foo:
$ref: 'path-items.yaml#/~1foo'
/bar:
$ref: 'path-items.yaml#/~1bar'
components:
responses:
Bar:
description: A bar
content:
application/json:
schema: { type: object }
14 changes: 14 additions & 0 deletions tests/spec/data/paths/path-items.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/foo: # this path item is empty
/bar:
get:
operationId: getBar
responses:
'200':
$ref: 'openapi.yaml#/components/responses/Bar'
'404':
$ref: '#/components/responses/404'

components:
responses:
404:
description: non-existing resource

0 comments on commit 6288f41

Please sign in to comment.