Skip to content
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
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"cebe/php-openapi": "^1.7"
},
"require-dev": {
"phpunit/phpunit": "^10.1",
"phpstan/phpstan": "^1.10.56",
"squizlabs/php_codesniffer": "^3.7",
"phpunit/phpunit": "^10.5.36",
"phpstan/phpstan": "^1.12.6",
"squizlabs/php_codesniffer": "^3.5.4",
"mikey179/vfsstream": "^1.6.7",
"infection/infection": "^0.27.0"
"infection/infection": "^0.29.7"
},
"config": {
"allow-plugins": {
Expand Down
4 changes: 2 additions & 2 deletions src/Factory/V30/FromCebe.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public static function createOpenAPI(

/**
* todo when phpstan 1.11 stable is released
* replace the below lines with @phpstan-ignore nullsafe.neverNull
* replace the below lines with phpstan-ignore nullsafe.neverNull
* The reason for this is the cebe library does not specify that info is nullable
* However it is not always set, so it can be null
*/
return new Valid\V30\OpenAPI(new OpenAPI(
return Valid\V30\OpenAPI::fromPartial(new OpenAPI(
$openApi->openapi,
$openApi->info?->title, // @phpstan-ignore-line
$openApi->info?->version, // @phpstan-ignore-line
Expand Down
11 changes: 8 additions & 3 deletions src/ValueObject/Valid/Identifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ public function __construct(string $field, string ...$fields)
$this->chain = [$field, ...$fields];
}

public function append(string $primaryId, string $secondaryId = ''): self
public function append(string $primaryId, string $secondaryId = ''): Identifier
{
$field = sprintf(
'%s%s',
$primaryId,
$secondaryId === '' ? '' : "($secondaryId)"
);

return new self(...[...$this->chain, $field]);
return new Identifier(...[...$this->chain, $field]);
}

public function fromEnd(int $level): ?string
public function fromStart(int $level = 0): ?string
{
return $this->chain[$level] ?? null;
}

public function fromEnd(int $level = 0): ?string
{
return array_reverse($this->chain)[$level] ?? null;
}
Expand Down
109 changes: 65 additions & 44 deletions src/ValueObject/Valid/V30/OpenAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,88 +13,99 @@
final class OpenAPI extends Validated
{
/**
* @param array<int, Server> $servers
* Optional, may be left empty.
* If empty or unspecified, the array will contain the default Server.
* The default Server has "url" === "/" and no "variables"
* @var array<int, Server>
*/
public readonly array $servers;

/**
* REQUIRED
*
* @param array<string,PathItem> $paths
* REQUIRED:
* It may be empty due to ACL constraints
* The PathItem's relative endpoint key mapped to the PathItem
* @var array<string,PathItem>
*/
public readonly array $paths;
private function __construct(
Identifier $identifier,
public readonly array $servers,
public readonly array $paths
) {
parent::__construct($identifier);

public function __construct(Partial\OpenAPI $openAPI)
$this->reviewServers($this->servers);
$this->reviewPaths($this->paths);
}

public function withoutServers(): OpenAPI
{
if (!isset($openAPI->title) || !isset($openAPI->version)) {
throw InvalidOpenAPI::missingInfo();
}
return new OpenAPI(
$this->getIdentifier(),
[new Server($this->getIdentifier(), new Partial\Server('/'))],
array_map(fn($p) => $p->withoutServers(), $this->paths),
);
}

parent::__construct(new Identifier("$openAPI->title($openAPI->version)"));
public static function fromPartial(Partial\OpenAPI $openAPI): self
{
$identifier = new Identifier(sprintf(
'%s(%s)',
$openAPI->title ?? throw InvalidOpenAPI::missingInfo(),
$openAPI->version ?? throw InvalidOpenAPI::missingInfo(),
));

if (!isset($openAPI->openAPI)) {
throw InvalidOpenAPI::missingOpenAPIVersion($this->getIdentifier());
}
$openAPI->openAPI ??
throw InvalidOpenAPI::missingOpenAPIVersion($identifier);

$this->servers = $this->validateServers(
$this->getIdentifier(),
$openAPI->servers
);
$servers = self::validateServers($identifier, $openAPI->servers);
$paths = self::validatePaths($identifier, $servers, $openAPI->paths);

$this->paths = $this->validatePaths(
$this->getIdentifier(),
$openAPI->paths,
);
return new OpenAPI($identifier, $servers, $paths);
}

/**
* @param Partial\Server[] $servers
* @return array<int,Server>
*/
private function validateServers(
private static function validateServers(
Identifier $identifier,
array $servers
): array {
if (empty($servers)) {
return [new Server($identifier, new Partial\Server('/'))];
}

$result = array_values(array_map(
return array_values(array_map(
fn($s) => new Server($identifier, $s),
$servers
));
}

$uniqueURLS = array_unique(array_map(fn($s) => $s->url, $result));
if (count($result) !== count($uniqueURLS)) {
/**
* @param Server[] $servers
*/
private function reviewServers(array $servers): void
{
$uniqueURLS = array_unique(array_map(fn($s) => $s->url, $servers));
if (count($servers) !== count($uniqueURLS)) {
$this->addWarning(
'Server URLs are not unique',
Warning::IDENTICAL_SERVER_URLS
);
}

return $result;
}

/**
/**
* @param Server[] $servers
* @param null|Partial\PathItem[] $pathItems
* @return array<string,PathItem>
*/
private function validatePaths(
private static function validatePaths(
Identifier $identifier,
array $servers,
?array $pathItems
): array {
if (is_null($pathItems)) {
throw InvalidOpenAPI::missingPaths($identifier);
}

if (empty($pathItems)) {
$this->addWarning('No Paths in OpenAPI', Warning::EMPTY_PATHS);
}

$result = [];

foreach ($pathItems as $pathItem) {
Expand All @@ -115,27 +126,37 @@ private function validatePaths(
);
}

$result[$pathItem->path] = new PathItem(
$result[$pathItem->path] = PathItem::fromPartial(
$identifier->append($pathItem->path),
$this->servers,
$servers,
$pathItem
);
}

$this->checkForEquivalentPathTemplates($result);
$this->checkForDuplicatedOperationIds($result);
self::checkForEquivalentPathTemplates($result);
self::checkForDuplicatedOperationIds($result);

return $result;
}

/**
* @param PathItem[] $paths
*/
private function reviewPaths(array $paths): void
{
if (empty($paths)) {
$this->addWarning('No Paths in OpenAPI', Warning::EMPTY_PATHS);
}
}

/**
* @param array<string,PathItem> $pathItems
*/
private function checkForEquivalentPathTemplates(array $pathItems): void
private static function checkForEquivalentPathTemplates(array $pathItems): void
{
$regexToIdentifier = [];
foreach ($pathItems as $path => $pathItem) {
$regex = $this->getPathRegex($path);
$regex = self::getPathRegex($path);

if (isset($regexToIdentifier[$regex])) {
throw InvalidOpenAPI::equivalentTemplates(
Expand All @@ -151,7 +172,7 @@ private function checkForEquivalentPathTemplates(array $pathItems): void
/**
* @param PathItem[] $paths
*/
private function checkForDuplicatedOperationIds(array $paths): void
private static function checkForDuplicatedOperationIds(array $paths): void
{
$checked = [];

Expand All @@ -174,7 +195,7 @@ private function checkForDuplicatedOperationIds(array $paths): void
}
}

private function getPathRegex(string $path): string
private static function getPathRegex(string $path): string
{
$pattern = preg_replace('#{[^/]+}#', '{([^/]+)}', $path);

Expand Down
Loading