Skip to content

Commit 4a4892d

Browse files
authored
Merge pull request #12 from cebe/references
WIP references
2 parents 161464f + 271bb4a commit 4a4892d

22 files changed

+792
-16
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ coverage: .php-openapi-covA .php-openapi-covB
2222
.php-openapi-covA:
2323
grep -rhPo '@covers .+' tests |cut -c 28- |sort > $@
2424
.php-openapi-covB:
25-
grep -rhPo 'class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@
25+
grep -rhPo '^class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@
2626

2727
.PHONY: all check-style fix-style install test coverage
2828

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ READ [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the
1717

1818
## Usage
1919

20+
### Reading Specification information
21+
2022
Read OpenAPI spec from JSON:
2123

2224
```php
@@ -46,6 +48,43 @@ foreach($openapi->paths as $path => $definition) {
4648
Object properties are exactly like in the [OpenAPI specification](https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#openapi-specification).
4749
You may also access additional properties added by specification extensions.
4850

51+
### Reading Specification Files and Resolving References
52+
53+
In the above we have passed the raw JSON or YAML data to the Reader. In order to be able to resolve
54+
references to external files that may exist in the specification files, we must provide the full context.
55+
56+
```php
57+
use cebe\openapi\Reader;
58+
// an absolute URL or file path is needed to allow resolving internal references
59+
$openapi = Reader::readFromJsonFile('https://www.example.com/api/openapi.json');
60+
$openapi = Reader::readFromYamlFile('https://www.example.com/api/openapi.yaml');
61+
```
62+
63+
If data has been loaded in a different way you can manually resolve references like this by giving a context:
64+
65+
```php
66+
$openapi->resolveReferences(
67+
new \cebe\openapi\ReferenceContext($openapi, 'https://www.example.com/api/openapi.yaml')
68+
);
69+
```
70+
71+
> **Note:** Resolving references currently does not deal with references in referenced files, you have to call it multiple times to resolve these.
72+
73+
### Validation
74+
75+
The library provides simple validation operations, that check basic OpenAPI spec requirements.
76+
77+
```
78+
// return `true` in case no errors have been found, `false` in case of errors.
79+
$specValid = $openapi->validate();
80+
// after validation getErrors() can be used to retrieve the list of errors found.
81+
$errors = $openapi->getErrors();
82+
```
83+
84+
> **Note:** Validation is done on a very basic level and is not complete. So a failing validation will show some errors,
85+
> but the list of errors given may not be complete. Also a passing validation does not necessarily indicate a completely
86+
> valid spec.
87+
4988

5089
## Completeness
5190

@@ -77,7 +116,7 @@ This library is currently work in progress, the following list tracks completene
77116
- [ ] [Runtime Expressions](https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#runtime-expressions)
78117
- [x] Header Object
79118
- [x] Tag Object
80-
- [ ] Reference Object
119+
- [x] Reference Object
81120
- [x] Schema Object
82121
- [x] load/read
83122
- [ ] validation

src/Reader.php

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,90 @@
77

88
namespace cebe\openapi;
99

10+
use cebe\openapi\exceptions\TypeErrorException;
11+
use cebe\openapi\exceptions\UnresolvableReferenceException;
1012
use cebe\openapi\spec\OpenApi;
1113
use Symfony\Component\Yaml\Yaml;
1214

1315
/**
14-
*
16+
* Utility class to simplify reading JSON or YAML OpenAPI specs.
1517
*
1618
*/
1719
class Reader
1820
{
21+
/**
22+
* Populate OpenAPI spec object from JSON data.
23+
* @param string $json the JSON string to decode.
24+
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
25+
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
26+
* You may choose a different type if you instantiate objects from sub sections of a specification.
27+
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
28+
* @throws TypeErrorException in case invalid spec data is supplied.
29+
*/
1930
public static function readFromJson(string $json, string $baseType = OpenApi::class): SpecObjectInterface
2031
{
2132
return new $baseType(json_decode($json, true));
2233
}
2334

35+
/**
36+
* Populate OpenAPI spec object from YAML data.
37+
* @param string $yaml the YAML string to decode.
38+
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
39+
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
40+
* You may choose a different type if you instantiate objects from sub sections of a specification.
41+
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
42+
* @throws TypeErrorException in case invalid spec data is supplied.
43+
*/
2444
public static function readFromYaml(string $yaml, string $baseType = OpenApi::class): SpecObjectInterface
2545
{
2646
return new $baseType(Yaml::parse($yaml));
2747
}
48+
49+
/**
50+
* Populate OpenAPI spec object from a JSON file.
51+
* @param string $fileName the file name of the file to be read.
52+
* If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or
53+
* an absolute path to allow resolving relative path references.
54+
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
55+
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
56+
* You may choose a different type if you instantiate objects from sub sections of a specification.
57+
* @param bool $resolveReferences whether to automatically resolve references in the specification.
58+
* If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling
59+
* [[SpecObjectInterface::resolveReferences()]].
60+
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
61+
* @throws TypeErrorException in case invalid spec data is supplied.
62+
* @throws UnresolvableReferenceException in case references could not be resolved.
63+
*/
64+
public static function readFromJsonFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface
65+
{
66+
$spec = static::readFromJson(file_get_contents($fileName), $baseType);
67+
if ($resolveReferences) {
68+
$spec->resolveReferences(new ReferenceContext($spec, $fileName));
69+
}
70+
return $spec;
71+
}
72+
73+
/**
74+
* Populate OpenAPI spec object from YAML file.
75+
* @param string $fileName the file name of the file to be read.
76+
* If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or
77+
* an absolute path to allow resolving relative path references.
78+
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
79+
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
80+
* You may choose a different type if you instantiate objects from sub sections of a specification.
81+
* @param bool $resolveReferences whether to automatically resolve references in the specification.
82+
* If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling
83+
* [[SpecObjectInterface::resolveReferences()]].
84+
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
85+
* @throws TypeErrorException in case invalid spec data is supplied.
86+
* @throws UnresolvableReferenceException in case references could not be resolved.
87+
*/
88+
public static function readFromYamlFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface
89+
{
90+
$spec = static::readFromYaml(file_get_contents($fileName), $baseType);
91+
if ($resolveReferences) {
92+
$spec->resolveReferences(new ReferenceContext($spec, $fileName));
93+
}
94+
return $spec;
95+
}
2896
}

src/ReferenceContext.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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\exceptions\UnresolvableReferenceException;
11+
12+
/**
13+
* ReferenceContext represents a context in which references are resolved.
14+
*/
15+
class ReferenceContext
16+
{
17+
/**
18+
* @var SpecObjectInterface
19+
*/
20+
private $_baseSpec;
21+
/**
22+
* @var string
23+
*/
24+
private $_uri;
25+
26+
/**
27+
* ReferenceContext constructor.
28+
* @param SpecObjectInterface $base the base object of the spec.
29+
* @param string $uri the URI to the base object.
30+
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
31+
*/
32+
public function __construct(SpecObjectInterface $base, string $uri)
33+
{
34+
$this->_baseSpec = $base;
35+
$this->_uri = $this->normalizeUri($uri);
36+
}
37+
38+
/**
39+
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
40+
*/
41+
private function normalizeUri($uri)
42+
{
43+
if (strpos($uri, '://') !== false) {
44+
return $uri;
45+
}
46+
if (strncmp($uri, '/', 1) === 0) {
47+
return "file://$uri";
48+
}
49+
throw new UnresolvableReferenceException('Can not resolve references for a specification given as a relative path.');
50+
}
51+
52+
/**
53+
* @return mixed
54+
*/
55+
public function getBaseSpec(): SpecObjectInterface
56+
{
57+
return $this->_baseSpec;
58+
}
59+
60+
/**
61+
* @return mixed
62+
*/
63+
public function getUri(): string
64+
{
65+
return $this->_uri;
66+
}
67+
68+
/**
69+
* Resolve a relative URI to an absolute URI in the current context.
70+
* @param string $uri
71+
* @throws UnresolvableReferenceException
72+
* @return string
73+
*/
74+
public function resolveRelativeUri(string $uri): string
75+
{
76+
$parts = parse_url($uri);
77+
if (isset($parts['scheme'])) {
78+
// absolute URL
79+
return $uri;
80+
}
81+
82+
$baseUri = $this->getUri();
83+
if (strncmp($baseUri, 'file://', 7) === 0) {
84+
if (isset($parts['path'][0]) && $parts['path'][0] === '/') {
85+
// absolute path
86+
return 'file://' . $parts['path'];
87+
}
88+
if (isset($parts['path'])) {
89+
// relative path
90+
return dirname($baseUri) . '/' . $parts['path'];
91+
}
92+
93+
throw new UnresolvableReferenceException("Invalid URI: '$uri'");
94+
}
95+
96+
$baseParts = parse_url($baseUri);
97+
$absoluteUri = implode('', [
98+
$baseParts['scheme'],
99+
'://',
100+
isset($baseParts['username']) ? $baseParts['username'] . (
101+
isset($baseParts['password']) ? ':' . $baseParts['password'] : ''
102+
) . '@' : '',
103+
$baseParts['host'] ?? '',
104+
isset($baseParts['port']) ? ':' . $baseParts['port'] : '',
105+
]);
106+
if (isset($parts['path'][0]) && $parts['path'][0] === '/') {
107+
$absoluteUri .= $parts['path'];
108+
} elseif (isset($parts['path'])) {
109+
$absoluteUri .= rtrim(dirname($baseParts['path'] ?? ''), '/') . '/' . $parts['path'];
110+
}
111+
return $absoluteUri
112+
. (isset($parts['query']) ? '?' . $parts['query'] : '')
113+
. (isset($parts['fragment']) ? '#' . $parts['fragment'] : '');
114+
}
115+
}

src/SpecBaseObject.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use cebe\openapi\exceptions\ReadonlyPropertyException;
1111
use cebe\openapi\exceptions\TypeErrorException;
1212
use cebe\openapi\exceptions\UnknownPropertyException;
13+
use cebe\openapi\spec\Reference;
1314
use cebe\openapi\spec\Type;
1415

1516
/**
@@ -73,7 +74,6 @@ public function __construct(array $data)
7374
} elseif ($type[0] === Type::ANY || $type[0] === Type::BOOLEAN || $type[0] === Type::INTEGER) { // TODO simplify handling of scalar types
7475
$this->_properties[$property][] = $item;
7576
} else {
76-
// TODO implement reference objects
7777
$this->_properties[$property][] = $this->instantiate($type[0], $item);
7878
}
7979
}
@@ -93,7 +93,6 @@ public function __construct(array $data)
9393
} elseif ($type[1] === Type::ANY || $type[1] === Type::BOOLEAN || $type[1] === Type::INTEGER) { // TODO simplify handling of scalar types
9494
$this->_properties[$property][$key] = $item;
9595
} else {
96-
// TODO implement reference objects
9796
$this->_properties[$property][$key] = $this->instantiate($type[1], $item);
9897
}
9998
}
@@ -114,6 +113,9 @@ public function __construct(array $data)
114113
*/
115114
private function instantiate($type, $data)
116115
{
116+
if (isset($data['$ref'])) {
117+
return new Reference($data, $type);
118+
}
117119
try {
118120
return new $type($data);
119121
} catch (\TypeError $e) {
@@ -239,4 +241,27 @@ public function __unset($name)
239241
{
240242
throw new ReadonlyPropertyException('Unsetting read-only property: ' . \get_class($this) . '::' . $name);
241243
}
244+
245+
/**
246+
* Resolves all Reference Objects in this object and replaces them with their resolution.
247+
* @throws exceptions\UnresolvableReferenceException in case resolving a reference fails.
248+
*/
249+
public function resolveReferences(ReferenceContext $context)
250+
{
251+
foreach ($this->_properties as $property => $value) {
252+
if ($value instanceof Reference) {
253+
$this->_properties[$property] = $value->resolve($context);
254+
} elseif ($value instanceof SpecObjectInterface) {
255+
$value->resolveReferences($context);
256+
} elseif (is_array($value)) {
257+
foreach ($value as $k => $item) {
258+
if ($item instanceof Reference) {
259+
$this->_properties[$property][$k] = $item->resolve($context);
260+
} elseif ($item instanceof SpecObjectInterface) {
261+
$item->resolveReferences($context);
262+
}
263+
}
264+
}
265+
}
266+
}
242267
}

src/SpecObjectInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ public function validate(): bool;
3030
* @see validate()
3131
*/
3232
public function getErrors(): array;
33+
34+
/**
35+
* Resolves all Reference Objects in this object and replaces them with their resolution.
36+
*/
37+
public function resolveReferences(ReferenceContext $context);
3338
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\exceptions;
9+
10+
/**
11+
*
12+
*
13+
*/
14+
class UnresolvableReferenceException extends \Exception
15+
{
16+
}

src/spec/Callback.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
namespace cebe\openapi\spec;
99

1010
use cebe\openapi\exceptions\TypeErrorException;
11+
use cebe\openapi\exceptions\UnresolvableReferenceException;
12+
use cebe\openapi\ReferenceContext;
1113
use cebe\openapi\SpecObjectInterface;
1214

1315
/**
@@ -75,4 +77,15 @@ public function getErrors(): array
7577
$pathItemErrors = $this->_pathItem === null ? [] : $this->_pathItem->getErrors();
7678
return array_merge($this->_errors, $pathItemErrors);
7779
}
80+
81+
/**
82+
* Resolves all Reference Objects in this object and replaces them with their resolution.
83+
* @throws UnresolvableReferenceException
84+
*/
85+
public function resolveReferences(ReferenceContext $context)
86+
{
87+
if ($this->_pathItem !== null) {
88+
$this->_pathItem->resolveReferences($context);
89+
}
90+
}
7891
}

0 commit comments

Comments
 (0)