Skip to content

Commit e2d3cc6

Browse files
committed
Fix resolving recursive references
- fix a bug with resolving recursive references when subdirectories are involved fixes #32 - overall improvement of resolving references - one call to resolveReferences() is now sufficient to resolve all reference levels (only loops are detected and stay reference objects)
1 parent aed871a commit e2d3cc6

File tree

11 files changed

+135
-11
lines changed

11 files changed

+135
-11
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,11 @@ $openapi->resolveReferences(
194194
);
195195
```
196196

197-
> **Note:** Resolving references currently does not deal with references in referenced files, you have to call it multiple times to resolve these.
198-
199197
### Validation
200198

201199
The library provides simple validation operations, that check basic OpenAPI spec requirements.
202200
This is the same as "structural errors found while reading the API Description file" from the CLI tool.
203-
This validation does not include checking against the OpenAPI v3.0 JSON schema.
201+
This validation does not include checking against the OpenAPI v3.0 JSON schema, this is only implemented in the CLI.
204202

205203
```
206204
// return `true` in case no errors have been found, `false` in case of errors.

src/SpecBaseObject.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,21 +343,37 @@ public function __unset($name)
343343
*/
344344
public function resolveReferences(ReferenceContext $context = null)
345345
{
346+
// avoid recursion to get stuck in a loop
347+
if ($this->_recursing) {
348+
return;
349+
}
350+
$this->_recursing = true;
351+
346352
foreach ($this->_properties as $property => $value) {
347353
if ($value instanceof Reference) {
348-
$this->_properties[$property] = $value->resolve($context);
354+
$referencedObject = $value->resolve($context);
355+
$this->_properties[$property] = $referencedObject;
356+
if (!$referencedObject instanceof Reference && $referencedObject !== null) {
357+
$referencedObject->resolveReferences();
358+
}
349359
} elseif ($value instanceof SpecObjectInterface) {
350360
$value->resolveReferences($context);
351361
} elseif (is_array($value)) {
352362
foreach ($value as $k => $item) {
353363
if ($item instanceof Reference) {
354-
$this->_properties[$property][$k] = $item->resolve($context);
364+
$referencedObject = $item->resolve($context);
365+
$this->_properties[$property][$k] = $referencedObject;
366+
if (!$referencedObject instanceof Reference && $referencedObject !== null) {
367+
$referencedObject->resolveReferences();
368+
}
355369
} elseif ($item instanceof SpecObjectInterface) {
356370
$item->resolveReferences($context);
357371
}
358372
}
359373
}
360374
}
375+
376+
$this->_recursing = false;
361377
}
362378

363379
/**

src/spec/Operation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* @property string $operationId
2222
* @property Parameter[]|Reference[] $parameters
2323
* @property RequestBody|Reference|null $requestBody
24-
* @property Responses|null $responses
24+
* @property Responses|Response[]|null $responses
2525
* @property Callback[]|Reference[] $callbacks
2626
* @property bool $deprecated
2727
* @property SecurityRequirement[] $security

src/spec/PathItem.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,30 @@ public function resolveReferences(ReferenceContext $context = null)
159159
$this->addError("Conflicting properties, property '$attribute' exists in local PathItem and also in the referenced one.");
160160
}
161161
$this->$attribute = $pathItem->$attribute;
162+
163+
// resolve references in all properties assinged from the reference
164+
// use the referenced object context in this case
165+
if ($this->$attribute instanceof Reference) {
166+
$referencedObject = $this->$attribute->resolve();
167+
$this->$attribute = $referencedObject;
168+
if (!$referencedObject instanceof Reference && $referencedObject !== null) {
169+
$referencedObject->resolveReferences();
170+
}
171+
} elseif ($this->$attribute instanceof SpecObjectInterface) {
172+
$this->$attribute->resolveReferences();
173+
} elseif (is_array($this->$attribute)) {
174+
foreach ($this->$attribute as $k => $item) {
175+
if ($item instanceof Reference) {
176+
$referencedObject = $item->resolve();
177+
$this->$attribute[$k] = $referencedObject;
178+
if (!$referencedObject instanceof Reference && $referencedObject !== null) {
179+
$referencedObject->resolveReferences();
180+
}
181+
} elseif ($item instanceof SpecObjectInterface) {
182+
$item->resolveReferences();
183+
}
184+
}
185+
}
162186
}
163187
}
164188
parent::resolveReferences($context);

src/spec/Reference.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ public function getContext() : ?ReferenceContext
146146
* If not specified, `getContext()` will be called to determine the context, if
147147
* that does not return a context, the UnresolvableReferenceException will be thrown.
148148
* @return SpecObjectInterface the resolved spec type.
149+
* You might want to call resolveReferences() on the resolved object to recursively resolve recursive references.
150+
* This is not done automatically to avoid recursion to run into the same function again.
151+
* If you call resolveReferences() make sure to replace the Reference with the resolved object first.
149152
* @throws UnresolvableReferenceException in case of errors.
150153
*/
151154
public function resolve(ReferenceContext $context = null)
@@ -169,7 +172,10 @@ public function resolve(ReferenceContext $context = null)
169172
$baseSpec = $context->getBaseSpec();
170173
if ($baseSpec !== null) {
171174
// TODO type error if resolved object does not match $this->_to ?
172-
return $jsonReference->getJsonPointer()->evaluate($baseSpec);
175+
/** @var $referencedObject SpecObjectInterface */
176+
$referencedObject = $jsonReference->getJsonPointer()->evaluate($baseSpec);
177+
$referencedObject->setReferenceContext($context);
178+
return $referencedObject;
173179
} else {
174180
// if current document was loaded via reference, it may be null,
175181
// so we load current document by URI instead.
@@ -189,15 +195,19 @@ public function resolve(ReferenceContext $context = null)
189195
/** @var $referencedObject SpecObjectInterface */
190196
$referencedObject = new $this->_to($referencedData);
191197
if ($jsonReference->getJsonPointer()->getPointer() === '') {
192-
$referencedObject->setReferenceContext(new ReferenceContext($referencedObject, $file));
198+
$newContext = new ReferenceContext($referencedObject, $file);
199+
$newContext->throwException = $context->throwException;
200+
$referencedObject->setReferenceContext($newContext);
193201
if ($referencedObject instanceof DocumentContextInterface) {
194202
$referencedObject->setDocumentContext($referencedObject, $jsonReference->getJsonPointer());
195203
}
196204
} else {
197205
// resolving references recursively does not work the same if we have not referenced
198206
// the whole document. We do not know the base type of the file at this point,
199207
// so base document must be null.
200-
$referencedObject->setReferenceContext(new ReferenceContext(null, $file));
208+
$newContext = new ReferenceContext(null, $file);
209+
$newContext->throwException = $context->throwException;
210+
$referencedObject->setReferenceContext($newContext);
201211
}
202212

203213
return $referencedObject;

src/spec/Responses.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,11 @@ public function resolveReferences(ReferenceContext $context = null)
239239
{
240240
foreach ($this->_responses as $key => $response) {
241241
if ($response instanceof Reference) {
242-
$this->_responses[$key] = $response->resolve($context);
242+
$referencedObject = $response->resolve($context);
243+
$this->_responses[$key] = $referencedObject;
244+
if (!$referencedObject instanceof Reference && $referencedObject !== null) {
245+
$referencedObject->resolveReferences();
246+
}
243247
} else {
244248
$response->resolveReferences($context);
245249
}

tests/spec/ReferenceTest.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,43 @@ public function testResolveFile()
162162

163163
// second level reference inside of definitions.yaml
164164
$this->assertArrayHasKey('food', $openapi->components->schemas['Dog']->properties);
165-
$this->assertInstanceOf(Reference::class, $openapi->components->schemas['Dog']->properties['food']);
165+
$this->assertInstanceOf(Schema::class, $openapi->components->schemas['Dog']->properties['food']);
166+
$this->assertArrayHasKey('id', $openapi->components->schemas['Dog']->properties['food']->properties);
167+
$this->assertArrayHasKey('name', $openapi->components->schemas['Dog']->properties['food']->properties);
168+
$this->assertEquals(1, $openapi->components->schemas['Dog']->properties['food']->properties['id']->example);
169+
}
170+
171+
public function testResolveFileInSubdir()
172+
{
173+
$file = __DIR__ . '/data/reference/subdir.yaml';
174+
/** @var $openapi OpenApi */
175+
$openapi = Reader::readFromYamlFile($file, OpenApi::class, false);
176+
177+
$result = $openapi->validate();
178+
$this->assertEquals([], $openapi->getErrors());
179+
$this->assertTrue($result);
180+
181+
$this->assertInstanceOf(Reference::class, $petItems = $openapi->components->schemas['Pet']);
182+
$this->assertInstanceOf(Reference::class, $petItems = $openapi->components->schemas['Dog']);
166183

167184
$openapi->resolveReferences(new \cebe\openapi\ReferenceContext($openapi, $file));
168185

186+
$this->assertInstanceOf(Schema::class, $petItems = $openapi->components->schemas['Pet']);
187+
$this->assertInstanceOf(Schema::class, $petItems = $openapi->components->schemas['Dog']);
188+
$this->assertArrayHasKey('id', $openapi->components->schemas['Pet']->properties);
189+
$this->assertArrayHasKey('name', $openapi->components->schemas['Dog']->properties);
190+
191+
// second level reference inside of definitions.yaml
169192
$this->assertArrayHasKey('food', $openapi->components->schemas['Dog']->properties);
170193
$this->assertInstanceOf(Schema::class, $openapi->components->schemas['Dog']->properties['food']);
171194
$this->assertArrayHasKey('id', $openapi->components->schemas['Dog']->properties['food']->properties);
172195
$this->assertArrayHasKey('name', $openapi->components->schemas['Dog']->properties['food']->properties);
173196
$this->assertEquals(1, $openapi->components->schemas['Dog']->properties['food']->properties['id']->example);
197+
198+
$this->assertEquals('return a pet', $openapi->paths->getPath('/pets')->get->responses[200]->description);
199+
$responseContent = $openapi->paths->getPath('/pets')->get->responses[200]->content['application/json'];
200+
$this->assertInstanceOf(Schema::class, $responseContent->schema);
201+
$this->assertEquals('A Pet', $responseContent->schema->description);
174202
}
175203

176204
public function testResolveFileHttp()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"get": {
3+
"responses": {
4+
"200": {
5+
"description": "return a pet",
6+
"content": {
7+
"application/json": {
8+
"schema": {"$ref": "../subdir/Pet.yaml"}
9+
}
10+
}
11+
}
12+
}
13+
}
14+
}

tests/spec/data/reference/subdir.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Link Example
4+
version: 1.0.0
5+
components:
6+
schemas:
7+
Pet:
8+
$ref: 'subdir/Pet.yaml'
9+
Dog:
10+
$ref: 'subdir/Dog.yaml'
11+
paths:
12+
'/pet':
13+
get:
14+
responses:
15+
200:
16+
description: return a pet
17+
'/pets':
18+
"$ref": "paths/pets.json"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
properties:
3+
name:
4+
type: string
5+
food:
6+
$ref: '../Food.yaml'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: object
2+
description: "A Pet"
3+
properties:
4+
id:
5+
type: integer
6+
format: int64

0 commit comments

Comments
 (0)