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:
Martin Rademacher 2022-04-22 11:03:38 +12:00 committed by GitHub
parent bda506f64f
commit a36ba9ac4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 300 additions and 56 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
bin/
!bin/openapi
composer.lock
coverage/
docs/.vitepress/dist/
docs/node_modules/
docs/package-lock.json

View File

@ -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 }

View File

@ -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()

View File

@ -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'

View File

@ -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'

View File

@ -3,7 +3,7 @@
namespace OpenApi\Examples\UsingInterfaces;
/**
* @OA\Schema(title="Pet")
* @OA\Schema(title="GreenProduct")
*/
class GreenProduct extends Product implements ColorInterface
{

View File

@ -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'

View File

@ -28,4 +28,9 @@ class Product extends Model
* @OA\Property(ref="#/components/schemas/product_status")
*/
public $status;
/**
* @OA\Property
*/
public StockLevel $stockLevel;
}

View File

@ -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"
* )
* )
*/

View File

@ -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:

View File

@ -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",

View File

@ -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")
* }
* )
* )
* )
*/

View File

@ -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'

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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';
}

View File

@ -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

View File

@ -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';
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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'])));
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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);