Skip to content

Commit

Permalink
Merge pull request #51 from marcelthole/normalize-more-fields
Browse files Browse the repository at this point in the history
Normalize more fields
  • Loading branch information
marcelthole authored Jul 23, 2024
2 parents ca9d198 + dadf6b6 commit 7d249c8
Show file tree
Hide file tree
Showing 23 changed files with 460 additions and 57 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"require-dev": {
"doctrine/coding-standard": "^12.0.0",
"infection/infection": "^0.29.6",
"phpstan/phpstan": "^1.10.58",
"phpstan/phpstan-phpunit": "^1.3.15",
"phpstan/phpstan": "^1.11.0",
"phpstan/phpstan-phpunit": "^1.4.0",
"phpunit/phpunit": "^10.1 || ^11.0",
"roave/security-advisories": "dev-master"
},
Expand Down
11 changes: 11 additions & 0 deletions src/FileHandling/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public function getAbsoluteFile(): string
return $fullFilename;
}

public function exists(): bool
{
try {
$this->getAbsoluteFile();

return true;
} catch (IOException) {
return false;
}
}

public function getAbsolutePath(): string
{
return dirname($this->getAbsoluteFile());
Expand Down
10 changes: 10 additions & 0 deletions src/Merge/ComponentsMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public function merge(
);
}

if (
count($existingComponents->examples ?? []) > 0
|| count($newComponents->examples ?? []) > 0
) {
$mergedComponents->examples = array_merge(
$existingComponents->examples ?? [],
$newComponents->examples ?? [],
);
}

$clonedSpec = new OpenApi(Json::toArray($existingSpec->getSerializableData()));

$clonedSpec->components = $mergedComponents;
Expand Down
158 changes: 148 additions & 10 deletions src/Merge/ReferenceNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
use Mthole\OpenApiMerge\FileHandling\File;
use openapiphp\openapi\spec\MediaType;
use openapiphp\openapi\spec\OpenApi;
use openapiphp\openapi\spec\Parameter;
use openapiphp\openapi\spec\Reference;
use openapiphp\openapi\spec\RequestBody;
use openapiphp\openapi\spec\Response;
use openapiphp\openapi\spec\Schema;

use function array_map;
use function array_values;
use function assert;
use function count;
use function preg_match;
use function sha1;

use const DIRECTORY_SEPARATOR;

Expand All @@ -26,17 +31,55 @@ public function normalizeInlineReferences(
$refFileCollection = [];
foreach ($openApiDefinition->paths as $path) {
foreach ($path->getOperations() as $operation) {
assert($operation->responses !== null);
foreach ($operation->responses->getResponses() as $statusCode => $response) {
if ($response === null) {
foreach (($operation->parameters ?? []) as $parameterIndex => $parameter) {
if (! $parameter instanceof Parameter) {
continue;
}

/** @var array<int, Parameter> $allParameters */
$allParameters = $operation->parameters;
if ($parameter->schema instanceof Reference) {
$allParameters[$parameterIndex]->schema = $this->normalizeReference(
$parameter->schema,
$refFileCollection,
);
}

if ($parameter->schema instanceof Schema) {
$allParameters[$parameterIndex]->schema = $this->normalizeProperties(
$parameter->schema,
$refFileCollection,
);
}

$operation->parameters = $allParameters;
}

if ($operation->requestBody instanceof RequestBody) {
foreach ($operation->requestBody->content as $contentType => $content) {
$allRequestBodyContent = $operation->requestBody->content;
if (! $content->schema instanceof Schema) {

Check warning on line 61 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "InstanceOf_": @@ @@ if ($operation->requestBody instanceof RequestBody) { foreach ($operation->requestBody->content as $contentType => $content) { $allRequestBodyContent = $operation->requestBody->content; - if (!$content->schema instanceof Schema) { + if (!true) { continue; } $allRequestBodyContent[$contentType]->schema = $this->normalizeProperties($content->schema, $refFileCollection);
continue;
}

$allRequestBodyContent[$contentType]->schema = $this->normalizeProperties(
$content->schema,
$refFileCollection,
);
$operation->requestBody->content = $allRequestBodyContent;
}
}

assert($operation->responses !== null);
foreach ($operation->responses->getResponses() as $statusCode => $response) {
if ($response instanceof Reference) {
$operation->responses->addResponse(
(string) $statusCode,
$this->normalizeReference($response, $refFileCollection),
);
}

if (! ($response instanceof Response)) {
continue;
}

Expand Down Expand Up @@ -89,13 +132,46 @@ public function normalizeInlineReferences(
}
}

if ($openApiDefinition->components !== null) {

Check warning on line 135 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "NotIdentical": @@ @@ } } } - if ($openApiDefinition->components !== null) { + if ($openApiDefinition->components === null) { foreach ($openApiDefinition->components->schemas ?? [] as $key => $schema) { $allSchemas = $openApiDefinition->components->schemas; if (!$schema instanceof Schema) {
foreach (($openApiDefinition->components->schemas ?? []) as $key => $schema) {

Check warning on line 136 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "Coalesce": @@ @@ } } if ($openApiDefinition->components !== null) { - foreach ($openApiDefinition->components->schemas ?? [] as $key => $schema) { + foreach ([] ?? $openApiDefinition->components->schemas as $key => $schema) { $allSchemas = $openApiDefinition->components->schemas; if (!$schema instanceof Schema) { continue;

Check warning on line 136 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "Foreach_": @@ @@ } } if ($openApiDefinition->components !== null) { - foreach ($openApiDefinition->components->schemas ?? [] as $key => $schema) { + foreach ([] as $key => $schema) { $allSchemas = $openApiDefinition->components->schemas; if (!$schema instanceof Schema) { continue;
$allSchemas = $openApiDefinition->components->schemas;
if (! $schema instanceof Schema) {
continue;
}

$allSchemas[$key] = $this->normalizeProperties($schema, $refFileCollection);
$openApiDefinition->components->schemas = $allSchemas;
}

foreach (($openApiDefinition->components->responses ?? []) as $key => $response) {

Check warning on line 146 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "Coalesce": @@ @@ $allSchemas[$key] = $this->normalizeProperties($schema, $refFileCollection); $openApiDefinition->components->schemas = $allSchemas; } - foreach ($openApiDefinition->components->responses ?? [] as $key => $response) { + foreach ([] ?? $openApiDefinition->components->responses as $key => $response) { if (!$response instanceof Response) { continue; }

Check warning on line 146 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "Foreach_": @@ @@ $allSchemas[$key] = $this->normalizeProperties($schema, $refFileCollection); $openApiDefinition->components->schemas = $allSchemas; } - foreach ($openApiDefinition->components->responses ?? [] as $key => $response) { + foreach ([] as $key => $response) { if (!$response instanceof Response) { continue; }
if (! $response instanceof Response) {

Check warning on line 147 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "InstanceOf_": @@ @@ $openApiDefinition->components->schemas = $allSchemas; } foreach ($openApiDefinition->components->responses ?? [] as $key => $response) { - if (!$response instanceof Response) { + if (!true) { continue; } /** @var array<string, Response> $allSchemas */

Check warning on line 147 in src/Merge/ReferenceNormalizer.php

View workflow job for this annotation

GitHub Actions / Tests with coverage and PR Comments (8.3)

Escaped Mutant for Mutator "InstanceOf_": @@ @@ $openApiDefinition->components->schemas = $allSchemas; } foreach ($openApiDefinition->components->responses ?? [] as $key => $response) { - if (!$response instanceof Response) { + if (!false) { continue; } /** @var array<string, Response> $allSchemas */
continue;
}

/** @var array<string, Response> $allSchemas */
$allSchemas = $openApiDefinition->components->responses;
foreach ($response->content as $contentKey => $content) {
if (! $content->schema instanceof Schema) {
continue;
}

$allSchemas[$key]->content[$contentKey]->schema = $this->normalizeProperties(
$content->schema,
$refFileCollection,
);
}

$openApiDefinition->components->responses = $allSchemas;
}
}

return new ReferenceResolverResult(
$openApiDefinition,
$this->normalizeFilePaths($openApiFile, $refFileCollection),
);
}

/** @param list<string> $refFileCollection */
/** @param array<string, string> $refFileCollection */
private function normalizeReference(Reference $reference, array &$refFileCollection): Reference
{
$matches = [];
Expand All @@ -104,7 +180,7 @@ private function normalizeReference(Reference $reference, array &$refFileCollect
$refFile = $matches['referenceFile'];

if ($refFile !== '') {
$refFileCollection[] = $refFile;
$refFileCollection[sha1($refFile)] = $refFile;
}

return new Reference(['$ref' => $matches['referenceString']]);
Expand All @@ -114,17 +190,79 @@ private function normalizeReference(Reference $reference, array &$refFileCollect
}

/**
* @param list<string> $refFileCollection
* @param array<string, string> $refFileCollection
*
* @return list<File>
*/
private function normalizeFilePaths(File $openApiFile, array $refFileCollection): array
{
return array_map(
static fn (string $refFile): File => new File(
$openApiFile->getAbsolutePath() . DIRECTORY_SEPARATOR . $refFile,
),
$refFileCollection,
static function (string $refFile) use ($openApiFile): File {
$absoluteFile = new File($refFile);
if ($absoluteFile->exists()) {
return $absoluteFile;
}

return new File(
$openApiFile->getAbsolutePath() . DIRECTORY_SEPARATOR . $refFile,
);
},
array_values($refFileCollection),
);
}

/** @param array<string, string> $refFileCollection */
public function normalizeProperties(Schema $schema, array &$refFileCollection): Schema
{
if (! isset($schema->properties)) {
return $schema;
}

$schema = $this->normalizeProperty($schema, $refFileCollection);
$newProperties = $schema->properties;
foreach ($schema->properties as $propertyName => $property) {
$newProperties[$propertyName] = $this->normalizeProperty($property, $refFileCollection);
$schema->properties = $newProperties;
}

return $schema;
}

/**
* @param TArg $property
* @param array<string, string> $refFileCollection
*
* @phpstan-return TArg
*
* @template TArg of Reference|Schema
*/
public function normalizeProperty(Reference|Schema $property, array &$refFileCollection): Reference|Schema
{
if ($property instanceof Reference) {
return $this->normalizeReference($property, $refFileCollection);
}

if (! ($property instanceof Schema)) {
return $property;
}

if ($property->items !== null) {
$property->items = $this->normalizeProperty($property->items, $refFileCollection);
}

if ($property->additionalProperties instanceof Reference || $property->additionalProperties instanceof Schema) {
$property->additionalProperties = $this->normalizeProperty(
$property->additionalProperties,
$refFileCollection,
);
}

foreach (($property->anyOf ?? []) as $index => $anyOf) {
$anyOfs = $property->anyOf;
$anyOfs[$index] = $this->normalizeProperty($anyOf, $refFileCollection);
$property->anyOf = $anyOfs;
}

return $property;
}
}
24 changes: 21 additions & 3 deletions src/OpenApiMerge.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use Mthole\OpenApiMerge\Merge\ReferenceNormalizer;
use Mthole\OpenApiMerge\Reader\FileReader;

use function array_push;
use function array_flip;
use function array_key_exists;
use function array_map;
use function count;

class OpenApiMerge implements OpenApiMergeInterface
Expand All @@ -23,11 +25,18 @@ public function __construct(
) {
}

/** @param list<File> $additionalFiles */
/** @param array<array-key, File> $additionalFiles */
public function mergeFiles(File $baseFile, array $additionalFiles, bool $resolveReference = true): SpecificationFile
{
$mergedOpenApiDefinition = $this->openApiReader->readFile($baseFile, $resolveReference)->getOpenApi();

$additionalFileHashMap = array_flip(
array_map(
static fn (File $file): string => $file->getAbsoluteFile(),
$additionalFiles,
),
);

// use "for" instead of "foreach" to iterate over new added files
for ($i = 0; $i < count($additionalFiles); $i++) {
$additionalFile = $additionalFiles[$i];
Expand All @@ -37,7 +46,16 @@ public function mergeFiles(File $baseFile, array $additionalFiles, bool $resolve
$additionalFile,
$additionalDefinition,
);
array_push($additionalFiles, ...$resolvedReferenceResult->getFoundReferenceFiles());

foreach ($resolvedReferenceResult->getFoundReferenceFiles() as $foundReferenceFile) {
if (array_key_exists($foundReferenceFile->getAbsoluteFile(), $additionalFileHashMap)) {
continue;
}

$additionalFiles[] = $foundReferenceFile;
$additionalFileHashMap[$foundReferenceFile->getAbsoluteFile()] = true;
}

$additionalDefinition = $resolvedReferenceResult->getNormalizedDefinition();
}

Expand Down
2 changes: 1 addition & 1 deletion src/OpenApiMergeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

interface OpenApiMergeInterface
{
/** @param list<File> $additionalFiles */
/** @param array<array-key, File> $additionalFiles */
public function mergeFiles(
File $baseFile,
array $additionalFiles,
Expand Down
6 changes: 3 additions & 3 deletions tests/Console/Command/MergeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
use const PHP_EOL;

#[CoversClass(MergeCommand::class)]
#[UsesClass('\Mthole\OpenApiMerge\FileHandling\File')]
#[UsesClass('\Mthole\OpenApiMerge\FileHandling\SpecificationFile')]
#[UsesClass(File::class)]
#[UsesClass(SpecificationFile::class)]
class MergeCommandTest extends TestCase
{
#[DataProvider('invalidArgumentsDataProvider')]
Expand Down Expand Up @@ -141,7 +141,7 @@ public function write(SpecificationFile $specFile): string
}
};
$openApiMergeInterface = new class implements OpenApiMergeInterface {
/** @param list<File> $additionalFiles */
/** @param array<array-key, File> $additionalFiles */
public function mergeFiles(
File $baseFile,
array $additionalFiles,
Expand Down
4 changes: 3 additions & 1 deletion tests/FileHandling/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use function str_replace;

#[CoversClass(File::class)]
#[UsesClass('\Mthole\OpenApiMerge\FileHandling\Exception\IOException')]
#[UsesClass(IOException::class)]
class FileTest extends TestCase
{
#[DataProvider('fileExtensionProvider')]
Expand Down Expand Up @@ -52,6 +52,7 @@ public function testGetAbsoluteFileWithAbsoluteInvalidFile(): void
$invalidFilename = __FILE__ . '-nonexisting.dat';
$sut = new File($invalidFilename);

self::assertFalse($sut->exists());
$this->expectException(IOException::class);
$this->expectExceptionMessageMatches('~"' . preg_quote($invalidFilename, '~') . '"~');

Expand Down Expand Up @@ -81,6 +82,7 @@ public function testGetAbsoluteFile(): void
public function testGetAbsolutePath(): void
{
$sut = new File(__FILE__);
self::assertTrue($sut->exists());
self::assertSame(__DIR__, $sut->getAbsolutePath());
}
}
2 changes: 1 addition & 1 deletion tests/FileHandling/SpecificationFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use PHPUnit\Framework\TestCase;

#[CoversClass(SpecificationFile::class)]
#[UsesClass('\Mthole\OpenApiMerge\FileHandling\File')]
#[UsesClass(File::class)]
class SpecificationFileTest extends TestCase
{
public function testGetter(): void
Expand Down
Loading

0 comments on commit 7d249c8

Please sign in to comment.