Skip to content

Commit 8142289

Browse files
committed
Add support for WebHooks
1 parent 4fcc155 commit 8142289

File tree

8 files changed

+474
-5
lines changed

8 files changed

+474
-5
lines changed

src/SpecBaseObject.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,23 @@ protected function hasProperty(string $name): bool
302302
return isset($this->_properties[$name]) || isset($this->attributes()[$name]);
303303
}
304304

305-
protected function requireProperties(array $names)
305+
protected function requireProperties(array $names, array $atLeastOne = [])
306306
{
307307
foreach ($names as $name) {
308308
if (!isset($this->_properties[$name])) {
309309
$this->addError(" is missing required property: $name", get_class($this));
310310
}
311311
}
312+
313+
if (count($atLeastOne) > 0) {
314+
foreach ($atLeastOne as $name) {
315+
if (array_key_exists($name, $this->_properties)) {
316+
return;
317+
}
318+
}
319+
320+
$this->addError(" is missing at least one of the following required properties: " . implode(', ', $atLeastOne), get_class($this));
321+
}
312322
}
313323

314324
protected function validateEmail(string $property)

src/spec/OpenApi.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* @property Server[] $servers
2121
* @property Paths|PathItem[] $paths
2222
* @property Components|null $components
23+
* @property WebHooks|null $webhooks
2324
* @property SecurityRequirement[] $security
2425
* @property Tag[] $tags
2526
* @property ExternalDocumentation|null $externalDocs
@@ -46,6 +47,7 @@ protected function attributes(): array
4647
'info' => Info::class,
4748
'servers' => [Server::class],
4849
'paths' => Paths::class,
50+
'webhooks' => WebHooks::class,
4951
'components' => Components::class,
5052
'security' => [SecurityRequirement::class],
5153
'tags' => [Tag::class],
@@ -83,7 +85,12 @@ public function __get($name)
8385
*/
8486
public function performValidation()
8587
{
86-
$this->requireProperties(['openapi', 'info', 'paths']);
88+
if ($this->getMajorVersion() === static::VERSION_3_0) {
89+
$this->requireProperties(['openapi', 'info', 'paths']);
90+
} else {
91+
$this->requireProperties(['openapi', 'info'], ['paths', 'webhooks', 'components']);
92+
}
93+
8794
if (!empty($this->openapi) && !preg_match(static::PATTERN_VERSION, $this->openapi)) {
8895
$this->addError('Unsupported openapi version: ' . $this->openapi);
8996
}

src/spec/Schema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
* @property string[] $required list of required properties
4141
* @property array $enum
4242
*
43-
* @property string $type
43+
* @property string|string[] $type
4444
* @property Schema[]|Reference[] $allOf
4545
* @property Schema[]|Reference[] $oneOf
4646
* @property Schema[]|Reference[] $anyOf

src/spec/Type.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Type
2121
const BOOLEAN = 'boolean';
2222
const OBJECT = 'object';
2323
const ARRAY = 'array';
24+
const NULL = 'null';
2425

2526
/**
2627
* Indicate whether a type is a scalar type, i.e. not an array or object.
@@ -38,6 +39,7 @@ public static function isScalar(string $type): bool
3839
self::NUMBER,
3940
self::STRING,
4041
self::BOOLEAN,
42+
self::NULL,
4143
]);
4244
}
4345
}

src/spec/WebHooks.php

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (c) 2021 Carsten Brandt <[email protected]> and contributors
5+
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
6+
*/
7+
8+
namespace cebe\openapi\spec;
9+
10+
use ArrayAccess;
11+
use ArrayIterator;
12+
use cebe\openapi\DocumentContextInterface;
13+
use cebe\openapi\exceptions\TypeErrorException;
14+
use cebe\openapi\exceptions\UnresolvableReferenceException;
15+
use cebe\openapi\json\JsonPointer;
16+
use cebe\openapi\ReferenceContext;
17+
use cebe\openapi\SpecObjectInterface;
18+
use Countable;
19+
use IteratorAggregate;
20+
use Traversable;
21+
22+
/**
23+
* Holds the webhook events to the individual endpoints and their operations.
24+
*
25+
* @link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#oasWebhooks
26+
*
27+
*/
28+
class WebHooks implements SpecObjectInterface, DocumentContextInterface, ArrayAccess, Countable, IteratorAggregate
29+
{
30+
/**
31+
* @var (PathItem|null)[]
32+
*/
33+
private $_webHooks = [];
34+
/**
35+
* @var array
36+
*/
37+
private $_errors = [];
38+
/**
39+
* @var SpecObjectInterface|null
40+
*/
41+
private $_baseDocument;
42+
/**
43+
* @var JsonPointer|null
44+
*/
45+
private $_jsonPointer;
46+
47+
48+
/**
49+
* Create an object from spec data.
50+
* @param (PathItem|array|null)[] $data spec data read from YAML or JSON
51+
* @throws TypeErrorException in case invalid data is supplied.
52+
*/
53+
public function __construct(array $data)
54+
{
55+
foreach ($data as $path => $object) {
56+
if ($object === null) {
57+
$this->_webHooks[$path] = null;
58+
} elseif (is_array($object)) {
59+
$this->_webHooks[$path] = new PathItem($object);
60+
} elseif ($object instanceof PathItem) {
61+
$this->_webHooks[$path] = $object;
62+
} else {
63+
$givenType = gettype($object);
64+
if ($givenType === 'object') {
65+
$givenType = get_class($object);
66+
}
67+
throw new TypeErrorException(sprintf('Path MUST be either array or PathItem object, "%s" given', $givenType));
68+
}
69+
}
70+
}
71+
72+
/**
73+
* @return mixed returns the serializable data of this object for converting it
74+
* to JSON or YAML.
75+
*/
76+
public function getSerializableData()
77+
{
78+
$data = [];
79+
foreach ($this->_webHooks as $path => $pathItem) {
80+
$data[$path] = ($pathItem === null) ? null : $pathItem->getSerializableData();
81+
}
82+
return (object) $data;
83+
}
84+
85+
/**
86+
* @param string $name path name
87+
* @return bool
88+
*/
89+
public function hasWebHook(string $name): bool
90+
{
91+
return isset($this->_webHooks[$name]);
92+
}
93+
94+
/**
95+
* @param string $name path name
96+
* @return PathItem
97+
*/
98+
public function getWebHook(string $name): ?PathItem
99+
{
100+
return $this->_webHooks[$name] ?? null;
101+
}
102+
103+
/**
104+
* @param string $name path name
105+
* @param PathItem $pathItem the path item to add
106+
*/
107+
public function addWebHook(string $name, PathItem $pathItem): void
108+
{
109+
$this->_webHooks[$name] = $pathItem;
110+
}
111+
112+
/**
113+
* @param string $name path name
114+
*/
115+
public function removeWebHook(string $name): void
116+
{
117+
unset($this->_webHooks[$name]);
118+
}
119+
120+
/**
121+
* @return PathItem[]
122+
*/
123+
public function getWebHooks(): array
124+
{
125+
return $this->_webHooks;
126+
}
127+
128+
/**
129+
* Validate object data according to OpenAPI spec.
130+
* @return bool whether the loaded data is valid according to OpenAPI spec
131+
* @see getErrors()
132+
*/
133+
public function validate(): bool
134+
{
135+
$valid = true;
136+
$this->_errors = [];
137+
foreach ($this->_webHooks as $key => $path) {
138+
if ($path === null) {
139+
continue;
140+
}
141+
if (!$path->validate()) {
142+
$valid = false;
143+
}
144+
}
145+
return $valid && empty($this->_errors);
146+
}
147+
148+
/**
149+
* @return string[] list of validation errors according to OpenAPI spec.
150+
* @see validate()
151+
*/
152+
public function getErrors(): array
153+
{
154+
if (($pos = $this->getDocumentPosition()) !== null) {
155+
$errors = [
156+
array_map(function ($e) use ($pos) {
157+
return "[{$pos}] $e";
158+
}, $this->_errors)
159+
];
160+
} else {
161+
$errors = [$this->_errors];
162+
}
163+
164+
foreach ($this->_webHooks as $path) {
165+
if ($path === null) {
166+
continue;
167+
}
168+
$errors[] = $path->getErrors();
169+
}
170+
return array_merge(...$errors);
171+
}
172+
173+
/**
174+
* Whether a offset exists
175+
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
176+
* @param mixed $offset An offset to check for.
177+
* @return boolean true on success or false on failure.
178+
* The return value will be casted to boolean if non-boolean was returned.
179+
*/
180+
public function offsetExists($offset)
181+
{
182+
return $this->hasWebHook($offset);
183+
}
184+
185+
/**
186+
* Offset to retrieve
187+
* @link http://php.net/manual/en/arrayaccess.offsetget.php
188+
* @param mixed $offset The offset to retrieve.
189+
* @return PathItem Can return all value types.
190+
*/
191+
public function offsetGet($offset)
192+
{
193+
return $this->getWebHook($offset);
194+
}
195+
196+
/**
197+
* Offset to set
198+
* @link http://php.net/manual/en/arrayaccess.offsetset.php
199+
* @param mixed $offset The offset to assign the value to.
200+
* @param mixed $value The value to set.
201+
*/
202+
public function offsetSet($offset, $value)
203+
{
204+
$this->addWebHook($offset, $value);
205+
}
206+
207+
/**
208+
* Offset to unset
209+
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
210+
* @param mixed $offset The offset to unset.
211+
*/
212+
public function offsetUnset($offset)
213+
{
214+
$this->removeWebHook($offset);
215+
}
216+
217+
/**
218+
* Count elements of an object
219+
* @link http://php.net/manual/en/countable.count.php
220+
* @return int The custom count as an integer.
221+
* The return value is cast to an integer.
222+
*/
223+
public function count()
224+
{
225+
return count($this->_webHooks);
226+
}
227+
228+
/**
229+
* Retrieve an external iterator
230+
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
231+
* @return Traversable An instance of an object implementing <b>Iterator</b> or <b>Traversable</b>
232+
*/
233+
public function getIterator()
234+
{
235+
return new ArrayIterator($this->_webHooks);
236+
}
237+
238+
/**
239+
* Resolves all Reference Objects in this object and replaces them with their resolution.
240+
* @throws UnresolvableReferenceException
241+
*/
242+
public function resolveReferences(ReferenceContext $context = null)
243+
{
244+
foreach ($this->_webHooks as $key => $path) {
245+
if ($path === null) {
246+
continue;
247+
}
248+
$path->resolveReferences($context);
249+
}
250+
}
251+
252+
/**
253+
* Set context for all Reference Objects in this object.
254+
*/
255+
public function setReferenceContext(ReferenceContext $context)
256+
{
257+
foreach ($this->_webHooks as $key => $path) {
258+
if ($path === null) {
259+
continue;
260+
}
261+
$path->setReferenceContext($context);
262+
}
263+
}
264+
265+
/**
266+
* Provide context information to the object.
267+
*
268+
* Context information contains a reference to the base object where it is contained in
269+
* as well as a JSON pointer to its position.
270+
* @param SpecObjectInterface $baseDocument
271+
* @param JsonPointer $jsonPointer
272+
*/
273+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
274+
{
275+
$this->_baseDocument = $baseDocument;
276+
$this->_jsonPointer = $jsonPointer;
277+
278+
foreach ($this->_webHooks as $key => $path) {
279+
if ($path instanceof DocumentContextInterface) {
280+
$path->setDocumentContext($baseDocument, $jsonPointer->append($key));
281+
}
282+
}
283+
}
284+
285+
/**
286+
* @return SpecObjectInterface|null returns the base document where this object is located in.
287+
* Returns `null` if no context information was provided by [[setDocumentContext]].
288+
*/
289+
public function getBaseDocument(): ?SpecObjectInterface
290+
{
291+
return $this->_baseDocument;
292+
}
293+
294+
/**
295+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
296+
* Returns `null` if no context information was provided by [[setDocumentContext]].
297+
*/
298+
public function getDocumentPosition(): ?JsonPointer
299+
{
300+
return $this->_jsonPointer;
301+
}
302+
}

tests/spec/OpenApiTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function testEmpty()
1717
$this->assertEquals([
1818
'OpenApi is missing required property: openapi',
1919
'OpenApi is missing required property: info',
20-
'OpenApi is missing required property: paths',
20+
'OpenApi is missing at least one of the following required properties: paths, webhooks, components',
2121
], $openapi->getErrors());
2222

2323
// check default value of servers

0 commit comments

Comments
 (0)