Skip to content

Commit 133a466

Browse files
committed
Refactor Util helper into non-static SourceFinder
1 parent e17314b commit 133a466

12 files changed

Lines changed: 158 additions & 250 deletions

File tree

bin/openapi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use OpenApi\Analysers\DocBlockAnnotationFactory;
66
use OpenApi\Analysers\ReflectionAnalyser;
77
use OpenApi\Annotations\OpenApi;
88
use OpenApi\Generator;
9-
use OpenApi\Util;
9+
use OpenApi\SourceFinder;
1010
use OpenApi\Loggers\ConsoleLogger;
1111

1212
if (class_exists(Generator::class) === false) {
@@ -224,7 +224,7 @@ $openapi = $generator
224224
->setVersion($options['version'])
225225
->setConfig($options['config'])
226226
->setAnalyser($analyser)
227-
->generate(Util::finder($paths, $exclude, $pattern));
227+
->generate(new SourceFinder($paths, $exclude, $pattern));
228228

229229
if ($options['output'] === false) {
230230
if (strtolower($options['format']) === 'json') {

docs/reference/generator.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,12 @@ require('"vendor/autoload.php');
5151
$openapi = (new \OpenApi\Generator())->generate(['/path1/to/project']);
5252
```
5353

54-
The `generate()` method does not support the CLI `exclude` and `pattern` options directly. Instead, a Symfony `Finder` instance can be passed in
55-
as source directly.
56-
57-
If needed, the `\OpenApi\Util` class provides a builder method that undertands these options.
54+
The `generate()` method does not support the CLI `exclude` and `pattern` options directly.
55+
Instead, a custom Symfony `Finder` class (`SourceFinder`) can be used that understands these options.
5856

5957
```php
6058
$exclude = ['tests'];
6159
$pattern = '*.php';
6260

63-
$openapi = (new \OpenApi\Generator())->generate(\OpenApi\Util::finder(__DIR__, $exclude, $pattern));
61+
$openapi = (new \OpenApi\Generator())->generate(new \OpenApi\SourceFinder(__DIR__, $exclude, $pattern));
6462
```

src/Annotations/AbstractAnnotation.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use OpenApi\Generator;
1111
use OpenApi\Annotations as OA;
1212
use OpenApi\OpenApiException;
13-
use OpenApi\Util;
1413
use Symfony\Component\Yaml\Yaml;
1514

1615
/**
@@ -454,14 +453,14 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
454453
if ($details = $this->matchNested($annotation)) {
455454
$property = $details->value;
456455
if (is_array($property)) {
457-
$this->_context->logger->warning('Only one ' . Util::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . ' multiple found, skipped: ' . $annotation->_context);
456+
$this->_context->logger->warning('Only one ' . static::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . ' multiple found, skipped: ' . $annotation->_context);
458457
} else {
459-
$this->_context->logger->warning('Only one ' . Util::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . " multiple found in:\n Using: " . $this->{$property}->_context . "\n Skipped: " . $annotation->_context);
458+
$this->_context->logger->warning('Only one ' . static::shorten(get_class($annotation)) . '() allowed for ' . $this->identity() . " multiple found in:\n Using: " . $this->{$property}->_context . "\n Skipped: " . $annotation->_context);
460459
}
461460
} elseif ($annotation instanceof AbstractAnnotation) {
462461
$message = 'Unexpected ' . $annotation->identity();
463462
if ($class::$_parents) {
464-
$message .= ', expected to be inside ' . implode(', ', Util::shorten($class::$_parents));
463+
$message .= ', expected to be inside ' . implode(', ', static::shorten($class::$_parents));
465464
}
466465
$this->_context->logger->warning($message . ' in ' . $annotation->_context);
467466
}
@@ -481,7 +480,7 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
481480
$keyField = $nested[1];
482481
foreach ($this->{$property} as $key => $item) {
483482
if (is_array($item) && is_numeric($key) === false) {
484-
$this->_context->logger->warning($this->identity() . '->' . $property . ' is an object literal, use nested ' . Util::shorten($annotationClass) . '() annotation(s) in ' . $this->_context);
483+
$this->_context->logger->warning($this->identity() . '->' . $property . ' is an object literal, use nested ' . static::shorten($annotationClass) . '() annotation(s) in ' . $this->_context);
485484
$keys[$key] = $item;
486485
} elseif (Generator::isDefault($item->{$keyField})) {
487486
$this->_context->logger->error($item->identity() . ' is missing key-field: "' . $keyField . '" in ' . $item->_context);
@@ -511,11 +510,11 @@ public function validate(array $stack = [], array $skip = [], string $ref = '',
511510
$nestedProperty = is_array($nested) ? $nested[0] : $nested;
512511
if ($property === $nestedProperty) {
513512
if ($this instanceof OpenApi) {
514-
$message = 'Required ' . Util::shorten($class) . '() not found';
513+
$message = 'Required ' . static::shorten($class) . '() not found';
515514
} elseif (is_array($nested)) {
516-
$message = $this->identity() . ' requires at least one ' . Util::shorten($class) . '() in ' . $this->_context;
515+
$message = $this->identity() . ' requires at least one ' . static::shorten($class) . '() in ' . $this->_context;
517516
} else {
518-
$message = $this->identity() . ' requires a ' . Util::shorten($class) . '() in ' . $this->_context;
517+
$message = $this->identity() . ' requires a ' . static::shorten($class) . '() in ' . $this->_context;
519518
}
520519
break;
521520
}
@@ -675,7 +674,7 @@ protected function _identity(array $properties): string
675674
}
676675
}
677676

678-
return Util::shorten(get_class($this)) . '(' . implode(',', $fields) . ')';
677+
return static::shorten(get_class($this)) . '(' . implode(',', $fields) . ')';
679678
}
680679

681680
/**
@@ -794,4 +793,24 @@ protected function combine(...$args): array
794793

795794
return array_filter($combined, fn ($value): bool => !Generator::isDefault($value) && $value !== null);
796795
}
796+
797+
/**
798+
* Shorten class name(s).
799+
*
800+
* @param array|object|string $classes Class(es) to shorten
801+
*
802+
* @return string|string[] One or more shortened class names
803+
*/
804+
protected static function shorten($classes)
805+
{
806+
$short = [];
807+
foreach ((array) $classes as $class) {
808+
$short[] = '@' . str_replace([
809+
'OpenApi\\Annotations\\',
810+
'OpenApi\\Attributes\\',
811+
], 'OA\\', $class);
812+
}
813+
814+
return is_array($classes) ? $short : array_pop($short);
815+
}
797816
}

src/Annotations/Components.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
namespace OpenApi\Annotations;
88

99
use OpenApi\Generator;
10-
use OpenApi\Util;
1110

1211
/**
1312
* Holds a set of reusable objects for different aspects of the OA.
@@ -151,6 +150,28 @@ public static function ref($component, bool $encode = true): string
151150
$name = $component;
152151
}
153152

154-
return self::COMPONENTS_PREFIX . $type . '/' . ($encode ? Util::refEncode((string) $name) : $name);
153+
return self::COMPONENTS_PREFIX . $type . '/' . ($encode ? static::refEncode((string) $name) : $name);
154+
}
155+
156+
/**
157+
* Escapes the special characters "/" and "~".
158+
*
159+
* https://swagger.io/docs/specification/using-ref/
160+
* https://tools.ietf.org/html/rfc6901#page-3
161+
*/
162+
public static function refEncode(string $raw): string
163+
{
164+
return str_replace('/', '~1', str_replace('~', '~0', $raw));
165+
}
166+
167+
/**
168+
* Converted the escaped characters "~1" and "~" back to "/" and "~".
169+
*
170+
* https://swagger.io/docs/specification/using-ref/
171+
* https://tools.ietf.org/html/rfc6901#page-3
172+
*/
173+
public static function refDecode(string $encoded): string
174+
{
175+
return str_replace('~1', '/', str_replace('~0', '~', $encoded));
155176
}
156177
}

src/Annotations/OpenApi.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use OpenApi\Analysis;
1010
use OpenApi\Generator;
1111
use OpenApi\OpenApiException;
12-
use OpenApi\Util;
1312

1413
/**
1514
* This is the root document object for the API specification.
@@ -215,7 +214,7 @@ private static function resolveRef(string $ref, string $resolved, $container, ar
215214
$slash = strpos($path, '/');
216215

217216
$subpath = $slash === false ? $path : substr($path, 0, $slash);
218-
$property = Util::refDecode($subpath);
217+
$property = Components::refDecode($subpath);
219218
$unresolved = $slash === false ? $resolved . $subpath : $resolved . $subpath . '/';
220219

221220
if (is_object($container)) {

src/Generator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ protected function scanSources(iterable $sources, Analysis $analysis, Context $r
362362
continue;
363363
}
364364
if (is_dir($resolvedSource)) {
365-
$this->scanSources(Util::finder($resolvedSource), $analysis, $rootContext);
365+
$this->scanSources(new SourceFinder($resolvedSource), $analysis, $rootContext);
366366
} else {
367367
$rootContext->logger->debug(sprintf('Analysing source: %s', $resolvedSource));
368368
$analysis->addAnalysis($analyser->fromFile($resolvedSource, $rootContext));

src/SourceFinder.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace OpenApi;
4+
5+
use Symfony\Component\Finder\Finder;
6+
7+
/**
8+
* Custom Symfony `Finder` that understands `swagger-php` CLI options.
9+
*/
10+
class SourceFinder extends Finder
11+
{
12+
public function __construct(string|array $directory, null|array|string $exclude = null, string $pattern = '*.php')
13+
{
14+
parent::__construct();
15+
16+
$this
17+
->sortByName()
18+
->files()
19+
->followLinks()
20+
->name($pattern);
21+
22+
$directories = (array) $directory;
23+
24+
foreach ($directories as $path) {
25+
if (is_file($path)) {
26+
$this->append([$path]);
27+
} else {
28+
$this->in($path);
29+
}
30+
}
31+
32+
foreach ((array) $exclude as $path) {
33+
$this->notPath($this->getRelativePath($path, $directories));
34+
}
35+
}
36+
37+
/**
38+
* Turns the given $fullPath into a relative path based on $basePaths, which can either
39+
* be a single string path, or a list of possible paths. If a list is given, the first
40+
* matching basePath in the list will be used to compute the relative path. If no
41+
* relative path could be computed, the original string will be returned because there
42+
* is always a chance it was a valid relative path to begin with.
43+
*
44+
* It should be noted that these are "relative paths" primarily in Finder's sense of them,
45+
* and conform specifically to what is expected by functions like <code>exclude()</code> and <code>notPath()</code>.
46+
*
47+
* In particular, leading and trailing slashes are removed.
48+
*/
49+
private function getRelativePath(string $fullPath, array $directories): string
50+
{
51+
foreach ($directories as $directory) {
52+
if (str_starts_with($directory, $fullPath)) {
53+
$relativePath = substr($directory, strlen($fullPath));
54+
55+
if ($relativePath !== '' && $relativePath !== '0') {
56+
return trim($relativePath, '/');
57+
}
58+
}
59+
}
60+
61+
return $fullPath;
62+
}
63+
}

0 commit comments

Comments
 (0)