diff --git a/src/Annotations/PathItem.php b/src/Annotations/PathItem.php index cfeeedda..c99efd5c 100644 --- a/src/Annotations/PathItem.php +++ b/src/Annotations/PathItem.php @@ -155,4 +155,21 @@ class PathItem extends AbstractAnnotation public static $_parents = [ OpenApi::class, ]; + + /** + * Returns a list of all operations (all methods) for this path item. + * + * @return Operation[] + */ + public function operations(): array + { + $operations = []; + foreach (PathItem::$_nested as $className => $property) { + if (is_subclass_of($className, Operation::class) && !Generator::isDefault($this->{$property})) { + $operations[] = $this->{$property}; + } + } + + return $operations; + } } diff --git a/src/Generator.php b/src/Generator.php index ebcbf320..16d61acd 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -277,6 +277,8 @@ public function getProcessorPipeline(): Pipeline new Processors\OperationId(), new Processors\AugmentTags(), new Processors\CleanUnmerged(), + new Processors\PathFilter(), + new Processors\CleanUnusedComponents(), ]); } diff --git a/src/Processors/AugmentParameters.php b/src/Processors/AugmentParameters.php index cb5818e1..47670724 100644 --- a/src/Processors/AugmentParameters.php +++ b/src/Processors/AugmentParameters.php @@ -30,9 +30,11 @@ public function isAugmentOperationParameters(): bool /** * If set to true try to find operation parameter descriptions in the operation docblock. */ - public function setAugmentOperationParameters(bool $augmentOperationParameters): void + public function setAugmentOperationParameters(bool $augmentOperationParameters): AugmentParameters { $this->augmentOperationParameters = $augmentOperationParameters; + + return $this; } public function __invoke(Analysis $analysis) diff --git a/src/Processors/CleanUnusedComponents.php b/src/Processors/CleanUnusedComponents.php index 4a7303c3..624ae7e4 100644 --- a/src/Processors/CleanUnusedComponents.php +++ b/src/Processors/CleanUnusedComponents.php @@ -10,17 +10,42 @@ use OpenApi\Annotations as OA; use OpenApi\Generator; +/** + * Tracks the use of all Components and removed unused schemas. + */ class CleanUnusedComponents implements ProcessorInterface { - use Concerns\CollectorTrait; + use Concerns\AnnotationTrait; + + /** + * @var bool + */ + protected $enabled = false; + + public function __construct(bool $enabled = false) + { + $this->enabled = $enabled; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function setEnabled(bool $enabled): CleanUnusedComponents + { + $this->enabled = $enabled; + + return $this; + } public function __invoke(Analysis $analysis) { - if (Generator::isDefault($analysis->openapi->components)) { + if (!$this->enabled || Generator::isDefault($analysis->openapi->components)) { return; } - $analysis->annotations = $this->collect($analysis->annotations); + $analysis->annotations = $this->collectAnnotations($analysis->annotations); // allow multiple runs to catch nested dependencies for ($ii = 0; $ii < 10; ++$ii) { @@ -83,10 +108,12 @@ protected function cleanup(Analysis $analysis): bool foreach ($analysis->openapi->components->{$componentType} as $ii => $component) { if ($component->{$nameProperty} == $name) { $annotation = $analysis->openapi->components->{$componentType}[$ii]; - foreach ($this->collect([$annotation]) as $unused) { - $analysis->annotations->detach($unused); - } + $this->removeAnnotation($analysis->annotations, $annotation); unset($analysis->openapi->components->{$componentType}[$ii]); + + if (!$analysis->openapi->components->{$componentType}) { + $analysis->openapi->components->{$componentType} = Generator::UNDEFINED; + } } } } diff --git a/src/Processors/Concerns/AnnotationTrait.php b/src/Processors/Concerns/AnnotationTrait.php new file mode 100644 index 00000000..0db40707 --- /dev/null +++ b/src/Processors/Concerns/AnnotationTrait.php @@ -0,0 +1,67 @@ +traverseAnnotations($root, function ($item) use (&$storage) { + if ($item instanceof OA\AbstractAnnotation && !$storage->contains($item)) { + $storage->attach($item); + } + }); + + return $storage; + } + + /** + * Remove all annotations that are part of the `$annotation` tree. + */ + public function removeAnnotation(iterable $root, OA\AbstractAnnotation $annotation): void + { + $remove = $this->collectAnnotations($annotation); + $this->traverseAnnotations($root, function ($item) use ($remove) { + if ($item instanceof \SplObjectStorage) { + foreach ($remove as $annotation) { + $item->detach($annotation); + } + } + }); + } + + /** + * @param string|array|iterable|OA\AbstractAnnotation $root + */ + public function traverseAnnotations($root, callable $callable): void + { + $callable($root); + + if (is_iterable($root)) { + foreach ($root as $value) { + $this->traverseAnnotations($value, $callable); + } + } elseif ($root instanceof OA\AbstractAnnotation) { + foreach (array_merge($root::$_nested, ['allOf', 'anyOf', 'oneOf', 'callbacks']) as $properties) { + foreach ((array) $properties as $property) { + if (isset($root->{$property})) { + $this->traverseAnnotations($root->{$property}, $callable); + } + } + } + } + } +} diff --git a/src/Processors/Concerns/CollectorTrait.php b/src/Processors/Concerns/CollectorTrait.php deleted file mode 100644 index 8e2ef8c1..00000000 --- a/src/Processors/Concerns/CollectorTrait.php +++ /dev/null @@ -1,48 +0,0 @@ -traverse($root, function (OA\AbstractAnnotation $annotation) use (&$storage) { - $storage->attach($annotation); - }); - - return $storage; - } - - /** - * @param string|array|OA\AbstractAnnotation $root - */ - public function traverse($root, callable $callable): void - { - if (is_iterable($root)) { - foreach ($root as $value) { - $this->traverse($value, $callable); - } - } elseif ($root instanceof OA\AbstractAnnotation) { - $callable($root); - - foreach (array_merge($root::$_nested, ['allOf', 'anyOf', 'oneOf', 'callbacks']) as $properties) { - foreach ((array) $properties as $property) { - if (isset($root->{$property})) { - $this->traverse($root->{$property}, $callable); - } - } - } - } - } -} diff --git a/src/Processors/PathFilter.php b/src/Processors/PathFilter.php new file mode 100644 index 00000000..13c3fd5e --- /dev/null +++ b/src/Processors/PathFilter.php @@ -0,0 +1,105 @@ +tags = $tags; + $this->paths = $paths; + } + + public function getTags(): array + { + return $this->tags; + } + + /** + * A list of regular expressions to match tags to include. + * + * @param array $tags + */ + public function setTags(array $tags): PathFilter + { + $this->tags = $tags; + + return $this; + } + + public function getPaths(): array + { + return $this->paths; + } + + /** + * A list of regular expressions to match paths to include. + * + * @param array $paths + */ + public function setPaths(array $paths): PathFilter + { + $this->paths = $paths; + + return $this; + } + + public function __invoke(Analysis $analysis) + { + if (($this->tags || $this->paths) && !Generator::isDefault($analysis->openapi->paths)) { + $filtered = []; + foreach ($analysis->openapi->paths as $pathItem) { + $matched = null; + foreach ($this->tags as $pattern) { + foreach ($pathItem->operations() as $operation) { + if (!Generator::isDefault($operation->tags)) { + foreach ($operation->tags as $tag) { + if (preg_match($pattern, $tag)) { + $matched = $pathItem; + break 3; + } + } + } + } + } + + foreach ($this->paths as $pattern) { + if (preg_match($pattern, $pathItem->path)) { + $matched = $pathItem; + break; + } + } + + if ($matched) { + $filtered[] = $matched; + } else { + $this->removeAnnotation($analysis->annotations, $pathItem); + } + } + + $analysis->openapi->paths = $filtered; + } + } +} diff --git a/tests/Processors/CleanUnusedComponentsTest.php b/tests/Processors/CleanUnusedComponentsTest.php index ee9f51bc..bcc0fcb6 100644 --- a/tests/Processors/CleanUnusedComponentsTest.php +++ b/tests/Processors/CleanUnusedComponentsTest.php @@ -6,6 +6,7 @@ namespace OpenApi\Tests\Processors; +use OpenApi\Generator; use OpenApi\Processors\CleanUnusedComponents; use OpenApi\Tests\OpenApiTestCase; @@ -17,9 +18,9 @@ public static function countCases(): iterable return [ 'var-default' => [$defaultProcessors, 'UsingVar.php', 2, 5], - 'var-clean' => [array_merge($defaultProcessors, [new CleanUnusedComponents()]), 'UsingVar.php', 0, 2], + 'var-clean' => [array_merge($defaultProcessors, [new CleanUnusedComponents(true)]), 'UsingVar.php', 0, 2], 'unreferenced-default' => [$defaultProcessors, 'Unreferenced.php', 2, 11], - 'unreferenced-clean' => [array_merge($defaultProcessors, [new CleanUnusedComponents()]), 'Unreferenced.php', 0, 5], + 'unreferenced-clean' => [array_merge($defaultProcessors, [new CleanUnusedComponents(true)]), 'Unreferenced.php', 0, 5], ]; } @@ -30,7 +31,11 @@ public function testCounts(array $processors, string $fixture, int $expectedSche { $analysis = $this->analysisFromFixtures([$fixture], $processors); - $this->assertCount($expectedSchemaCount, $analysis->openapi->components->schemas); + if ($expectedSchemaCount === 0) { + $this->assertTrue(Generator::isDefault($analysis->openapi->components->schemas)); + } else { + $this->assertCount($expectedSchemaCount, $analysis->openapi->components->schemas); + } $this->assertCount($expectedAnnotationCount, $analysis->annotations); } }