Skip to content

Commit 6288f41

Browse files
committed
Added support for $ref in PathItem Objects
fixes #17
1 parent 855aab5 commit 6288f41

File tree

6 files changed

+210
-7
lines changed

6 files changed

+210
-7
lines changed

src/ReferenceContext.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ReferenceContext
3434
* @param string $uri the URI to the base object.
3535
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
3636
*/
37-
public function __construct(SpecObjectInterface $base, string $uri)
37+
public function __construct(?SpecObjectInterface $base, string $uri)
3838
{
3939
$this->_baseSpec = $base;
4040
$this->_uri = $this->normalizeUri($uri);
@@ -60,7 +60,7 @@ private function normalizeUri($uri)
6060
/**
6161
* @return mixed
6262
*/
63-
public function getBaseSpec(): SpecObjectInterface
63+
public function getBaseSpec(): ?SpecObjectInterface
6464
{
6565
return $this->_baseSpec;
6666
}

src/spec/PathItem.php

+103
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
namespace cebe\openapi\spec;
99

10+
use cebe\openapi\ReferenceContext;
1011
use cebe\openapi\SpecBaseObject;
12+
use cebe\openapi\SpecObjectInterface;
13+
use cebe\openapi\json\JsonPointer;
1114

1215
/**
1316
* Describes the operations available on a single path.
@@ -33,6 +36,12 @@
3336
*/
3437
class PathItem extends SpecBaseObject
3538
{
39+
/**
40+
* @var Reference|null
41+
*/
42+
private $_ref;
43+
44+
3645
/**
3746
* @return array array of attributes available in this object.
3847
*/
@@ -54,6 +63,37 @@ protected function attributes(): array
5463
];
5564
}
5665

66+
/**
67+
* Create an object from spec data.
68+
* @param array $data spec data read from YAML or JSON
69+
* @throws TypeErrorException in case invalid data is supplied.
70+
*/
71+
public function __construct(array $data)
72+
{
73+
if (isset($data['$ref'])) {
74+
// Allows for an external definition of this path item.
75+
// $ref in a Path Item Object is not a Reference.
76+
// https://github.com/OAI/OpenAPI-Specification/issues/1038
77+
$this->_ref = new Reference(['$ref' => $data['$ref']], PathItem::class);
78+
unset($data['$ref']);
79+
}
80+
81+
parent::__construct($data);
82+
}
83+
84+
/**
85+
* @return mixed returns the serializable data of this object for converting it
86+
* to JSON or YAML.
87+
*/
88+
public function getSerializableData()
89+
{
90+
$data = parent::getSerializableData();
91+
if ($this->_ref instanceof Reference) {
92+
$data->{'$ref'} = $this->_ref->getReference();
93+
}
94+
return $data;
95+
}
96+
5797
/**
5898
* Perform validation on this object, check data against OpenAPI Specification rules.
5999
*/
@@ -76,4 +116,67 @@ public function getOperations()
76116
}
77117
return $operations;
78118
}
119+
120+
/**
121+
* Allows for an external definition of this path item. The referenced structure MUST be in the format of a
122+
* PathItem Object. The properties of the referenced structure are merged with the local Path Item Object.
123+
* If the same property exists in both, the referenced structure and the local one, this is a conflict.
124+
* In this case the behavior is *undefined*.
125+
* @return Reference|null
126+
*/
127+
public function getReference(): ?Reference
128+
{
129+
return $this->_ref;
130+
}
131+
132+
/**
133+
* Set context for all Reference Objects in this object.
134+
*/
135+
public function setReferenceContext(ReferenceContext $context)
136+
{
137+
if ($this->_ref instanceof Reference) {
138+
$this->_ref->setContext($context);
139+
}
140+
parent::setReferenceContext($context);
141+
}
142+
143+
/**
144+
* Resolves all Reference Objects in this object and replaces them with their resolution.
145+
* @throws exceptions\UnresolvableReferenceException in case resolving a reference fails.
146+
*/
147+
public function resolveReferences(ReferenceContext $context = null)
148+
{
149+
if ($this->_ref instanceof Reference) {
150+
$pathItem = $this->_ref->resolve($context);
151+
$this->_ref = null;
152+
// The properties of the referenced structure are merged with the local Path Item Object.
153+
foreach(self::attributes() as $attribute => $type) {
154+
if (!isset($pathItem->$attribute)) {
155+
continue;
156+
}
157+
// If the same property exists in both, the referenced structure and the local one, this is a conflict.
158+
if (isset($this->$attribute) && !empty($this->$attribute)) {
159+
$this->addError("Conflicting properties, property '$attribute' exists in local PathItem and also in the referenced one.");
160+
}
161+
$this->$attribute = $pathItem->$attribute;
162+
}
163+
}
164+
parent::resolveReferences($context);
165+
}
166+
167+
/**
168+
* Provide context information to the object.
169+
*
170+
* Context information contains a reference to the base object where it is contained in
171+
* as well as a JSON pointer to its position.
172+
* @param SpecObjectInterface $baseDocument
173+
* @param JsonPointer $jsonPointer
174+
*/
175+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
176+
{
177+
parent::setDocumentContext($baseDocument, $jsonPointer);
178+
if ($this->_ref instanceof Reference) {
179+
$this->_ref->setDocumentContext($baseDocument, $jsonPointer->append('$ref'));
180+
}
181+
}
79182
}

src/spec/Reference.php

+23-5
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function __construct(array $data, string $to = null)
6464
$this->_to = $to;
6565
$this->_ref = $data['$ref'];
6666
try {
67-
$this->_jsonReference = JsonReference::createFromReference($data['$ref']);
67+
$this->_jsonReference = JsonReference::createFromReference($this->_ref);
6868
} catch (InvalidJsonPointerSyntaxException $e) {
6969
$this->_errors[] = 'Reference: value of $ref is not a valid JSON pointer: ' . $e->getMessage();
7070
}
@@ -158,21 +158,39 @@ public function resolve(ReferenceContext $context = null)
158158
$jsonReference = $this->_jsonReference;
159159
try {
160160
if ($jsonReference->getDocumentUri() === '') {
161-
// TODO type error if resolved object does not match $this->_to ?
162-
return $jsonReference->getJsonPointer()->evaluate($context->getBaseSpec());
161+
// resolve in current document
162+
$baseSpec = $context->getBaseSpec();
163+
if ($baseSpec !== null) {
164+
// TODO type error if resolved object does not match $this->_to ?
165+
return $jsonReference->getJsonPointer()->evaluate($baseSpec);
166+
} else {
167+
// if current document was loaded via reference, it may be null,
168+
// so we load current document by URI instead.
169+
$jsonReference = JsonReference::createFromUri($context->getUri(), $jsonReference->getJsonPointer());
170+
}
163171
}
172+
173+
// resolve in external document
164174
$file = $context->resolveRelativeUri($jsonReference->getDocumentUri());
165175
// TODO could be a good idea to cache loaded files in current context to avoid loading the same files over and over again
166176
$referencedDocument = $this->fetchReferencedFile($file);
167177
$referencedData = $jsonReference->getJsonPointer()->evaluate($referencedDocument);
168178

179+
if ($referencedData === null) {
180+
return null;
181+
}
169182
/** @var $referencedObject SpecObjectInterface */
170183
$referencedObject = new $this->_to($referencedData);
171184
if ($jsonReference->getJsonPointer()->getPointer() === '') {
172185
$referencedObject->setReferenceContext(new ReferenceContext($referencedObject, $file));
186+
if ($referencedObject instanceof DocumentContextInterface) {
187+
$referencedObject->setDocumentContext($referencedObject, $jsonReference->getJsonPointer());
188+
}
173189
} else {
174-
// TODO resolving references recursively does not work as we do not know the base type of the file at this point
175-
// $referencedObject->resolveReferences(new ReferenceContext($referencedObject, $file));
190+
// resolving references recursively does not work the same if we have not referenced
191+
// the whole document. We do not know the base type of the file at this point,
192+
// so base document must be null.
193+
$referencedObject->setReferenceContext(new ReferenceContext(null, $file));
176194
}
177195

178196
return $referencedObject;

tests/spec/PathTest.php

+51
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use cebe\openapi\spec\Operation;
55
use cebe\openapi\spec\PathItem;
66
use cebe\openapi\spec\Paths;
7+
use cebe\openapi\spec\Reference;
78

89
/**
910
* @covers \cebe\openapi\spec\Paths
@@ -88,4 +89,54 @@ public function testInvalidPath()
8889
], $paths->getErrors());
8990
$this->assertFalse($result);
9091
}
92+
93+
public function testPathItemReference()
94+
{
95+
$file = __DIR__ . '/data/paths/openapi.yaml';
96+
/** @var $openapi OpenApi */
97+
$openapi = Reader::readFromYamlFile($file, \cebe\openapi\spec\OpenApi::class, false);
98+
99+
$result = $openapi->validate();
100+
$this->assertEquals([], $openapi->getErrors(), print_r($openapi->getErrors(), true));
101+
$this->assertTrue($result);
102+
103+
$this->assertInstanceOf(Paths::class, $openapi->paths);
104+
$this->assertInstanceOf(PathItem::class, $fooPath = $openapi->paths['/foo']);
105+
$this->assertInstanceOf(PathItem::class, $barPath = $openapi->paths['/bar']);
106+
107+
$this->assertEmpty($fooPath->getOperations());
108+
$this->assertEmpty($barPath->getOperations());
109+
110+
$this->assertInstanceOf(\cebe\openapi\spec\Reference::class, $fooPath->getReference());
111+
$this->assertInstanceOf(\cebe\openapi\spec\Reference::class, $barPath->getReference());
112+
113+
$this->assertNull($fooPath->getReference()->resolve());
114+
$this->assertInstanceOf(PathItem::class, $ReferencedBarPath = $barPath->getReference()->resolve());
115+
116+
$this->assertCount(1, $ReferencedBarPath->getOperations());
117+
$this->assertInstanceOf(Operation::class, $ReferencedBarPath->get);
118+
$this->assertEquals('getBar', $ReferencedBarPath->get->operationId);
119+
120+
$this->assertInstanceOf(Reference::class, $ReferencedBarPath->get->responses['200']);
121+
$this->assertInstanceOf(Reference::class, $ReferencedBarPath->get->responses['404']);
122+
123+
/** @var $openapi OpenApi */
124+
$openapi = Reader::readFromYamlFile($file, \cebe\openapi\spec\OpenApi::class, true);
125+
126+
$result = $openapi->validate();
127+
$this->assertEquals([], $openapi->getErrors(), print_r($openapi->getErrors(), true));
128+
$this->assertTrue($result);
129+
130+
$this->assertInstanceOf(Paths::class, $openapi->paths);
131+
$this->assertInstanceOf(PathItem::class, $fooPath = $openapi->paths['/foo']);
132+
$this->assertInstanceOf(PathItem::class, $barPath = $openapi->paths['/bar']);
133+
134+
$this->assertEmpty($fooPath->getOperations());
135+
$this->assertCount(1, $barPath->getOperations());
136+
$this->assertInstanceOf(Operation::class, $barPath->get);
137+
$this->assertEquals('getBar', $barPath->get->operationId);
138+
139+
$this->assertEquals('A bar', $barPath->get->responses['200']->description);
140+
$this->assertEquals('non-existing resource', $barPath->get->responses['404']->description);
141+
}
91142
}

tests/spec/data/paths/openapi.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# https://github.com/OAI/OpenAPI-Specification/issues/1961#issuecomment-506026142
2+
openapi: 3.0.2
3+
info:
4+
title: My API
5+
version: 1.0.0
6+
paths:
7+
/foo:
8+
$ref: 'path-items.yaml#/~1foo'
9+
/bar:
10+
$ref: 'path-items.yaml#/~1bar'
11+
components:
12+
responses:
13+
Bar:
14+
description: A bar
15+
content:
16+
application/json:
17+
schema: { type: object }

tests/spec/data/paths/path-items.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/foo: # this path item is empty
2+
/bar:
3+
get:
4+
operationId: getBar
5+
responses:
6+
'200':
7+
$ref: 'openapi.yaml#/components/responses/Bar'
8+
'404':
9+
$ref: '#/components/responses/404'
10+
11+
components:
12+
responses:
13+
404:
14+
description: non-existing resource

0 commit comments

Comments
 (0)