Skip to content

Commit 4462172

Browse files
committed
Merge remote-tracking branch 'origin/master' into feature/pass-objects
2 parents 6973170 + 855aab5 commit 4462172

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2843
-265
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/vendor
22
/composer.lock
33

4+
/node_modules
45

56
/.php_cs.cache

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ matrix:
3232

3333
install: make install
3434
script:
35+
- make lint
3536
- make test
3637
- if [[ $TRAVIS_PHP_VERSION = "7.3" || $TRAVIS_PHP_VERSION = "nightly" ]]; then true; else make check-style; fi
3738
- make coverage

Makefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
TESTCASE=
2+
PHPARGS=
3+
#PHPARGS=-dzend_extension=xdebug.so -dxdebug.remote_enable=1
14

25
all:
36

@@ -11,9 +14,16 @@ fix-style:
1114

1215
install:
1316
composer install --prefer-dist --no-interaction
17+
yarn install
1418

1519
test:
16-
vendor/bin/phpunit
20+
php $(PHPARGS) vendor/bin/phpunit $(TESTCASE)
21+
22+
lint:
23+
php $(PHPARGS) bin/php-openapi validate tests/spec/data/reference/playlist.json
24+
php $(PHPARGS) bin/php-openapi validate tests/spec/data/recursion.json
25+
node_modules/.bin/speccy lint tests/spec/data/reference/playlist.json
26+
node_modules/.bin/speccy lint tests/spec/data/recursion.json
1727

1828
# copy openapi3 json schema
1929
schemas/openapi-v3.0.json: vendor/oai/openapi-specification/schemas/v3.0/schema.json
@@ -32,5 +42,5 @@ coverage: .php-openapi-covA .php-openapi-covB
3242
.php-openapi-covB:
3343
grep -rhPo '^class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@
3444

35-
.PHONY: all check-style fix-style install test coverage
45+
.PHONY: all check-style fix-style install test lint coverage
3646

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Read and write [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files an
55
It also provides a CLI tool for validating and converting OpenAPI 3.0.x YAML and JSON files.
66

77
[![Latest Stable Version](https://poser.pugx.org/cebe/php-openapi/v/stable)](https://packagist.org/packages/cebe/php-openapi)
8+
[![Total Downloads](https://poser.pugx.org/cebe/php-openapi/downloads)](https://packagist.org/packages/cebe/php-openapi)
89
[![Build Status](https://travis-ci.org/cebe/php-openapi.svg?branch=master)](https://travis-ci.org/cebe/php-openapi)
910
[![License](https://poser.pugx.org/cebe/php-openapi/license)](https://packagist.org/packages/cebe/php-openapi)
1011

@@ -227,6 +228,8 @@ This library is currently work in progress, the following list tracks completene
227228

228229
# Support
229230

231+
**Need help with your API project?**
232+
230233
Professional support, consulting as well as software development services are available:
231234

232235
https://www.cebe.cc/en/contact

bin/php-openapi

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
99
*/
1010

11+
use cebe\openapi\ReferenceContext;
12+
1113
$composerAutoload = [
1214
__DIR__ . '/../vendor/autoload.php', // standalone with "composer install" run
1315
__DIR__ . '/../../../autoload.php', // script is installed as a composer binary
@@ -97,15 +99,21 @@ foreach($argv as $k => $arg) {
9799
switch ($command) {
98100
case 'validate':
99101

102+
$errors = [];
103+
100104
$openApi = read_input($inputFile, $inputFormat);
105+
$referenceContext = new ReferenceContext($openApi, $inputFile ? realpath($inputFile) : '');
106+
$referenceContext->throwException = false;
107+
$openApi->resolveReferences($referenceContext);
101108

102109
// Validate
103110

104111
$openApi->validate();
105-
$errors = $openApi->getErrors();
112+
$errors = array_merge($errors, $openApi->getErrors());
106113

107114
$validator = new JsonSchema\Validator;
108-
$validator->validate($openApi->getSerializableData(), (object)['$ref' => 'file://' . dirname(__DIR__) . '/schemas/openapi-v3.0.json']);
115+
$openApiData = $openApi->getSerializableData();
116+
$validator->validate($openApiData, (object)['$ref' => 'file://' . dirname(__DIR__) . '/schemas/openapi-v3.0.json']);
109117

110118
if ($validator->isValid() && empty($errors)) {
111119
print_formatted("The supplied API Description \B\Gvalidates\C against the OpenAPI v3.0 schema.\n", STDERR);
@@ -115,13 +123,20 @@ switch ($command) {
115123
if (!empty($errors)) {
116124
print_formatted("\BErrors found while reading the API Description:\C\n", STDERR);
117125
foreach ($errors as $error) {
118-
fwrite(STDERR, "- $error\n");
126+
if (($openPos = strpos($error, '[')) !== false && ($closePos = strpos($error, ']')) !== false && $openPos < $closePos) {
127+
$error = escape_formatted(substr($error, 0, $openPos + 1)) . '\Y'
128+
. escape_formatted(substr($error, $openPos + 1, $closePos - $openPos - 1)) . '\C'
129+
. escape_formatted(substr($error, $closePos));
130+
} else {
131+
$error = escape_formatted($error);
132+
}
133+
print_formatted("- " . $error . "\n", STDERR);
119134
}
120135
}
121136
if (!$validator->isValid()) {
122137
print_formatted("\BOpenAPI v3.0 schema violations:\C\n", STDERR);
123138
foreach ($validator->getErrors() as $error) {
124-
print_formatted(sprintf("- [\Y%s\C] %s\n", $error['property'], $error['message']), STDERR);
139+
print_formatted(sprintf("- [\Y%s\C] %s\n", escape_formatted($error['property']), escape_formatted($error['message'])), STDERR);
125140
}
126141
}
127142
exit(2);
@@ -130,6 +145,11 @@ switch ($command) {
130145
case 'convert':
131146

132147
$openApi = read_input($inputFile, $inputFormat);
148+
try {
149+
$openApi->resolveReferences();
150+
} catch (\cebe\openapi\exceptions\UnresolvableReferenceException $e) {
151+
error("[\e[33m{$e->context}\e[0m] " . $e->getMessage());
152+
}
133153

134154
if ($outputFile === null) {
135155
if ($outputFormat === null) {
@@ -202,11 +222,12 @@ function read_input($inputFile, $inputFormat)
202222
}
203223
}
204224
if ($inputFormat === 'json') {
205-
$openApi = \cebe\openapi\Reader::readFromJsonFile(realpath($inputFile));
225+
$openApi = \cebe\openapi\Reader::readFromJsonFile(realpath($inputFile), \cebe\openapi\spec\OpenApi::class, false);
206226
} else {
207-
$openApi = \cebe\openapi\Reader::readFromYamlFile(realpath($inputFile));
227+
$openApi = \cebe\openapi\Reader::readFromYamlFile(realpath($inputFile), \cebe\openapi\spec\OpenApi::class, false);
208228
}
209229
}
230+
$openApi->setDocumentContext($openApi, new \cebe\openapi\json\JsonPointer(''));
210231
} catch (Symfony\Component\Yaml\Exception\ParseException $e) {
211232
error($e->getMessage());
212233
exit(1);
@@ -269,7 +290,7 @@ EOF
269290
* @return void
270291
*/
271292
function error($message, $callback = null) {
272-
print_formatted("\B\RError\C: " . $message . "\n", STDERR);
293+
print_formatted("\B\RError\C: " . escape_formatted($message) . "\n", STDERR);
273294
if (is_callable($callback)) {
274295
call_user_func($callback);
275296
}
@@ -283,5 +304,10 @@ function print_formatted($string, $stream) {
283304
'\\R' => "\033[31m", // green
284305
'\\B' => "\033[1m", // bold
285306
'\\C' => "\033[0m", // clear
307+
'\\\\' => '\\',
286308
]));
287309
}
310+
311+
function escape_formatted($string) {
312+
return strtr($string, ['\\' => '\\\\']);
313+
}

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"speccy": "^0.11.0"
4+
}
5+
}

src/DocumentContextInterface.php

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

src/ReferenceContext.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
*/
1515
class ReferenceContext
1616
{
17+
/**
18+
* @var bool whether to throw UnresolvableReferenceException in case a reference can not
19+
* be resolved. If `false` errors are added to the Reference Objects error list instead.
20+
*/
21+
public $throwException = true;
1722
/**
1823
* @var SpecObjectInterface
1924
*/

src/SpecBaseObject.php

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
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;
13+
use cebe\openapi\json\JsonReference;
1314
use cebe\openapi\spec\Reference;
1415
use cebe\openapi\spec\Type;
1516

@@ -19,10 +20,15 @@
1920
* Implements property management and validation basics.
2021
*
2122
*/
22-
abstract class SpecBaseObject implements SpecObjectInterface
23+
abstract class SpecBaseObject implements SpecObjectInterface, DocumentContextInterface
2324
{
2425
private $_properties = [];
2526
private $_errors = [];
27+
private $_recursing = false;
28+
29+
private $_baseDocument;
30+
private $_jsonPointer;
31+
2632

2733
/**
2834
* @return array array of attributes available in this object.
@@ -146,6 +152,12 @@ private function instantiate($type, $data)
146152
*/
147153
public function getSerializableData()
148154
{
155+
if ($this->_recursing) {
156+
// return a reference
157+
return (object) ['$ref' => JsonReference::createFromUri('', $this->getDocumentPosition())->getReference()];
158+
}
159+
$this->_recursing = true;
160+
149161
$data = $this->_properties;
150162
foreach ($data as $k => $v) {
151163
if ($v instanceof SpecObjectInterface) {
@@ -166,6 +178,9 @@ public function getSerializableData()
166178
}
167179
}
168180
}
181+
182+
$this->_recursing = false;
183+
169184
return (object) $data;
170185
}
171186

@@ -176,19 +191,36 @@ public function getSerializableData()
176191
*/
177192
public function validate(): bool
178193
{
194+
// avoid recursion to get stuck in a loop
195+
if ($this->_recursing) {
196+
return true;
197+
}
198+
$this->_recursing = true;
199+
$valid = true;
179200
foreach ($this->_properties as $v) {
180201
if ($v instanceof SpecObjectInterface) {
181-
$v->validate();
202+
if (!$v->validate()) {
203+
$valid = false;
204+
}
182205
} elseif (is_array($v)) {
183206
foreach ($v as $item) {
184207
if ($item instanceof SpecObjectInterface) {
185-
$item->validate();
208+
if (!$item->validate()) {
209+
$valid = false;
210+
}
186211
}
187212
}
188213
}
189214
}
215+
$this->_recursing = false;
216+
190217
$this->performValidation();
191-
return \count($this->getErrors()) === 0;
218+
219+
if (!empty($this->_errors)) {
220+
$valid = false;
221+
}
222+
223+
return $valid;
192224
}
193225

194226
/**
@@ -197,7 +229,21 @@ public function validate(): bool
197229
*/
198230
public function getErrors(): array
199231
{
200-
$errors = [$this->_errors];
232+
// avoid recursion to get stuck in a loop
233+
if ($this->_recursing) {
234+
return [];
235+
}
236+
$this->_recursing = true;
237+
238+
if (($pos = $this->getDocumentPosition()) !== null) {
239+
$errors = [
240+
array_map(function ($e) use ($pos) {
241+
return "[{$pos->getPointer()}] $e";
242+
}, $this->_errors)
243+
];
244+
} else {
245+
$errors = [$this->_errors];
246+
}
201247
foreach ($this->_properties as $v) {
202248
if ($v instanceof SpecObjectInterface) {
203249
$errors[] = $v->getErrors();
@@ -209,6 +255,9 @@ public function getErrors(): array
209255
}
210256
}
211257
}
258+
259+
$this->_recursing = false;
260+
212261
return array_merge(...$errors);
213262
}
214263

@@ -331,4 +380,48 @@ public function setReferenceContext(ReferenceContext $context)
331380
}
332381
}
333382
}
383+
384+
/**
385+
* Provide context information to the object.
386+
*
387+
* Context information contains a reference to the base object where it is contained in
388+
* as well as a JSON pointer to its position.
389+
* @param SpecObjectInterface $baseDocument
390+
* @param JsonPointer $jsonPointer
391+
*/
392+
public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer)
393+
{
394+
$this->_baseDocument = $baseDocument;
395+
$this->_jsonPointer = $jsonPointer;
396+
397+
foreach ($this->_properties as $property => $value) {
398+
if ($value instanceof DocumentContextInterface) {
399+
$value->setDocumentContext($baseDocument, $jsonPointer->append($property));
400+
} elseif (is_array($value)) {
401+
foreach ($value as $k => $item) {
402+
if ($item instanceof DocumentContextInterface) {
403+
$item->setDocumentContext($baseDocument, $jsonPointer->append($property)->append($k));
404+
}
405+
}
406+
}
407+
}
408+
}
409+
410+
/**
411+
* @return SpecObjectInterface|null returns the base document where this object is located in.
412+
* Returns `null` if no context information was provided by [[setDocumentContext]].
413+
*/
414+
public function getBaseDocument(): ?SpecObjectInterface
415+
{
416+
return $this->_baseDocument;
417+
}
418+
419+
/**
420+
* @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document.
421+
* Returns `null` if no context information was provided by [[setDocumentContext]].
422+
*/
423+
public function getDocumentPosition(): ?JsonPointer
424+
{
425+
return $this->_jsonPointer;
426+
}
334427
}

0 commit comments

Comments
 (0)