Skip to content

Commit 4cd7404

Browse files
committed
Improve error handling and document navigation
by providing each object with context information (base document and the JSON pointer to the objects position in the document).
1 parent 9d168be commit 4cd7404

9 files changed

Lines changed: 367 additions & 22 deletions

File tree

src/DocumentContextInterface.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace cebe\openapi;
4+
5+
use cebe\openapi\json\JsonPointer;
6+
7+
/**
8+
* Interface implemented by OpenAPI objects that provide functionality for context in the document.
9+
*
10+
* Allows an object to reference the base OpenAPI document as well as its own position inside of
11+
* the document in form of a [JSON pointer](https://tools.ietf.org/html/rfc6901).
12+
*/
13+
interface DocumentContextInterface
14+
{
15+
/**
16+
* Provide context information to the object.
17+
*
18+
* Context information contains a reference to the base object where it is contained in
19+
* as well as a JSON pointer to its position.
20+
* @param SpecObjectInterface $baseDocument
21+
* @param JsonPointer $jsonPointer
22+
*/
23+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer);
24+
25+
/**
26+
* @return SpecObjectInterface|null returns the base document where this object is located in.
27+
* Returns `null` if no context information was provided by [[setDocumentContext]].
28+
*/
29+
public function getBaseDocument(): ?SpecObjectInterface;
30+
/**
31+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
32+
* Returns `null` if no context information was provided by [[setDocumentContext]].
33+
*/
34+
public function getDocumentPosition(): ?JsonPointer;
35+
}

src/SpecBaseObject.php

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
namespace cebe\openapi;
99

10-
use cebe\openapi\exceptions\ReadonlyPropertyException;
1110
use cebe\openapi\exceptions\TypeErrorException;
1211
use cebe\openapi\exceptions\UnknownPropertyException;
12+
use cebe\openapi\json\JsonPointer;
1313
use cebe\openapi\spec\Reference;
1414
use cebe\openapi\spec\Type;
1515

@@ -19,11 +19,15 @@
1919
* Implements property management and validation basics.
2020
*
2121
*/
22-
abstract class SpecBaseObject implements SpecObjectInterface
22+
abstract class SpecBaseObject implements SpecObjectInterface, DocumentContextInterface
2323
{
2424
private $_properties = [];
2525
private $_errors = [];
2626

27+
private $_baseDocument;
28+
private $_jsonPointer;
29+
30+
2731
/**
2832
* @return array array of attributes available in this object.
2933
*/
@@ -192,7 +196,15 @@ public function validate(): bool
192196
*/
193197
public function getErrors(): array
194198
{
195-
$errors = [$this->_errors];
199+
if (($pos = $this->getDocumentPosition()) !== null) {
200+
$errors = [
201+
array_map(function($e) use ($pos) {
202+
return "[{$pos->getPointer()}] $e";
203+
}, $this->_errors)
204+
];
205+
} else {
206+
$errors = [$this->_errors];
207+
}
196208
foreach ($this->_properties as $v) {
197209
if ($v instanceof SpecObjectInterface) {
198210
$errors[] = $v->getErrors();
@@ -326,4 +338,48 @@ public function setReferenceContext(ReferenceContext $context)
326338
}
327339
}
328340
}
341+
342+
/**
343+
* Provide context information to the object.
344+
*
345+
* Context information contains a reference to the base object where it is contained in
346+
* as well as a JSON pointer to its position.
347+
* @param SpecObjectInterface $baseDocument
348+
* @param JsonPointer $jsonPointer
349+
*/
350+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
351+
{
352+
$this->_baseDocument = $baseDocument;
353+
$this->_jsonPointer = $jsonPointer;
354+
355+
foreach ($this->_properties as $property => $value) {
356+
if ($value instanceof DocumentContextInterface) {
357+
$value->setDocumentContext($baseDocument, $jsonPointer->append($property));
358+
} elseif (is_array($value)) {
359+
foreach ($value as $k => $item) {
360+
if ($item instanceof DocumentContextInterface) {
361+
$item->setDocumentContext($baseDocument, $jsonPointer->append($property)->append($k));
362+
}
363+
}
364+
}
365+
}
366+
}
367+
368+
/**
369+
* @return SpecObjectInterface|null returns the base document where this object is located in.
370+
* Returns `null` if no context information was provided by [[setDocumentContext]].
371+
*/
372+
public function getBaseDocument(): ?SpecObjectInterface
373+
{
374+
return $this->_baseDocument;
375+
}
376+
377+
/**
378+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
379+
* Returns `null` if no context information was provided by [[setDocumentContext]].
380+
*/
381+
public function getDocumentPosition(): ?JsonPointer
382+
{
383+
return $this->_jsonPointer;
384+
}
329385
}

src/json/JsonPointer.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public function __construct(string $pointer)
3232
$this->_pointer = $pointer;
3333
}
3434

35+
public function __toString()
36+
{
37+
return $this->_pointer;
38+
}
39+
3540
/**
3641
* @return string returns the JSON Pointer.
3742
*/
@@ -52,6 +57,34 @@ public function getPath(): array
5257
return array_map([get_class($this), 'decode'], explode('/', $pointer));
5358
}
5459

60+
/**
61+
* Append a new part to the JSON path.
62+
* @param string $subpath the path element to append.
63+
* @return JsonPointer a new JSON pointer pointing to the subpath.
64+
*/
65+
public function append(string $subpath): JsonPointer
66+
{
67+
return new JsonPointer($this->_pointer . '/' . static::encode($subpath));
68+
}
69+
70+
/**
71+
* Returns a JSON pointer to the parent path element of this pointer.
72+
* @return JsonPointer|null a new JSON pointer pointing to the parent element
73+
* or null if this pointer already points to the document root.
74+
*/
75+
public function parent(): ?JsonPointer
76+
{
77+
$path = $this->getPath();
78+
if (empty($path)) {
79+
return null;
80+
}
81+
array_pop($path);
82+
if (empty($path)) {
83+
return new JsonPointer('');
84+
}
85+
return new JsonPointer('/' . implode('/', array_map([get_class($this), 'encode'], $path)));
86+
}
87+
5588
/**
5689
* Evaluate the JSON Pointer on the provided document.
5790
*

src/spec/Callback.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
namespace cebe\openapi\spec;
99

10+
use cebe\openapi\DocumentContextInterface;
1011
use cebe\openapi\exceptions\TypeErrorException;
1112
use cebe\openapi\exceptions\UnresolvableReferenceException;
13+
use cebe\openapi\json\JsonPointer;
1214
use cebe\openapi\ReferenceContext;
1315
use cebe\openapi\SpecObjectInterface;
1416

@@ -18,13 +20,16 @@
1820
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#callbackObject
1921
*
2022
*/
21-
class Callback implements SpecObjectInterface
23+
class Callback implements SpecObjectInterface, DocumentContextInterface
2224
{
2325
private $_url;
2426
private $_pathItem;
2527

2628
private $_errors = [];
2729

30+
private $_baseDocument;
31+
private $_jsonPointer;
32+
2833

2934
/**
3035
* Create an object from spec data.
@@ -99,8 +104,16 @@ public function validate(): bool
99104
*/
100105
public function getErrors(): array
101106
{
107+
if (($pos = $this->getDocumentPosition()) !== null) {
108+
$errors = array_map(function($e) use ($pos) {
109+
return "[{$pos}] $e";
110+
}, $this->_errors);
111+
} else {
112+
$errors = $this->_errors;
113+
}
114+
102115
$pathItemErrors = $this->_pathItem === null ? [] : $this->_pathItem->getErrors();
103-
return array_merge($this->_errors, $pathItemErrors);
116+
return array_merge($errors, $pathItemErrors);
104117
}
105118

106119
/**
@@ -123,4 +136,40 @@ public function setReferenceContext(ReferenceContext $context)
123136
$this->_pathItem->setReferenceContext($context);
124137
}
125138
}
139+
140+
/**
141+
* Provide context information to the object.
142+
*
143+
* Context information contains a reference to the base object where it is contained in
144+
* as well as a JSON pointer to its position.
145+
* @param SpecObjectInterface $baseDocument
146+
* @param JsonPointer $jsonPointer
147+
*/
148+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
149+
{
150+
$this->_baseDocument = $baseDocument;
151+
$this->_jsonPointer = $jsonPointer;
152+
153+
if ($this->_pathItem instanceof DocumentContextInterface) {
154+
$this->_pathItem->setDocumentContext($baseDocument, $jsonPointer->append($this->_url));
155+
}
156+
}
157+
158+
/**
159+
* @return SpecObjectInterface|null returns the base document where this object is located in.
160+
* Returns `null` if no context information was provided by [[setDocumentContext]].
161+
*/
162+
public function getBaseDocument(): ?SpecObjectInterface
163+
{
164+
return $this->_baseDocument;
165+
}
166+
167+
/**
168+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
169+
* Returns `null` if no context information was provided by [[setDocumentContext]].
170+
*/
171+
public function getDocumentPosition(): ?JsonPointer
172+
{
173+
return $this->_jsonPointer;
174+
}
126175
}

src/spec/Paths.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
use ArrayAccess;
1111
use ArrayIterator;
12+
use cebe\openapi\DocumentContextInterface;
1213
use cebe\openapi\exceptions\ReadonlyPropertyException;
1314
use cebe\openapi\exceptions\TypeErrorException;
1415
use cebe\openapi\exceptions\UnresolvableReferenceException;
16+
use cebe\openapi\json\JsonPointer;
1517
use cebe\openapi\ReferenceContext;
1618
use cebe\openapi\SpecObjectInterface;
1719
use Countable;
@@ -27,7 +29,7 @@
2729
* @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#pathsObject
2830
*
2931
*/
30-
class Paths implements SpecObjectInterface, ArrayAccess, Countable, IteratorAggregate
32+
class Paths implements SpecObjectInterface, DocumentContextInterface, ArrayAccess, Countable, IteratorAggregate
3133
{
3234
/**
3335
* @var PathItem[]
@@ -36,6 +38,9 @@ class Paths implements SpecObjectInterface, ArrayAccess, Countable, IteratorAggr
3638

3739
private $_errors = [];
3840

41+
private $_baseDocument;
42+
private $_jsonPointer;
43+
3944

4045
/**
4146
* Create an object from spec data.
@@ -138,7 +143,16 @@ public function validate(): bool
138143
*/
139144
public function getErrors(): array
140145
{
141-
$errors = [$this->_errors];
146+
if (($pos = $this->getDocumentPosition()) !== null) {
147+
$errors = [
148+
array_map(function($e) use ($pos) {
149+
return "[{$pos}] $e";
150+
}, $this->_errors)
151+
];
152+
} else {
153+
$errors = [$this->_errors];
154+
}
155+
142156
foreach ($this->_paths as $path) {
143157
if ($path === null) {
144158
continue;
@@ -241,4 +255,42 @@ public function setReferenceContext(ReferenceContext $context)
241255
$path->setReferenceContext($context);
242256
}
243257
}
258+
259+
/**
260+
* Provide context information to the object.
261+
*
262+
* Context information contains a reference to the base object where it is contained in
263+
* as well as a JSON pointer to its position.
264+
* @param SpecObjectInterface $baseDocument
265+
* @param JsonPointer $jsonPointer
266+
*/
267+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
268+
{
269+
$this->_baseDocument = $baseDocument;
270+
$this->_jsonPointer = $jsonPointer;
271+
272+
foreach ($this->_paths as $key => $path) {
273+
if ($path instanceof DocumentContextInterface) {
274+
$path->setDocumentContext($baseDocument, $jsonPointer->append($key));
275+
}
276+
}
277+
}
278+
279+
/**
280+
* @return SpecObjectInterface|null returns the base document where this object is located in.
281+
* Returns `null` if no context information was provided by [[setDocumentContext]].
282+
*/
283+
public function getBaseDocument(): ?SpecObjectInterface
284+
{
285+
return $this->_baseDocument;
286+
}
287+
288+
/**
289+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
290+
* Returns `null` if no context information was provided by [[setDocumentContext]].
291+
*/
292+
public function getDocumentPosition(): ?JsonPointer
293+
{
294+
return $this->_jsonPointer;
295+
}
244296
}

0 commit comments

Comments
 (0)