Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PathFilter processor to filter paths #1613

Merged
merged 2 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Annotations/PathItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ public function getProcessorPipeline(): Pipeline
new Processors\MergeXmlContent(),
new Processors\OperationId(),
new Processors\CleanUnmerged(),
new Processors\PathFilter(),
new Processors\CleanUnusedComponents(),
]);
}

Expand Down
4 changes: 3 additions & 1 deletion src/Processors/AugmentParameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public function isAugmentOperationParameters(): bool
/**
* If set to <code>true</code> 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)
Expand Down
39 changes: 33 additions & 6 deletions src/Processors/CleanUnusedComponents.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,42 @@
use OpenApi\Annotations as OA;
use OpenApi\Generator;

/**
* Tracks the use of all <code>Components</code> 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) {
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions src/Processors/Concerns/AnnotationTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Processors\Concerns;

use OpenApi\Annotations as OA;

trait AnnotationTrait
{
/**
* Collects a (complete) list of all nested/referenced annotations starting from the given root.
*
* @param string|array|iterable|OA\AbstractAnnotation $root
*/
public function collectAnnotations($root): \SplObjectStorage
{
$storage = new \SplObjectStorage();

$this->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);
}
}
}
}
}
}
48 changes: 0 additions & 48 deletions src/Processors/Concerns/CollectorTrait.php

This file was deleted.

105 changes: 105 additions & 0 deletions src/Processors/PathFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Processors;

use OpenApi\Analysis;
use OpenApi\Generator;
use OpenApi\Processors\Concerns\AnnotationTrait;

/**
* Allows to filter endpoints based on tags and/or path.
*
* If no `tags` or `paths` filters are set, no filtering is performed.
* All filter (regular) expressions must be enclosed within delimiter characters as they are used as-is.
*/
class PathFilter implements ProcessorInterface
{
use AnnotationTrait;

/** @var array */
protected $tags = [];

/** @var array */
protected $paths = [];

public function __construct(array $tags = [], array $paths = [])
{
$this->tags = $tags;
$this->paths = $paths;
}

public function getTags(): array
{
return $this->tags;
}

/**
* A list of regular expressions to match <code>tags</code> to include.
*
* @param array<string> $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 <code>paths</code> to include.
*
* @param array<string> $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;
}
}
}
11 changes: 8 additions & 3 deletions tests/Processors/CleanUnusedComponentsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace OpenApi\Tests\Processors;

use OpenApi\Generator;
use OpenApi\Processors\CleanUnusedComponents;
use OpenApi\Tests\OpenApiTestCase;

Expand All @@ -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],
];
}

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