Skip to content

Commit

Permalink
Merge pull request #12 from cebe/references
Browse files Browse the repository at this point in the history
WIP references
  • Loading branch information
cebe authored Nov 22, 2018
2 parents 161464f + 271bb4a commit 4a4892d
Show file tree
Hide file tree
Showing 22 changed files with 792 additions and 16 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ coverage: .php-openapi-covA .php-openapi-covB
.php-openapi-covA:
grep -rhPo '@covers .+' tests |cut -c 28- |sort > $@
.php-openapi-covB:
grep -rhPo 'class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@
grep -rhPo '^class \w+' src/spec/ | awk '{print $$2}' |grep -v '^Type$$' | sort > $@

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

41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ READ [OpenAPI](https://www.openapis.org/) 3.0.x YAML and JSON files and make the

## Usage

### Reading Specification information

Read OpenAPI spec from JSON:

```php
Expand Down Expand Up @@ -46,6 +48,43 @@ foreach($openapi->paths as $path => $definition) {
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).
You may also access additional properties added by specification extensions.

### Reading Specification Files and Resolving References

In the above we have passed the raw JSON or YAML data to the Reader. In order to be able to resolve
references to external files that may exist in the specification files, we must provide the full context.

```php
use cebe\openapi\Reader;
// an absolute URL or file path is needed to allow resolving internal references
$openapi = Reader::readFromJsonFile('https://www.example.com/api/openapi.json');
$openapi = Reader::readFromYamlFile('https://www.example.com/api/openapi.yaml');
```

If data has been loaded in a different way you can manually resolve references like this by giving a context:

```php
$openapi->resolveReferences(
new \cebe\openapi\ReferenceContext($openapi, 'https://www.example.com/api/openapi.yaml')
);
```

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

The library provides simple validation operations, that check basic OpenAPI spec requirements.

```
// return `true` in case no errors have been found, `false` in case of errors.
$specValid = $openapi->validate();
// after validation getErrors() can be used to retrieve the list of errors found.
$errors = $openapi->getErrors();
```

> **Note:** Validation is done on a very basic level and is not complete. So a failing validation will show some errors,
> but the list of errors given may not be complete. Also a passing validation does not necessarily indicate a completely
> valid spec.

## Completeness

Expand Down Expand Up @@ -77,7 +116,7 @@ This library is currently work in progress, the following list tracks completene
- [ ] [Runtime Expressions](https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#runtime-expressions)
- [x] Header Object
- [x] Tag Object
- [ ] Reference Object
- [x] Reference Object
- [x] Schema Object
- [x] load/read
- [ ] validation
Expand Down
70 changes: 69 additions & 1 deletion src/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,90 @@

namespace cebe\openapi;

use cebe\openapi\exceptions\TypeErrorException;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\spec\OpenApi;
use Symfony\Component\Yaml\Yaml;

/**
*
* Utility class to simplify reading JSON or YAML OpenAPI specs.
*
*/
class Reader
{
/**
* Populate OpenAPI spec object from JSON data.
* @param string $json the JSON string to decode.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
public static function readFromJson(string $json, string $baseType = OpenApi::class): SpecObjectInterface
{
return new $baseType(json_decode($json, true));
}

/**
* Populate OpenAPI spec object from YAML data.
* @param string $yaml the YAML string to decode.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* @throws TypeErrorException in case invalid spec data is supplied.
*/
public static function readFromYaml(string $yaml, string $baseType = OpenApi::class): SpecObjectInterface
{
return new $baseType(Yaml::parse($yaml));
}

/**
* Populate OpenAPI spec object from a JSON file.
* @param string $fileName the file name of the file to be read.
* If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or
* an absolute path to allow resolving relative path references.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @param bool $resolveReferences whether to automatically resolve references in the specification.
* If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling
* [[SpecObjectInterface::resolveReferences()]].
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* @throws TypeErrorException in case invalid spec data is supplied.
* @throws UnresolvableReferenceException in case references could not be resolved.
*/
public static function readFromJsonFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface
{
$spec = static::readFromJson(file_get_contents($fileName), $baseType);
if ($resolveReferences) {
$spec->resolveReferences(new ReferenceContext($spec, $fileName));
}
return $spec;
}

/**
* Populate OpenAPI spec object from YAML file.
* @param string $fileName the file name of the file to be read.
* If `$resolveReferences` is true (the default), this should be an absolute URL, a `file://` URI or
* an absolute path to allow resolving relative path references.
* @param string $baseType the base Type to instantiate. This must be an instance of [[SpecObjectInterface]].
* The default is [[OpenApi]] which is the base type of a OpenAPI specification file.
* You may choose a different type if you instantiate objects from sub sections of a specification.
* @param bool $resolveReferences whether to automatically resolve references in the specification.
* If `true`, all [[Reference]] objects will be replaced with their referenced spec objects by calling
* [[SpecObjectInterface::resolveReferences()]].
* @return SpecObjectInterface|OpenApi the OpenApi object instance.
* @throws TypeErrorException in case invalid spec data is supplied.
* @throws UnresolvableReferenceException in case references could not be resolved.
*/
public static function readFromYamlFile(string $fileName, string $baseType = OpenApi::class, $resolveReferences = true): SpecObjectInterface
{
$spec = static::readFromYaml(file_get_contents($fileName), $baseType);
if ($resolveReferences) {
$spec->resolveReferences(new ReferenceContext($spec, $fileName));
}
return $spec;
}
}
115 changes: 115 additions & 0 deletions src/ReferenceContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/**
* @copyright Copyright (c) 2018 Carsten Brandt <[email protected]> and contributors
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
*/

namespace cebe\openapi;

use cebe\openapi\exceptions\UnresolvableReferenceException;

/**
* ReferenceContext represents a context in which references are resolved.
*/
class ReferenceContext
{
/**
* @var SpecObjectInterface
*/
private $_baseSpec;
/**
* @var string
*/
private $_uri;

/**
* ReferenceContext constructor.
* @param SpecObjectInterface $base the base object of the spec.
* @param string $uri the URI to the base object.
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
*/
public function __construct(SpecObjectInterface $base, string $uri)
{
$this->_baseSpec = $base;
$this->_uri = $this->normalizeUri($uri);
}

/**
* @throws UnresolvableReferenceException in case an invalid or non-absolute URI is provided.
*/
private function normalizeUri($uri)
{
if (strpos($uri, '://') !== false) {
return $uri;
}
if (strncmp($uri, '/', 1) === 0) {
return "file://$uri";
}
throw new UnresolvableReferenceException('Can not resolve references for a specification given as a relative path.');
}

/**
* @return mixed
*/
public function getBaseSpec(): SpecObjectInterface
{
return $this->_baseSpec;
}

/**
* @return mixed
*/
public function getUri(): string
{
return $this->_uri;
}

/**
* Resolve a relative URI to an absolute URI in the current context.
* @param string $uri
* @throws UnresolvableReferenceException
* @return string
*/
public function resolveRelativeUri(string $uri): string
{
$parts = parse_url($uri);
if (isset($parts['scheme'])) {
// absolute URL
return $uri;
}

$baseUri = $this->getUri();
if (strncmp($baseUri, 'file://', 7) === 0) {
if (isset($parts['path'][0]) && $parts['path'][0] === '/') {
// absolute path
return 'file://' . $parts['path'];
}
if (isset($parts['path'])) {
// relative path
return dirname($baseUri) . '/' . $parts['path'];
}

throw new UnresolvableReferenceException("Invalid URI: '$uri'");
}

$baseParts = parse_url($baseUri);
$absoluteUri = implode('', [
$baseParts['scheme'],
'://',
isset($baseParts['username']) ? $baseParts['username'] . (
isset($baseParts['password']) ? ':' . $baseParts['password'] : ''
) . '@' : '',
$baseParts['host'] ?? '',
isset($baseParts['port']) ? ':' . $baseParts['port'] : '',
]);
if (isset($parts['path'][0]) && $parts['path'][0] === '/') {
$absoluteUri .= $parts['path'];
} elseif (isset($parts['path'])) {
$absoluteUri .= rtrim(dirname($baseParts['path'] ?? ''), '/') . '/' . $parts['path'];
}
return $absoluteUri
. (isset($parts['query']) ? '?' . $parts['query'] : '')
. (isset($parts['fragment']) ? '#' . $parts['fragment'] : '');
}
}
29 changes: 27 additions & 2 deletions src/SpecBaseObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use cebe\openapi\exceptions\ReadonlyPropertyException;
use cebe\openapi\exceptions\TypeErrorException;
use cebe\openapi\exceptions\UnknownPropertyException;
use cebe\openapi\spec\Reference;
use cebe\openapi\spec\Type;

/**
Expand Down Expand Up @@ -73,7 +74,6 @@ public function __construct(array $data)
} elseif ($type[0] === Type::ANY || $type[0] === Type::BOOLEAN || $type[0] === Type::INTEGER) { // TODO simplify handling of scalar types
$this->_properties[$property][] = $item;
} else {
// TODO implement reference objects
$this->_properties[$property][] = $this->instantiate($type[0], $item);
}
}
Expand All @@ -93,7 +93,6 @@ public function __construct(array $data)
} elseif ($type[1] === Type::ANY || $type[1] === Type::BOOLEAN || $type[1] === Type::INTEGER) { // TODO simplify handling of scalar types
$this->_properties[$property][$key] = $item;
} else {
// TODO implement reference objects
$this->_properties[$property][$key] = $this->instantiate($type[1], $item);
}
}
Expand All @@ -114,6 +113,9 @@ public function __construct(array $data)
*/
private function instantiate($type, $data)
{
if (isset($data['$ref'])) {
return new Reference($data, $type);
}
try {
return new $type($data);
} catch (\TypeError $e) {
Expand Down Expand Up @@ -239,4 +241,27 @@ public function __unset($name)
{
throw new ReadonlyPropertyException('Unsetting read-only property: ' . \get_class($this) . '::' . $name);
}

/**
* Resolves all Reference Objects in this object and replaces them with their resolution.
* @throws exceptions\UnresolvableReferenceException in case resolving a reference fails.
*/
public function resolveReferences(ReferenceContext $context)
{
foreach ($this->_properties as $property => $value) {
if ($value instanceof Reference) {
$this->_properties[$property] = $value->resolve($context);
} elseif ($value instanceof SpecObjectInterface) {
$value->resolveReferences($context);
} elseif (is_array($value)) {
foreach ($value as $k => $item) {
if ($item instanceof Reference) {
$this->_properties[$property][$k] = $item->resolve($context);
} elseif ($item instanceof SpecObjectInterface) {
$item->resolveReferences($context);
}
}
}
}
}
}
5 changes: 5 additions & 0 deletions src/SpecObjectInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ public function validate(): bool;
* @see validate()
*/
public function getErrors(): array;

/**
* Resolves all Reference Objects in this object and replaces them with their resolution.
*/
public function resolveReferences(ReferenceContext $context);
}
16 changes: 16 additions & 0 deletions src/exceptions/UnresolvableReferenceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/**
* @copyright Copyright (c) 2018 Carsten Brandt <[email protected]> and contributors
* @license https://github.com/cebe/php-openapi/blob/master/LICENSE
*/

namespace cebe\openapi\exceptions;

/**
*
*
*/
class UnresolvableReferenceException extends \Exception
{
}
13 changes: 13 additions & 0 deletions src/spec/Callback.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
namespace cebe\openapi\spec;

use cebe\openapi\exceptions\TypeErrorException;
use cebe\openapi\exceptions\UnresolvableReferenceException;
use cebe\openapi\ReferenceContext;
use cebe\openapi\SpecObjectInterface;

/**
Expand Down Expand Up @@ -75,4 +77,15 @@ public function getErrors(): array
$pathItemErrors = $this->_pathItem === null ? [] : $this->_pathItem->getErrors();
return array_merge($this->_errors, $pathItemErrors);
}

/**
* Resolves all Reference Objects in this object and replaces them with their resolution.
* @throws UnresolvableReferenceException
*/
public function resolveReferences(ReferenceContext $context)
{
if ($this->_pathItem !== null) {
$this->_pathItem->resolveReferences($context);
}
}
}
Loading

0 comments on commit 4a4892d

Please sign in to comment.