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:
parent
3f3b551db6
commit
03030ead98
|
@ -31,7 +31,7 @@ $options = [
|
|||
'help' => false,
|
||||
'debug' => false,
|
||||
'processor' => [],
|
||||
'version' => OpenApi::DEFAULT_VERSION,
|
||||
'version' => null,
|
||||
];
|
||||
$aliases = [
|
||||
'l' => 'legacy',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 element’s 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 element’s 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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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')]
|
||||
|
|
|
@ -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(
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue