Components cleanup (#1199)
* Make `Components::ref()` handle all types of components * Refactor how annotations are added programmatically so they are also added to `Analysis::annotations` * Handle more cases of type = 'object' (`allOf`, `oneOf`, `anyOf`) * Add `CleanUnusedComponents` processor * Update fixtures and examples to use all components so running the `CleanUnusedComponents` processor does not make a difference
This commit is contained in:
parent
bda506f64f
commit
a36ba9ac4b
|
@ -3,6 +3,7 @@
|
|||
bin/
|
||||
!bin/openapi
|
||||
composer.lock
|
||||
coverage/
|
||||
docs/.vitepress/dist/
|
||||
docs/node_modules/
|
||||
docs/package-lock.json
|
||||
|
|
|
@ -52,6 +52,7 @@ paths:
|
|||
name:
|
||||
type: string
|
||||
phone:
|
||||
type: object
|
||||
oneOf: [{ type: string }, { type: integer }]
|
||||
type: object
|
||||
example:
|
||||
|
@ -64,6 +65,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
oneOf:
|
||||
- { $ref: '#/components/schemas/Result' }
|
||||
- { type: boolean }
|
||||
|
|
|
@ -27,7 +27,7 @@ class Pet
|
|||
* security={
|
||||
* {"petstore_auth": {"write:pets", "read:pets"}}
|
||||
* },
|
||||
* requestBody={"$ref": "#/components/requestBodies/Pet"}
|
||||
* @OA\RequestBody(ref="#/components/requestBodies/Pet")
|
||||
* )
|
||||
*/
|
||||
public function addPet()
|
||||
|
@ -56,7 +56,7 @@ class Pet
|
|||
* security={
|
||||
* {"petstore_auth": {"write:pets", "read:pets"}}
|
||||
* },
|
||||
* requestBody={"$ref": "#/components/requestBodies/Pet"}
|
||||
* @OA\RequestBody(ref="#/components/requestBodies/Pet")
|
||||
* )
|
||||
*/
|
||||
public function updatePet()
|
||||
|
|
|
@ -45,6 +45,7 @@ components:
|
|||
-
|
||||
$ref: '#/components/schemas/EmployeeResponsible'
|
||||
EmployeeResponsible:
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Responsible'
|
||||
|
@ -56,6 +57,7 @@ components:
|
|||
type: string
|
||||
const: Virtual
|
||||
FlResponsible:
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Responsible'
|
||||
|
|
|
@ -46,6 +46,7 @@ components:
|
|||
-
|
||||
$ref: '#/components/schemas/EmployeeResponsible'
|
||||
EmployeeResponsible:
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Responsible'
|
||||
|
@ -59,6 +60,7 @@ components:
|
|||
enum:
|
||||
- Virtual
|
||||
FlResponsible:
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Responsible'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace OpenApi\Examples\UsingInterfaces;
|
||||
|
||||
/**
|
||||
* @OA\Schema(title="Pet")
|
||||
* @OA\Schema(title="GreenProduct")
|
||||
*/
|
||||
class GreenProduct extends Product implements ColorInterface
|
||||
{
|
||||
|
|
|
@ -55,7 +55,7 @@ paths:
|
|||
components:
|
||||
schemas:
|
||||
GreenProduct:
|
||||
title: Pet
|
||||
title: GreenProduct
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
|
@ -67,6 +67,7 @@ components:
|
|||
example: blue
|
||||
Product:
|
||||
title: 'Product model'
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/ProductInterface'
|
||||
|
|
|
@ -28,4 +28,9 @@ class Product extends Model
|
|||
* @OA\Property(ref="#/components/schemas/product_status")
|
||||
*/
|
||||
public $status;
|
||||
|
||||
/**
|
||||
* @OA\Property
|
||||
*/
|
||||
public StockLevel $stockLevel;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class PropertyRefController
|
|||
* @OA\Parameter(ref="#/components/schemas/Product/properties/id"),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* ref="#/components/responses/product"
|
||||
* ref="#/components/responses/todo"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -33,7 +33,7 @@ paths:
|
|||
$ref: '#/components/schemas/Product/allOf/1/properties/id'
|
||||
responses:
|
||||
default:
|
||||
$ref: '#/components/responses/product'
|
||||
$ref: '#/components/responses/todo'
|
||||
components:
|
||||
schemas:
|
||||
Model:
|
||||
|
@ -61,6 +61,8 @@ components:
|
|||
example: 1
|
||||
status:
|
||||
$ref: '#/components/schemas/product_status'
|
||||
stockLevel:
|
||||
$ref: '#/components/schemas/StockLevel'
|
||||
StockLevel:
|
||||
type: integer
|
||||
enum:
|
||||
|
|
|
@ -2,17 +2,13 @@
|
|||
|
||||
namespace OpenApi\Examples\UsingTraits;
|
||||
|
||||
/**
|
||||
* @OA\Schema(title="Delete entity trait")
|
||||
*
|
||||
* @todo Not sure if this is correct or wanted behaviour...
|
||||
*/
|
||||
trait DeleteEntity
|
||||
{
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* tags={"Entities"},
|
||||
* path="/entities/{id}",
|
||||
* operationId="deleteEntity",
|
||||
* @OA\Parameter(
|
||||
* description="ID of entity to delete",
|
||||
* in="path",
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace OpenApi\Examples\UsingTraits;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* A controller.
|
||||
*/
|
||||
|
@ -24,7 +26,14 @@ class ProductController
|
|||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="successful operation"
|
||||
* description="successful operation",
|
||||
* @OA\JsonContent(
|
||||
* oneOf={
|
||||
* @OA\Schema(ref="#/components/schemas/SimpleProduct"),
|
||||
* @OA\Schema(ref="#/components/schemas/Product"),
|
||||
* @OA\Schema(ref="#/components/schemas/TrickyProduct")
|
||||
* }
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -7,7 +7,7 @@ paths:
|
|||
delete:
|
||||
tags:
|
||||
- Entities
|
||||
operationId: 341f63c1bb8a9c0c0392b112a967a05f
|
||||
operationId: deleteEntity
|
||||
parameters:
|
||||
-
|
||||
name: id
|
||||
|
@ -35,10 +35,19 @@ paths:
|
|||
responses:
|
||||
default:
|
||||
description: 'successful operation'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
oneOf:
|
||||
- { $ref: '#/components/schemas/SimpleProduct' }
|
||||
- { $ref: '#/components/schemas/Product' }
|
||||
- { $ref: '#/components/schemas/TrickyProduct' }
|
||||
components:
|
||||
schemas:
|
||||
BellsAndWhistles:
|
||||
title: 'Bells and Whistles trait'
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Bells'
|
||||
|
@ -77,10 +86,9 @@ components:
|
|||
description: 'The bell.'
|
||||
example: 'bone whistle'
|
||||
type: object
|
||||
DeleteEntity:
|
||||
title: 'Delete entity trait'
|
||||
Product:
|
||||
title: 'Product model'
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Colour'
|
||||
|
@ -98,6 +106,7 @@ components:
|
|||
example: gong
|
||||
SimpleProduct:
|
||||
title: 'SimpleProduct model'
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/Bells'
|
||||
|
@ -110,6 +119,7 @@ components:
|
|||
example: 1
|
||||
TrickyProduct:
|
||||
title: 'TrickyProduct model'
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/SimpleProduct'
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Annotations\AbstractAnnotation;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
|
@ -13,5 +14,8 @@ interface AnnotationFactoryInterface
|
|||
{
|
||||
public function setGenerator(Generator $generator): void;
|
||||
|
||||
/**
|
||||
* @return array<AbstractAnnotation> top level annotations
|
||||
*/
|
||||
public function build(\Reflector $reflector, Context $context): array;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ use OpenApi\Util;
|
|||
*/
|
||||
class Components extends AbstractAnnotation
|
||||
{
|
||||
public const COMPONENTS_PREFIX = '#/components/';
|
||||
|
||||
/**
|
||||
* Schema reference.
|
||||
*
|
||||
|
@ -57,7 +59,7 @@ class Components extends AbstractAnnotation
|
|||
public $examples = Generator::UNDEFINED;
|
||||
|
||||
/**
|
||||
* Reusable Request Bodys.
|
||||
* Reusable Request Bodies.
|
||||
*
|
||||
* @var RequestBody[]
|
||||
*/
|
||||
|
@ -87,7 +89,7 @@ class Components extends AbstractAnnotation
|
|||
/**
|
||||
* Reusable Callbacks.
|
||||
*
|
||||
* @var callable[]
|
||||
* @var array
|
||||
*/
|
||||
public $callbacks = Generator::UNDEFINED;
|
||||
|
||||
|
@ -102,7 +104,6 @@ class Components extends AbstractAnnotation
|
|||
* @inheritdoc
|
||||
*/
|
||||
public static $_nested = [
|
||||
Schema::class => ['schemas', 'schema'],
|
||||
Response::class => ['responses', 'response'],
|
||||
Parameter::class => ['parameters', 'parameter'],
|
||||
PathParameter::class => ['parameters', 'parameter'],
|
||||
|
@ -111,13 +112,35 @@ class Components extends AbstractAnnotation
|
|||
Header::class => ['headers', 'header'],
|
||||
SecurityScheme::class => ['securitySchemes', 'securityScheme'],
|
||||
Link::class => ['links', 'link'],
|
||||
Schema::class => ['schemas', 'schema'],
|
||||
Attachable::class => ['attachables'],
|
||||
];
|
||||
|
||||
public static function ref($schema, bool $encode = true): string
|
||||
/**
|
||||
* Generate a `#/components/...` reference for the given annotation.
|
||||
*
|
||||
* A `string` component value always assumes type `Schema`.
|
||||
*
|
||||
* @param AbstractAnnotation|string $component
|
||||
*/
|
||||
public static function ref($component, bool $encode = true): string
|
||||
{
|
||||
$name = $schema instanceof Schema ? $schema->schema : $schema;
|
||||
if ($component instanceof AbstractAnnotation) {
|
||||
foreach (Components::$_nested as $type => $nested) {
|
||||
// exclude attachables
|
||||
if (2 == count($nested)) {
|
||||
if ($component instanceof $type) {
|
||||
$type = $nested[0];
|
||||
$name = $component->{$nested[1]};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$type = 'schemas';
|
||||
$name = $component;
|
||||
}
|
||||
|
||||
return Components::SCHEMA_REF . ($encode ? Util::refEncode($name) : $name);
|
||||
return self::COMPONENTS_PREFIX . $type . '/' . ($encode ? Util::refEncode((string) $name) : $name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,22 +101,24 @@ class AugmentProperties
|
|||
if (!Generator::isDefault($property->ref) && $typeMatches[2] === '' && $property->nullable) {
|
||||
$refKey = $this->toRefKey($context, $type);
|
||||
$property->oneOf = [
|
||||
new Schema([
|
||||
$schema = new Schema([
|
||||
'ref' => $refs[$refKey],
|
||||
'_context' => $property->_context,
|
||||
'_aux' => true,
|
||||
]),
|
||||
];
|
||||
$analysis->addAnnotation($schema, $schema->_context);
|
||||
$property->nullable = true;
|
||||
} elseif ($typeMatches[2] === '[]') {
|
||||
if (Generator::isDefault($property->items)) {
|
||||
$property->items = new Items(
|
||||
$property->items = $items = new Items(
|
||||
[
|
||||
'type' => $property->type,
|
||||
'_context' => new Context(['generated' => true], $context),
|
||||
'_aux' => true,
|
||||
]
|
||||
);
|
||||
$analysis->addAnnotation($items, $items->_context);
|
||||
if (!Generator::isDefault($property->ref)) {
|
||||
$property->items->ref = $property->ref;
|
||||
$property->ref = Generator::UNDEFINED;
|
||||
|
@ -135,7 +137,7 @@ class AugmentProperties
|
|||
if (!Util::mapNativeType($property, $type)) {
|
||||
$refKey = $this->toRefKey($context, $type);
|
||||
if (Generator::isDefault($property->ref) && array_key_exists($refKey, $refs)) {
|
||||
$this->applyRef($property, $refs[$refKey]);
|
||||
$this->applyRef($analysis, $property, $refs[$refKey]);
|
||||
} else {
|
||||
if ($typeSchema = $analysis->getSchemaForSource($context->type)) {
|
||||
if (Generator::isDefault($property->format)) {
|
||||
|
@ -175,16 +177,17 @@ class AugmentProperties
|
|||
return implode('|', $types);
|
||||
}
|
||||
|
||||
protected function applyRef(Property $property, string $ref): void
|
||||
protected function applyRef(Analysis $analysis, Property $property, string $ref): void
|
||||
{
|
||||
if ($property->nullable === true) {
|
||||
$property->oneOf = [
|
||||
new Schema([
|
||||
$schema = new Schema([
|
||||
'ref' => $ref,
|
||||
'_context' => $property->_context,
|
||||
'_aux' => true,
|
||||
]),
|
||||
];
|
||||
$analysis->addAnnotation($schema, $schema->_context);
|
||||
} else {
|
||||
$property->ref = $ref;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,10 @@ class AugmentSchemas
|
|||
continue;
|
||||
}
|
||||
|
||||
$schemaContext = $property->_context->with('class') ?: $property->_context->with('interface') ?: $property->_context->with('trait') ?: $property->_context->with('enum');
|
||||
$schemaContext = $property->_context->with('class')
|
||||
?: $property->_context->with('interface')
|
||||
?: $property->_context->with('trait')
|
||||
?: $property->_context->with('enum');
|
||||
if ($schemaContext->annotations) {
|
||||
foreach ($schemaContext->annotations as $annotation) {
|
||||
if ($annotation instanceof Schema) {
|
||||
|
@ -70,6 +73,7 @@ class AugmentSchemas
|
|||
'_context' => $annotation->_context,
|
||||
'_aux' => true,
|
||||
]);
|
||||
$analysis->addAnnotation($schema, $schema->_context);
|
||||
$annotation->allOf[] = $schema;
|
||||
}
|
||||
|
||||
|
@ -89,6 +93,12 @@ class AugmentSchemas
|
|||
if (Generator::isDefault($schema->type)) {
|
||||
if (is_array($schema->properties) && count($schema->properties) > 0) {
|
||||
$schema->type = 'object';
|
||||
} elseif (is_array($schema->allOf) && count($schema->allOf) > 0) {
|
||||
$schema->type = 'object';
|
||||
} elseif (is_array($schema->oneOf) && count($schema->oneOf) > 0) {
|
||||
$schema->type = 'object';
|
||||
} elseif (is_array($schema->anyOf) && count($schema->anyOf) > 0) {
|
||||
$schema->type = 'object';
|
||||
} elseif (is_array($schema->additionalProperties) && count($schema->additionalProperties) > 0) {
|
||||
$schema->type = 'object';
|
||||
} elseif (is_array($schema->patternProperties) && count($schema->patternProperties) > 0) {
|
||||
|
@ -122,6 +132,7 @@ class AugmentSchemas
|
|||
'_context' => $schema->_context,
|
||||
'_aux' => true,
|
||||
]);
|
||||
$analysis->addAnnotation($allOfPropertiesSchema, $allOfPropertiesSchema->_context);
|
||||
$schema->allOf[] = $allOfPropertiesSchema;
|
||||
}
|
||||
$allOfPropertiesSchema->properties = array_merge($allOfPropertiesSchema->properties, $schema->properties);
|
||||
|
|
|
@ -41,14 +41,14 @@ class BuildPaths
|
|||
foreach ($operations as $operation) {
|
||||
if ($operation->path) {
|
||||
if (empty($paths[$operation->path])) {
|
||||
$paths[$operation->path] = new PathItem(
|
||||
$paths[$operation->path] = $pathItem = new PathItem(
|
||||
[
|
||||
'path' => $operation->path,
|
||||
'_context' => new Context(['generated' => true], $operation->_context),
|
||||
'_aux' => true,
|
||||
]
|
||||
);
|
||||
$analysis->annotations->attach($paths[$operation->path]);
|
||||
$analysis->addAnnotation($pathItem, $pathItem->_context);
|
||||
}
|
||||
if ($paths[$operation->path]->merge([$operation])) {
|
||||
$operation->_context->logger->warning('Unable to merge ' . $operation->identity() . ' in ' . $operation->_context);
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace OpenApi\Processors;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations\AbstractAnnotation;
|
||||
use OpenApi\Annotations\Components;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use OpenApi\Annotations\Operation;
|
||||
use OpenApi\Annotations\Property;
|
||||
use OpenApi\Generator;
|
||||
|
||||
class CleanUnusedComponents
|
||||
{
|
||||
public function __invoke(Analysis $analysis)
|
||||
{
|
||||
if (Generator::isDefault($analysis->openapi->components)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// allow multiple runs to catch nested dependencies
|
||||
for ($ii = 0; $ii < 10; ++$ii) {
|
||||
if (!$this->cleanup($analysis)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function cleanup(Analysis $analysis): bool
|
||||
{
|
||||
$usedRefs = [];
|
||||
foreach ($analysis->annotations as $annotation) {
|
||||
if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && $annotation->ref !== null) {
|
||||
$usedRefs[$annotation->ref] = $annotation->ref;
|
||||
}
|
||||
foreach (['allOf', 'anyOf', 'oneOff'] as $sub) {
|
||||
if (property_exists($annotation, $sub) && !Generator::isDefault($annotation->{$sub})) {
|
||||
foreach ($annotation->{$sub} as $subElem) {
|
||||
if (is_object($subElem) && property_exists($subElem, 'ref') && !Generator::isDefault($subElem->ref) && $subElem->ref !== null) {
|
||||
$usedRefs[$subElem->ref] = $subElem->ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($annotation instanceof OpenApi || $annotation instanceof Operation) {
|
||||
if (!Generator::isDefault($annotation->security)) {
|
||||
foreach ($annotation->security as $security) {
|
||||
foreach (array_keys($security) as $securityName) {
|
||||
$ref = Components::COMPONENTS_PREFIX . 'securitySchemes/' . $securityName;
|
||||
$usedRefs[$ref] = $ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unusedRefs = [];
|
||||
foreach (Components::$_nested as $nested) {
|
||||
if (2 == count($nested)) {
|
||||
// $nested[1] is the name of the property that holds the component name
|
||||
[$componentType, $nameProperty] = $nested;
|
||||
if (!Generator::isDefault($analysis->openapi->components->{$componentType})) {
|
||||
foreach ($analysis->openapi->components->{$componentType} as $component) {
|
||||
$ref = Components::ref($component);
|
||||
if (!in_array($ref, $usedRefs)) {
|
||||
$unusedRefs[$ref] = [$ref, $nameProperty];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$detachNested = function (Analysis $analysis, AbstractAnnotation $annotation, callable $detachNested) {
|
||||
foreach ($annotation::$_nested as $nested) {
|
||||
$nestedKey = ((array) $nested)[0];
|
||||
if (!Generator::isDefault($annotation->{$nestedKey})) {
|
||||
if (is_array($annotation->{$nestedKey})) {
|
||||
foreach ($annotation->{$nestedKey} as $elem) {
|
||||
if ($elem instanceof AbstractAnnotation) {
|
||||
$detachNested($analysis, $elem, $detachNested);
|
||||
}
|
||||
}
|
||||
} elseif ($annotation->{$nestedKey} instanceof AbstractAnnotation) {
|
||||
$analysis->annotations->detach($annotation->{$nestedKey});
|
||||
}
|
||||
}
|
||||
}
|
||||
$analysis->annotations->detach($annotation);
|
||||
};
|
||||
|
||||
// remove unused
|
||||
foreach ($unusedRefs as $refDetails) {
|
||||
[$ref, $nameProperty] = $refDetails;
|
||||
[$hash, $components, $componentType, $name] = explode('/', $ref);
|
||||
foreach ($analysis->openapi->components->{$componentType} as $ii => $component) {
|
||||
if ($component->{$nameProperty} == $name) {
|
||||
$annotation = $analysis->openapi->components->{$componentType}[$ii];
|
||||
$detachNested($analysis, $annotation, $detachNested);
|
||||
unset($analysis->openapi->components->{$componentType}[$ii]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0 != count($unusedRefs);
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ class ExpandClasses
|
|||
$anchestorSchema = $analysis->getSchemaForSource($anchestor['context']->fullyQualifiedName($anchestor['class']));
|
||||
if ($anchestorSchema) {
|
||||
$refPath = !Generator::isDefault($anchestorSchema->schema) ? $anchestorSchema->schema : $anchestor['class'];
|
||||
$this->inheritFrom($schema, $anchestorSchema, $refPath, $anchestor['context']);
|
||||
$this->inheritFrom($analysis, $schema, $anchestorSchema, $refPath, $anchestor['context']);
|
||||
|
||||
// one anchestor is enough
|
||||
break;
|
||||
|
|
|
@ -45,7 +45,7 @@ class ExpandInterfaces
|
|||
$interfaceSchema = $analysis->getSchemaForSource($interfaceName);
|
||||
if ($interfaceSchema) {
|
||||
$refPath = !Generator::isDefault($interfaceSchema->schema) ? $interfaceSchema->schema : $interface['interface'];
|
||||
$this->inheritFrom($schema, $interfaceSchema, $refPath, $interface['context']);
|
||||
$this->inheritFrom($analysis, $schema, $interfaceSchema, $refPath, $interface['context']);
|
||||
} else {
|
||||
$this->mergeAnnotations($schema, $interface, $existing);
|
||||
$this->mergeMethods($schema, $interface, $existing);
|
||||
|
|
|
@ -34,7 +34,7 @@ class ExpandTraits
|
|||
$traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait']));
|
||||
if ($traitSchema) {
|
||||
$refPath = !Generator::isDefault($traitSchema->schema) ? $traitSchema->schema : $trait['trait'];
|
||||
$this->inheritFrom($schema, $traitSchema, $refPath, $trait['context']);
|
||||
$this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']);
|
||||
} else {
|
||||
if ($schema->_context->is('class')) {
|
||||
$this->mergeAnnotations($schema, $trait, $existing);
|
||||
|
|
|
@ -20,9 +20,7 @@ class MergeIntoComponents
|
|||
{
|
||||
$components = $analysis->openapi->components;
|
||||
if (Generator::isDefault($components)) {
|
||||
$context = new Context([], $analysis->context);
|
||||
$components = new Components(['_context' => $context]);
|
||||
$components->_context->generated = true;
|
||||
$components = new Components(['_context' => new Context(['generated' => true], $analysis->context)]);
|
||||
}
|
||||
|
||||
foreach ($analysis->annotations as $annotation) {
|
||||
|
|
|
@ -38,13 +38,14 @@ class MergeJsonContent
|
|||
if (Generator::isDefault($parent->content)) {
|
||||
$parent->content = [];
|
||||
}
|
||||
$parent->content['application/json'] = new MediaType([
|
||||
$parent->content['application/json'] = $mediaType = new MediaType([
|
||||
'schema' => $jsonContent,
|
||||
'example' => $jsonContent->example,
|
||||
'examples' => $jsonContent->examples,
|
||||
'_context' => new Context(['generated' => true], $jsonContent->_context),
|
||||
'_aux' => true,
|
||||
]);
|
||||
$analysis->addAnnotation($mediaType, $mediaType->_context);
|
||||
if (!$parent instanceof Parameter) {
|
||||
$parent->content['application/json']->mediaType = 'application/json';
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
namespace OpenApi\Processors;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations\Components;
|
||||
use OpenApi\Annotations\Property;
|
||||
use OpenApi\Annotations\Schema;
|
||||
|
@ -23,17 +24,18 @@ use OpenApi\Generator;
|
|||
*/
|
||||
trait MergeTrait
|
||||
{
|
||||
protected function inheritFrom(Schema $schema, Schema $from, string $refPath, Context $context): void
|
||||
protected function inheritFrom(Analysis $analysis, Schema $schema, Schema $from, string $refPath, Context $context): void
|
||||
{
|
||||
if (Generator::isDefault($schema->allOf)) {
|
||||
$schema->allOf = [];
|
||||
}
|
||||
// merging other properties into allOf is done in the AugmentSchemas processor
|
||||
$schema->allOf[] = new Schema([
|
||||
$schema->allOf[] = $refSchema = new Schema([
|
||||
'ref' => Components::ref($refPath),
|
||||
'_context' => $context,
|
||||
'_aux' => true,
|
||||
]);
|
||||
$analysis->addAnnotation($refSchema, $refSchema->_context);
|
||||
}
|
||||
|
||||
protected function mergeAnnotations(Schema $schema, array $from, array &$existing): void
|
||||
|
|
|
@ -38,13 +38,14 @@ class MergeXmlContent
|
|||
if (Generator::isDefault($parent->content)) {
|
||||
$parent->content = [];
|
||||
}
|
||||
$parent->content['application/xml'] = new MediaType([
|
||||
$parent->content['application/xml'] = $mediaType = new MediaType([
|
||||
'schema' => $xmlContent,
|
||||
'example' => $xmlContent->example,
|
||||
'examples' => $xmlContent->examples,
|
||||
'_context' => new Context(['generated' => true], $xmlContent->_context),
|
||||
'_aux' => true,
|
||||
]);
|
||||
$analysis->addAnnotation($mediaType, $mediaType->_context);
|
||||
if (!$parent instanceof Parameter) {
|
||||
$parent->content['application/xml']->mediaType = 'application/xml';
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ use OpenApi\Annotations\Schema;
|
|||
use OpenApi\Attributes\Get;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Tests\Fixtures\PHP\Inheritance\ExtendsClass;
|
||||
use OpenApi\Tests\Fixtures\PHP\Inheritance\ExtendsTrait;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
@ -188,7 +189,7 @@ class ReflectionAnalyserTest extends OpenApiTestCase
|
|||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
|
||||
$this->assertCount(1, $schemas);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
|
||||
/** @var Property[] $properties */
|
||||
$properties = $analysis->getAnnotationsOfType(Property::class);
|
||||
|
|
|
@ -13,6 +13,7 @@ use OpenApi\Annotations\Schema;
|
|||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
use OpenApi\Analysers\TokenAnalyser;
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Tests\Fixtures\Parser\User;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
||||
|
@ -250,7 +251,7 @@ class TokenAnalyserTest extends OpenApiTestCase
|
|||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
|
||||
$this->assertCount(1, $schemas);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
|
||||
/** @var Property[] $properties */
|
||||
$properties = $analysis->getAnnotationsOfType(Property::class, true);
|
||||
|
@ -268,7 +269,7 @@ class TokenAnalyserTest extends OpenApiTestCase
|
|||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
|
||||
$this->assertCount(1, $schemas);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
|
||||
/** @var Property[] $properties */
|
||||
$properties = $analysis->getAnnotationsOfType(Property::class);
|
||||
|
|
|
@ -10,6 +10,7 @@ use OpenApi\Analysis;
|
|||
use OpenApi\Annotations\Attachable;
|
||||
use OpenApi\Annotations\Schema;
|
||||
use OpenApi\Generator;
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Tests\Fixtures\Annotations\CustomAttachable;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
||||
|
@ -31,6 +32,7 @@ class AttachableTest extends OpenApiTestCase
|
|||
(new Generator())
|
||||
->addAlias('oaf', 'OpenApi\Tests\Fixtures\Annotations')
|
||||
->addNamespace('OpenApi\Tests\Fixtures\Annotations\\')
|
||||
->setProcessors($this->processors([CleanUnusedComponents::class]))
|
||||
->generate($this->fixtures(['UsingCustomAttachables.php']), $analysis);
|
||||
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace OpenApi\Tests\Annotations;
|
||||
|
||||
use OpenApi\Annotations\Examples;
|
||||
use OpenApi\Annotations\Components;
|
||||
use OpenApi\Annotations\Schema;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
||||
class ComponentsTest extends OpenApiTestCase
|
||||
{
|
||||
public function testRef()
|
||||
{
|
||||
$this->assertEquals('#/components/schemas/foo', Components::ref('foo'));
|
||||
$this->assertEquals('#/components/schemas/bar', Components::ref(new Schema(['ref' => null, 'schema' => 'bar'])));
|
||||
$this->assertEquals('#/components/examples/xx', Components::ref(new Examples(['example' => 'xx'])));
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
namespace OpenApi\Tests\Annotations;
|
||||
|
||||
use OpenApi\Generator;
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
||||
class ItemsTest extends OpenApiTestCase
|
||||
|
@ -33,7 +33,7 @@ class ItemsTest extends OpenApiTestCase
|
|||
|
||||
public function testRefDefinitionInProperty(): void
|
||||
{
|
||||
$analysis = $this->analysisFromFixtures(['UsingVar.php'], (new Generator())->getProcessors());
|
||||
$analysis = $this->analysisFromFixtures(['UsingVar.php'], $this->processors([CleanUnusedComponents::class]));
|
||||
|
||||
$this->assertCount(2, $analysis->openapi->components->schemas);
|
||||
$this->assertEquals('UsingVar', $analysis->openapi->components->schemas[0]->schema);
|
||||
|
|
|
@ -147,7 +147,7 @@ class ExamplesTest extends OpenApiTestCase
|
|||
if (0 === strpos($eKey, 'polymorphism') && 'token' == $aKey) {
|
||||
continue;
|
||||
}
|
||||
if (\PHP_VERSION_ID < 80100 && 'using-refs' == $eKey) {
|
||||
if ((\PHP_VERSION_ID < 80100 || 'token' == $aKey) && 'using-refs' == $eKey) {
|
||||
continue;
|
||||
}
|
||||
if ('using-links-php81' == $eKey && 'token' == $aKey) {
|
||||
|
|
|
@ -124,6 +124,7 @@ components:
|
|||
Product:
|
||||
title: Product
|
||||
description: Product
|
||||
type: object
|
||||
allOf:
|
||||
-
|
||||
$ref: '#/components/schemas/NameTrait'
|
||||
|
@ -139,12 +140,12 @@ components:
|
|||
example: null
|
||||
colour:
|
||||
$ref: '#/components/schemas/Colour'
|
||||
releasedAt:
|
||||
type: string
|
||||
id:
|
||||
description: 'The id.'
|
||||
format: int64
|
||||
example: 1
|
||||
releasedAt:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
const: Virtual
|
||||
|
|
|
@ -212,6 +212,17 @@ class OpenApiTestCase extends TestCase
|
|||
}, $files);
|
||||
}
|
||||
|
||||
public function processors(array $strip = [], array $add = []): array
|
||||
{
|
||||
$processors = (new Generator())->getProcessors();
|
||||
|
||||
$processors = array_filter($processors, function ($processor) use ($strip) {
|
||||
return !is_object($processor) || !in_array(get_class($processor), $strip);
|
||||
});
|
||||
|
||||
return $processors;
|
||||
}
|
||||
|
||||
public function analysisFromFixtures(array $files, array $processors = [], ?AnalyserInterface $analyzer = null): Analysis
|
||||
{
|
||||
$analysis = new Analysis([], $this->getContext());
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace OpenApi\Tests\Processors;
|
||||
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Tests\OpenApiTestCase;
|
||||
|
||||
class CleanUnusedComponentsTest extends OpenApiTestCase
|
||||
{
|
||||
public function processorCases()
|
||||
{
|
||||
$defaultProcessors = $this->processors([CleanUnusedComponents::class]);
|
||||
|
||||
return [
|
||||
'default' => [$defaultProcessors, 2],
|
||||
'clean' => [array_merge($defaultProcessors, [new CleanUnusedComponents()]), 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider processorCases
|
||||
*/
|
||||
public function testRefDefinitionInProperty(array $processors, $expectedCount): void
|
||||
{
|
||||
$analysis = $this->analysisFromFixtures(['UsingVar.php'], $processors);
|
||||
|
||||
$this->assertCount($expectedCount, $analysis->openapi->components->schemas);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ use OpenApi\Processors\AugmentProperties;
|
|||
use OpenApi\Processors\AugmentSchemas;
|
||||
use OpenApi\Processors\BuildPaths;
|
||||
use OpenApi\Processors\CleanUnmerged;
|
||||
use OpenApi\Processors\CleanUnusedComponents;
|
||||
use OpenApi\Processors\ExpandClasses;
|
||||
use OpenApi\Processors\ExpandInterfaces;
|
||||
use OpenApi\Processors\ExpandTraits;
|
||||
|
@ -79,11 +80,12 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
// this one doesn't
|
||||
'ExpandClasses/AncestorWithoutDocBlocks.php',
|
||||
]);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
$this->validate($analysis);
|
||||
|
||||
/** @var Schema[] $schemas */
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class);
|
||||
$this->assertCount(2, $schemas);
|
||||
$childSchema = $schemas[0];
|
||||
$this->assertSame('ChildWithDocBlocks', $childSchema->schema);
|
||||
$this->assertCount(1, $childSchema->properties);
|
||||
|
@ -103,12 +105,12 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
'ExpandClasses/Extended.php',
|
||||
'ExpandClasses/Base.php',
|
||||
]);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
$this->validate($analysis);
|
||||
|
||||
/** @var Schema[] $schemas */
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
$this->assertCount(3, $schemas);
|
||||
$this->assertCount(5, $schemas);
|
||||
|
||||
$extendedSchema = $schemas[0];
|
||||
$this->assertSame('ExtendedModel', $extendedSchema->schema);
|
||||
|
@ -131,12 +133,12 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
'ExpandClasses/ExtendedWithoutAllOf.php',
|
||||
'ExpandClasses/Base.php',
|
||||
]);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
$this->validate($analysis);
|
||||
|
||||
/** @var Schema[] $schemas */
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
$this->assertCount(2, $schemas);
|
||||
$this->assertCount(4, $schemas);
|
||||
|
||||
$extendedSchema = $schemas[0];
|
||||
$this->assertSame('ExtendedWithoutAllOf', $extendedSchema->schema);
|
||||
|
@ -158,12 +160,12 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
'ExpandClasses/ExtendedWithTwoSchemas.php',
|
||||
'ExpandClasses/Base.php',
|
||||
]);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
$this->validate($analysis);
|
||||
|
||||
/** @var Schema[] $schemas */
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
$this->assertCount(3, $schemas);
|
||||
$this->assertCount(7, $schemas);
|
||||
|
||||
$extendedSchema = $schemas[0];
|
||||
$this->assertSame('ExtendedWithTwoSchemas', $extendedSchema->schema);
|
||||
|
@ -191,7 +193,7 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
'ExpandClasses/BaseThatImplements.php',
|
||||
'ExpandClasses/TraitUsedByExtendsBaseThatImplements.php',
|
||||
]);
|
||||
$analysis->process((new Generator())->getProcessors());
|
||||
$analysis->process($this->processors([CleanUnusedComponents::class]));
|
||||
$this->validate($analysis);
|
||||
|
||||
$analysis->openapi->info = new Info(['title' => 'test', 'version' => '1.0.0', '_context' => $this->getContext()]);
|
||||
|
@ -200,7 +202,7 @@ class ExpandClassesTest extends OpenApiTestCase
|
|||
|
||||
/** @var Schema[] $schemas */
|
||||
$schemas = $analysis->getAnnotationsOfType(Schema::class, true);
|
||||
$this->assertCount(4, $schemas);
|
||||
$this->assertCount(10, $schemas);
|
||||
|
||||
$baseInterface = $schemas[0];
|
||||
$this->assertSame('BaseInterface', $baseInterface->schema);
|
||||
|
|
Loading…
Reference in New Issue