OpenApi version override (#1179)

Makes the version set in the `Generator` optional so if it is not set the version from the `OpenApi` is used.

Since the `OpenApi` instance is not available until after the analyser is finished, there is no reliable way to have conditional code before that and `Context::$version` is only guaranteed for validation and serialization.
This commit is contained in:
Martin Rademacher 2022-04-01 08:32:16 +13:00 committed by GitHub
parent 3f3b551db6
commit 03030ead98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 87 additions and 67 deletions

View File

@ -31,7 +31,7 @@ $options = [
'help' => false,
'debug' => false,
'processor' => [],
'version' => OpenApi::DEFAULT_VERSION,
'version' => null,
];
$aliases = [
'l' => 'legacy',

View File

@ -105,9 +105,11 @@ abstract class AbstractAnnotation implements \JsonSerializable
} else {
$this->_context = Context::detect(1);
}
if ($this->_context->is('annotations') === false) {
$this->_context->annotations = [];
}
$this->_context->annotations[] = $this;
$nestedContext = new Context(['nested' => $this], $this->_context);
foreach ($properties as $property => $value) {
@ -140,6 +142,15 @@ abstract class AbstractAnnotation implements \JsonSerializable
}
}
}
if ($this instanceof OpenApi) {
if ($this->_context->root()->version) {
// override via `Generator::setVersion()`
$this->openapi = $this->_context->root()->version;
} else {
$this->_context->root()->version = $this->openapi;
}
}
}
public function __get($property)
@ -386,6 +397,7 @@ abstract class AbstractAnnotation implements \JsonSerializable
if (in_array($this, $skip, true)) {
return true;
}
$valid = true;
// Report orphaned annotations
@ -437,6 +449,7 @@ abstract class AbstractAnnotation implements \JsonSerializable
}
}
}
if (property_exists($this, 'ref') && !Generator::isDefault($this->ref) && $this->ref !== null) {
if (substr($this->ref, 0, 2) === '#/' && count($stack) > 0 && $stack[0] instanceof OpenApi) {
// Internal reference

View File

@ -28,9 +28,12 @@ class OpenApi extends AbstractAnnotation
* The semantic version number of the OpenAPI Specification version that the OpenAPI document uses.
*
* The openapi field should be used by tooling specifications and clients to interpret the OpenAPI document.
*
* A version specified via `Generator::setVersion()` will overwrite this value.
*
* This is not related to the API info::version string.
*
* @var string
* @var string|null
*/
public $openapi = self::DEFAULT_VERSION;

View File

@ -40,7 +40,8 @@ use OpenApi\Loggers\DefaultLogger;
* @property string|null $type
* @property bool|null $static Indicate a static method
* @property bool|null $nullable Indicate a nullable value
* @property bool|null $generated Indicate the context was generated by a processor or the serializer
* @property bool|null $generated Indicate the context was generated by a processor or
* the serializer
* @property Annotations\AbstractAnnotation|null $nested
* @property Annotations\AbstractAnnotation[]|null $annotations
* @property \Psr\Log\LoggerInterface|null $logger Guaranteed to be set when using the `Generator`
@ -56,10 +57,6 @@ class Context
*/
private $_parent;
/**
* @param array $properties new properties for this context
* @param Context $parent The parent context
*/
public function __construct(array $properties = [], ?Context $parent = null)
{
foreach ($properties as $property => $value) {
@ -73,7 +70,7 @@ class Context
/**
* Check if a property is set directly on this context and not its parent context.
*
* @param string $type Example: $c->is('method') or $c->is('class')
* Example: $c->is('method') or $c->is('class')
*/
public function is(string $type): bool
{
@ -83,7 +80,7 @@ class Context
/**
* Check if a property is NOT set directly on this context and but its parent context.
*
* @param string $type Example: $c->not('method') or $c->not('class')
* Example: $c->not('method') or $c->not('class')
*/
public function not(string $type): bool
{
@ -105,6 +102,18 @@ class Context
return null;
}
/**
* Get the root context.
*/
public function root(): Context
{
if ($this->_parent !== null) {
return $this->_parent->root();
}
return $this;
}
/**
* Check if one of the given version numbers matches the current OpenAPI version.
*
@ -112,6 +121,10 @@ class Context
*/
public function isVersion($versions): bool
{
if (!$this->version) {
throw new \RuntimeException('Version is only available reliably for validation and serialization');
}
$versions = (array) $versions;
$currentVersion = $this->version ?: OpenApi::DEFAULT_VERSION;
@ -155,10 +168,8 @@ class Context
/**
* Traverse the context tree to get the property value.
*
* @param string $property
*/
public function __get($property)
public function __get(string $property)
{
if ($this->_parent !== null) {
return $this->_parent->$property;
@ -179,10 +190,8 @@ class Context
/**
* A short piece of text, usually one line, providing the basic function of the associated element.
*
* @return string
*/
public function phpdocSummary()
public function phpdocSummary(): string
{
$content = $this->phpdocContent();
if (!$content) {
@ -205,11 +214,10 @@ class Context
}
/**
* An optional longer piece of text providing more details on the associated elements function. This is very useful when working with a complex element.
*
* @return string
* An optional longer piece of text providing more details on the associated elements function. This is very
* useful when working with a complex element.
*/
public function phpdocDescription()
public function phpdocDescription(): string
{
$summary = $this->phpdocSummary();
if (!$summary) {
@ -230,10 +238,8 @@ class Context
/**
* The text contents of the phpdoc comment (excl. tags).
*
* @return string
*/
public function phpdocContent()
public function phpdocContent(): string
{
if (Generator::isDefault($this->comment)) {
return Generator::UNDEFINED;
@ -303,8 +309,6 @@ class Context
/**
* Resolve the fully qualified name.
*
* @param string $source The source name (class/interface/trait)
*/
public function fullyQualifiedName(?string $source): string
{

View File

@ -55,8 +55,17 @@ class Generator
/** @var LoggerInterface|null PSR logger. */
protected $logger = null;
/** @var string */
protected $version = OpenApi::DEFAULT_VERSION;
/**
* OpenApi version override.
*
* If set, it will override the version set in the `OpenApi` annotation.
*
* Due to the order of processing any conditional code using this (via `Context::$version`)
* must come only after the analysis is finished.
*
* @var string|null
*/
protected $version = null;
private $configStack;
@ -259,12 +268,12 @@ class Generator
return $this->logger ?: new DefaultLogger();
}
public function getVersion(): string
public function getVersion(): ?string
{
return $this->version;
}
public function setVersion(string $version): Generator
public function setVersion(?string $version): Generator
{
$this->version = $version;
@ -282,7 +291,7 @@ class Generator
'processors' => null,
'logger' => null,
'validate' => true,
'version' => OpenApi::DEFAULT_VERSION,
'version' => null,
];
return (new Generator($config['logger']))
@ -345,7 +354,8 @@ class Generator
$analysis->process($this->getProcessors());
if ($analysis->openapi) {
$analysis->openapi->openapi = $this->version;
$analysis->openapi->openapi = $this->version ?: $analysis->openapi->openapi;
$rootContext->version = $analysis->openapi->openapi;
}
// validation

View File

@ -37,7 +37,7 @@ class ExpandEnums
return $case->name;
}, $re->getCases());
$type = 'string';
if ($re->isBacked() && ($backingType = $re->getBackingType())) {
if ($re->isBacked() && ($backingType = $re->getBackingType()) && method_exists($backingType, 'getName')) {
$type = !Generator::isDefault($schema->type) ? $schema->type : $backingType->getName();
}
Util::mapNativeType($schema, $type);

View File

@ -56,58 +56,49 @@ class Serializer
OA\XmlContent::class,
];
public static function isValidAnnotationClass($className)
public static function isValidAnnotationClass($className): bool
{
return in_array($className, self::$VALID_ANNOTATIONS);
}
/**
* Serialize.
*
*
* @return string
*/
public function serialize(OA\AbstractAnnotation $annotation)
public function serialize(OA\AbstractAnnotation $annotation): string
{
return json_encode($annotation);
}
/**
* Deserialize a string.
*
* @return OA\AbstractAnnotation
*/
public function deserialize(string $jsonString, string $className)
public function deserialize(string $jsonString, string $className): OA\AbstractAnnotation
{
if (!$this->isValidAnnotationClass($className)) {
throw new \Exception($className . ' is not defined in OpenApi PHP Annotations');
}
return $this->doDeserialize(json_decode($jsonString), $className);
return $this->doDeserialize(json_decode($jsonString), $className, new Context(['generated' => true]));
}
/**
* Deserialize a file.
*
* @return OA\AbstractAnnotation
*/
public function deserializeFile(string $filename, string $className = OA\OpenApi::class)
public function deserializeFile(string $filename, string $className = OA\OpenApi::class): OA\AbstractAnnotation
{
if (!$this->isValidAnnotationClass($className)) {
throw new \Exception($className . ' is not defined in OpenApi PHP Annotations');
}
return $this->doDeserialize(json_decode(file_get_contents($filename)), $className);
return $this->doDeserialize(json_decode(file_get_contents($filename)), $className, new Context(['generated' => true]));
}
/**
* Do deserialization.
*
* @return OA\AbstractAnnotation
*/
protected function doDeserialize(\stdClass $c, string $class)
protected function doDeserialize(\stdClass $c, string $class, Context $context): OA\AbstractAnnotation
{
$annotation = new $class(['_context' => new Context(['generated' => true])]);
$annotation = new $class(['_context' => $context]);
foreach ((array) $c as $property => $value) {
if ($property === '$ref') {
$property = 'ref';
@ -120,7 +111,7 @@ class Serializer
$custom = substr($property, 2);
$annotation->x[$custom] = $value;
} else {
$annotation->$property = $this->doDeserializeProperty($annotation, $property, $value);
$annotation->$property = $this->doDeserializeProperty($annotation, $property, $value, $context);
}
}
@ -130,11 +121,11 @@ class Serializer
/**
* Deserialize the annotation's property.
*/
protected function doDeserializeProperty(OA\AbstractAnnotation $annotation, string $property, $value)
protected function doDeserializeProperty(OA\AbstractAnnotation $annotation, string $property, $value, Context $context)
{
// property is primitive type
if (array_key_exists($property, $annotation::$_types)) {
return $this->doDeserializeBaseProperty($annotation::$_types[$property], $value);
return $this->doDeserializeBaseProperty($annotation::$_types[$property], $value, $context);
}
// property is embedded annotation
@ -143,7 +134,7 @@ class Serializer
// property is an annotation
if (is_string($declaration) && $declaration === $property) {
if (is_object($value)) {
return $this->doDeserialize($value, $nestedClass);
return $this->doDeserialize($value, $nestedClass, $context);
} else {
return $value;
}
@ -153,7 +144,7 @@ class Serializer
if (is_array($declaration) && count($declaration) === 1 && $declaration[0] === $property) {
$annotationArr = [];
foreach ($value as $v) {
$annotationArr[] = $this->doDeserialize($v, $nestedClass);
$annotationArr[] = $this->doDeserialize($v, $nestedClass, $context);
}
return $annotationArr;
@ -164,7 +155,7 @@ class Serializer
$key = $declaration[1];
$annotationHash = [];
foreach ($value as $k => $v) {
$annotation = $this->doDeserialize($v, $nestedClass);
$annotation = $this->doDeserialize($v, $nestedClass, $context);
$annotation->$key = $k;
$annotationHash[$k] = $annotation;
}
@ -184,7 +175,7 @@ class Serializer
*
* @return array|OA\AbstractAnnotation
*/
protected function doDeserializeBaseProperty($type, $value)
protected function doDeserializeBaseProperty($type, $value, Context $context)
{
$isAnnotationClass = is_string($type) && is_subclass_of(trim($type, '[]'), OA\AbstractAnnotation::class);
@ -196,13 +187,13 @@ class Serializer
$class = trim($type, '[]');
foreach ($value as $v) {
$annotationArr[] = $this->doDeserialize($v, $class);
$annotationArr[] = $this->doDeserialize($v, $class, $context);
}
return $annotationArr;
}
return $this->doDeserialize($value, $type);
return $this->doDeserialize($value, $type, $context);
}
return $value;

View File

@ -94,7 +94,6 @@ class ReflectionAnalyserTest extends OpenApiTestCase
$analyser->setGenerator($generator);
$analysis = $analyser->fromFile($this->fixture('Apis/DocBlocks/basic.php'), $this->getContext([], $generator->getVersion()));
$analysis->process($generator->getProcessors());
$analysis->openapi->openapi = $generator->getVersion();
return $analysis;
});
@ -118,14 +117,12 @@ class ReflectionAnalyserTest extends OpenApiTestCase
/** @var Analysis $analysis */
$analysis = (new Generator())
->setVersion(OpenApi::VERSION_3_1_0)
->addAlias('oaf', 'OpenApi\\Tests\\Annotations')
->addNamespace('OpenApi\\Tests\\Annotations\\')
->withContext(function (Generator $generator) use ($analyser) {
$analyser->setGenerator($generator);
$analysis = $analyser->fromFile($this->fixture('Apis/Attributes/basic.php'), $this->getContext([], $generator->getVersion()));
$analysis->process((new Generator())->getProcessors());
$analysis->openapi->openapi = $generator->getVersion();
return $analysis;
});
@ -161,12 +158,10 @@ class ReflectionAnalyserTest extends OpenApiTestCase
require_once $this->fixture('Apis/Mixed/basic.php');
$analysis = (new Generator())
->setVersion(OpenApi::VERSION_3_1_0)
->withContext(function (Generator $generator) use ($analyser) {
$analyser->setGenerator($generator);
$analysis = $analyser->fromFile($this->fixture('Apis/Mixed/basic.php'), $this->getContext([], $generator->getVersion()));
$analysis->process((new Generator())->getProcessors());
$analysis->openapi->openapi = $generator->getVersion();
return $analysis;
});

View File

@ -9,6 +9,7 @@ namespace OpenApi\Tests\Fixtures\Apis\Attributes;
use OpenApi\Attributes as OAT;
use OpenApi\Tests\Fixtures\Attributes as OAF;
#[OAT\OpenApi(openapi: '3.1.0')]
#[OAT\Info(
version: '1.0.0',
title: 'Basic single file API',

View File

@ -10,10 +10,13 @@ use OpenApi\Annotations as OA;
use OpenApi\Attributes as OAT;
/**
* @OA\Info(
* version="1.0.0",
* title="Basic single file API",
* @OA\License(name="MIT", identifier="MIT")
* @OA\OpenApi(
* openapi="3.1.0",
* @OA\Info(
* version="1.0.0",
* title="Basic single file API",
* @OA\License(name="MIT", identifier="MIT")
* )
* )
*/
#[OAT\Tag(name: 'products', description: 'All about products')]

View File

@ -77,7 +77,7 @@ class OpenApiTestCase extends TestCase
};
}
public function getContext(array $properties = [], string $version = OpenApi::DEFAULT_VERSION): Context
public function getContext(array $properties = [], ?string $version = OpenApi::DEFAULT_VERSION): Context
{
return new Context(
[