diff --git a/composer.json b/composer.json index 989ce3c..27a93fb 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/Factory/V30/FromCebe.php b/src/Factory/V30/FromCebe.php index f20e99f..145f49f 100644 --- a/src/Factory/V30/FromCebe.php +++ b/src/Factory/V30/FromCebe.php @@ -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 diff --git a/src/ValueObject/Valid/Identifier.php b/src/ValueObject/Valid/Identifier.php index 9d0f19d..5a1c663 100644 --- a/src/ValueObject/Valid/Identifier.php +++ b/src/ValueObject/Valid/Identifier.php @@ -16,7 +16,7 @@ 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', @@ -24,10 +24,15 @@ public function append(string $primaryId, string $secondaryId = ''): self $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; } diff --git a/src/ValueObject/Valid/V30/OpenAPI.php b/src/ValueObject/Valid/V30/OpenAPI.php index 5194e0b..7bd40c0 100644 --- a/src/ValueObject/Valid/V30/OpenAPI.php +++ b/src/ValueObject/Valid/V30/OpenAPI.php @@ -13,49 +13,58 @@ final class OpenAPI extends Validated { /** + * @param array $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 - */ - public readonly array $servers; - - /** - * REQUIRED + * + * @param array $paths + * REQUIRED: * It may be empty due to ACL constraints * The PathItem's relative endpoint key mapped to the PathItem - * @var array */ - 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 */ - private function validateServers( + private static function validateServers( Identifier $identifier, array $servers ): array { @@ -63,38 +72,40 @@ private function validateServers( 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 */ - 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) { @@ -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 $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( @@ -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 = []; @@ -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); diff --git a/src/ValueObject/Valid/V30/Operation.php b/src/ValueObject/Valid/V30/Operation.php index ed35c53..6a5f23b 100644 --- a/src/ValueObject/Valid/V30/Operation.php +++ b/src/ValueObject/Valid/V30/Operation.php @@ -15,54 +15,77 @@ final class Operation extends Validated { /** + * @param array $servers * Optional, may be left empty. * If empty or unspecified, the array will contain the Path level servers - * @var array - */ - public readonly array $servers; - - /** + * + * @param Parameter[] $parameters * The list MUST NOT include duplicated parameters. * A unique parameter is defined by a combination of a name and location. - * @var Parameter[] - */ - public readonly array $parameters; - - /** + * + * @param string $operationId * Required by Membrane * MUST be unique, value is case-sensitive. */ - public readonly string $operationId; + private function __construct( + Identifier $identifier, + public readonly string $operationId, + public readonly array $servers, + public readonly array $parameters, + ) { + parent::__construct($identifier); + + $this->reviewServers($this->servers); + $this->reviewParameters($this->parameters); + } + + public function withoutServers(): Operation + { + return new Operation( + $this->getIdentifier(), + $this->operationId, + [new Server($this->getIdentifier(), new Partial\Server('/'))], + $this->parameters, + ); + } /** * @param Server[] $pathServers * @param Parameter[] $pathParameters */ - public function __construct( + public static function fromPartial( Identifier $parentIdentifier, array $pathServers, array $pathParameters, Method $method, Partial\Operation $operation, - ) { - $this->operationId = $operation->operationId ?? + ): Operation { + $operationId = $operation->operationId ?? throw CannotSupport::missingOperationId( $parentIdentifier->fromEnd(0) ?? '', $method->value, ); - parent::__construct($parentIdentifier->append("$this->operationId($method->value)")); + $identifier = $parentIdentifier->append("$operationId($method->value)"); - $this->servers = $this->validateServers( - $this->getIdentifier(), + $servers = self::validateServers( + $identifier, $pathServers, $operation->servers, ); - $this->parameters = $this->validateParameters( + $parameters = self::validateParameters( + $identifier, $pathParameters, $operation->parameters ); + + return new Operation( + $identifier, + $operationId, + $servers, + $parameters, + ); } /** @@ -70,7 +93,7 @@ public function __construct( * @param Partial\Server[] $operationServers * @return array> */ - private function validateServers( + private static function validateServers( Identifier $identifier, array $pathServers, array $operationServers @@ -84,15 +107,21 @@ private function validateServers( $operationServers )); - $uniqueURLS = array_unique(array_map(fn($s) => $s->url, $result)); - if (count($result) !== count($uniqueURLS)) { + return $result; + } + + /** + * @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; } /** @@ -100,22 +129,44 @@ private function validateServers( * @param Partial\Parameter[] $operationParameters * @return Parameter[] */ - private function validateParameters( + private static function validateParameters( + Identifier $identifier, array $pathParameters, array $operationParameters ): array { - $result = $this->mergeParameters($operationParameters, $pathParameters); + $result = self::mergeParameters( + $identifier, + $operationParameters, + $pathParameters + ); foreach ($result as $index => $parameter) { foreach (array_slice($result, $index + 1) as $otherParameter) { if ($parameter->isIdentical($otherParameter)) { throw InvalidOpenAPI::duplicateParameters( - $this->getIdentifier(), + $identifier, $parameter->getIdentifier(), $otherParameter->getIdentifier(), ); } + if ($parameter->canConflictWith($otherParameter)) { + throw CannotSupport::conflictingParameterStyles( + (string) $parameter->getIdentifier(), + (string) $otherParameter->getIdentifier(), + ); + } + } + } + + return $result; + } + + /** @param array $parameters */ + private function reviewParameters(array $parameters): void + { + foreach ($parameters as $index => $parameter) { + foreach (array_slice($parameters, $index + 1) as $otherParameter) { if ($parameter->isSimilar($otherParameter)) { $this->addWarning( <<canConflictWith($otherParameter)) { - throw CannotSupport::conflictingParameterStyles( - (string) $parameter->getIdentifier(), - (string) $otherParameter->getIdentifier(), - ); - } } } - - return $result; } /** @@ -144,10 +186,13 @@ private function validateParameters( * @param Parameter[] $pathParameters * @return array */ - private function mergeParameters(array $operationParameters, array $pathParameters): array - { + private static function mergeParameters( + Identifier $identifier, + array $operationParameters, + array $pathParameters + ): array { $result = array_map( - fn($p) => new Parameter($this->getIdentifier(), $p), + fn($p) => new Parameter($identifier, $p), $operationParameters ); diff --git a/src/ValueObject/Valid/V30/PathItem.php b/src/ValueObject/Valid/V30/PathItem.php index 04e2a68..bbc504a 100644 --- a/src/ValueObject/Valid/V30/PathItem.php +++ b/src/ValueObject/Valid/V30/PathItem.php @@ -14,56 +14,134 @@ final class PathItem extends Validated { /** - * Optional, may be left empty. - * If empty or unspecified, the array will contain the OpenAPI level servers - * @var array - */ - public readonly array $servers; - - /** + * @param array $servers + * + * @param array $parameters * The list MUST NOT include duplicated parameters. * A unique parameter is defined by a combination of a name and location. - * @var array */ - public readonly array $parameters; + private function __construct( + Identifier $identifier, + public readonly array $servers, + public readonly array $parameters, + public readonly ?Operation $get, + public readonly ?Operation $put, + public readonly ?Operation $post, + public readonly ?Operation $delete, + public readonly ?Operation $options, + public readonly ?Operation $head, + public readonly ?Operation $patch, + public readonly ?Operation $trace, + ) { + parent::__construct($identifier); - public readonly ?Operation $get; - public readonly ?Operation $put; - public readonly ?Operation $post; - public readonly ?Operation $delete; - public readonly ?Operation $options; - public readonly ?Operation $head; - public readonly ?Operation $patch; - public readonly ?Operation $trace; + $this->reviewServers($servers); + $this->reviewParameters($parameters); + $this->reviewOperations($this->getOperations()); + } + + public function withoutServers(): PathItem + { + return new PathItem( + $this->getIdentifier(), + [new Server( + new Identifier($this->getIdentifier()->fromStart() ?? ''), + new Partial\Server('/') + ), + ], + $this->parameters, + $this->get?->withoutServers(), + $this->put?->withoutServers(), + $this->post?->withoutServers(), + $this->delete?->withoutServers(), + $this->options?->withoutServers(), + $this->head?->withoutServers(), + $this->patch?->withoutServers(), + $this->trace?->withoutServers(), + ); + } /** * @param array $openAPIServers + * If the pathItem does not contain servers, this will be used instead */ - public function __construct( + public static function fromPartial( Identifier $identifier, array $openAPIServers, Partial\PathItem $pathItem, - ) { - parent::__construct($identifier); - - $this->servers = $this->validateServers( + ): PathItem { + $servers = self::validateServers( $identifier, $openAPIServers, $pathItem->servers, ); - $this->parameters = $this->validateParameters($pathItem->parameters); - - $this->get = $this->validateOperation(Method::GET, $pathItem->get); - $this->put = $this->validateOperation(Method::PUT, $pathItem->put); - $this->post = $this->validateOperation(Method::POST, $pathItem->post); - $this->delete = $this->validateOperation(Method::DELETE, $pathItem->delete); - $this->options = $this->validateOperation(Method::OPTIONS, $pathItem->options); - $this->head = $this->validateOperation(Method::HEAD, $pathItem->head); - $this->patch = $this->validateOperation(Method::PATCH, $pathItem->patch); - $this->trace = $this->validateOperation(Method::TRACE, $pathItem->trace); + $parameters = self::validateParameters( + $identifier, + $pathItem->parameters + ); - $this->checkOperations($this->getOperations()); + return new PathItem( + identifier: $identifier, + servers: $servers, + parameters: $parameters, + get: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::GET, + $pathItem->get + ), + put: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::PUT, + $pathItem->put + ), + post: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::POST, + $pathItem->post + ), + delete: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::DELETE, + $pathItem->delete + ), + options: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::OPTIONS, + $pathItem->options + ), + head: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::HEAD, + $pathItem->head + ), + patch: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::PATCH, + $pathItem->patch + ), + trace: self::validateOperation( + $identifier, + $servers, + $parameters, + Method::TRACE, + $pathItem->trace + ), + ); } /** @@ -92,7 +170,7 @@ public function getOperations(): array * @param Partial\Server[] $pathServers * @return array> */ - private function validateServers( + private static function validateServers( Identifier $identifier, array $openAPIServers, array $pathServers @@ -101,30 +179,36 @@ private function validateServers( return $openAPIServers; } - $result = array_values(array_map( + return array_values(array_map( fn($s) => new Server($identifier, $s), $pathServers )); + } - $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 Partial\Parameter[] $parameters * @return array */ - private function validateParameters(array $parameters): array - { + private static function validateParameters( + Identifier $identifier, + array $parameters + ): array { $result = array_values(array_map( - fn($p) => new Parameter($this->getIdentifier(), $p), + fn($p) => new Parameter($identifier, $p), $parameters )); @@ -132,29 +216,46 @@ private function validateParameters(array $parameters): array foreach (array_slice($result, $index + 1) as $otherParameter) { if ($parameter->isIdentical($otherParameter)) { throw InvalidOpenAPI::duplicateParameters( - $this->getIdentifier(), + $identifier, $parameter->getIdentifier(), $otherParameter->getIdentifier(), ); } + } + } + + return $result; + } - if ($parameter->isSimilar($otherParameter)) { + /** + * @param array $parameters + */ + private function reviewParameters(array $parameters): void + { + foreach ($parameters as $index => $parameter) { + foreach (array_slice($parameters, $index + 1) as $other) { + if ($parameter->isSimilar($other)) { $this->addWarning( <<name - $otherParameter->name + $other->name TEXT, Warning::SIMILAR_NAMES ); } } } - - return $result; } - private function validateOperation( + /** + * @param Server[] $servers + * @param Parameter[] $parameters + */ + private static function validateOperation( + Identifier $identifier, + array $servers, + array $parameters, Method $method, ?Partial\Operation $operation ): ?Operation { @@ -162,12 +263,12 @@ private function validateOperation( return null; } - return new Operation( - $this->getIdentifier(), - $this->servers, - $this->parameters, + return Operation::fromPartial( + $identifier, + $servers, + $parameters, $method, - $operation + $operation, ); } @@ -175,7 +276,7 @@ private function validateOperation( * Operation "method" keys mapped to Operation values * @param array $operations */ - private function checkOperations(array $operations): void + private function reviewOperations(array $operations): void { if (empty($operations)) { $this->addWarning('No Operations on Path', Warning::EMPTY_PATH); diff --git a/tests/ValueObject/Valid/IdentifierTest.php b/tests/ValueObject/Valid/IdentifierTest.php index 11da73e..a5f6b60 100644 --- a/tests/ValueObject/Valid/IdentifierTest.php +++ b/tests/ValueObject/Valid/IdentifierTest.php @@ -60,7 +60,65 @@ public function itCanBeCastToString(string $expected, array $chain): void self::assertSame($expected, (string)$sut); } - public static function provideChainsToSearchThrough(): Generator + /** @param string[] $chain */ + #[Test, DataProvider('provideChainsToSearchThroughForwards')] + public function itGetsStringsFromStartOfChain( + ?string $expected, + int $index, + array $chain, + ): void { + $sut = new Identifier(...$chain); + + self::assertSame($expected, $sut->fromStart($index)); + } + + /** @param string[] $chain */ + #[Test, DataProvider('provideChainsToSearchThroughBackwards')] + public function itGetsStringsFromEndOfChain( + ?string $expected, + int $index, + array $chain, + ): void { + $sut = new Identifier(...$chain); + + self::assertSame($expected, $sut->fromEnd($index)); + } + + public static function provideChainsToSearchThroughForwards(): Generator + { + yield 'single field, first field from start' => [ + 'field1', + 0, + ['field1'] + ]; + + yield 'single field, second field from start which should be null' => [ + null, + 1, + ['field1'] + ]; + + yield 'three fields, pick the first from start' => [ + 'field1', + 0, + ['field1', 'field2', 'field3'] + ]; + + yield 'three fields, pick the second from start' => [ + 'field2', + 1, + ['field1', 'field2', 'field3'] + ]; + + yield 'three fields, pick the third from start' => [ + 'field3', + 2, + ['field1', 'field2', 'field3'] + ]; + } + + + public static function provideChainsToSearchThroughBackwards(): Generator { yield 'single field, first field from end' => [ 'field1', @@ -92,16 +150,4 @@ public static function provideChainsToSearchThrough(): Generator ['field1', 'field2', 'field3'] ]; } - - /** @param string[] $chain */ - #[Test, DataProvider('provideChainsToSearchThrough')] - public function itGetsStringsFromEndOfChain( - ?string $expected, - int $index, - array $chain, - ): void { - $sut = new Identifier(...$chain); - - self::assertSame($expected, $sut->fromEnd($index)); - } } diff --git a/tests/ValueObject/Valid/V30/OpenAPITest.php b/tests/ValueObject/Valid/V30/OpenAPITest.php index a3d61eb..3ed0e22 100644 --- a/tests/ValueObject/Valid/V30/OpenAPITest.php +++ b/tests/ValueObject/Valid/V30/OpenAPITest.php @@ -46,7 +46,7 @@ public function itCannotBeInvalid( ): void { self::expectExceptionObject($expected); - new OpenAPI($partialOpenAPI); + OpenAPI::fromPartial($partialOpenAPI); } #[Test] @@ -54,7 +54,7 @@ public function itCannotBeInvalid( public function itWarnsAgainstEmptyPaths(): void { $expected = [new Warning('No Paths in OpenAPI', Warning::EMPTY_PATHS)]; - $sut = new OpenAPI(PartialHelper::createOpenAPI(paths: [])); + $sut = OpenAPI::fromPartial(PartialHelper::createOpenAPI(paths: [])); $actual = $sut->getWarnings()->findByWarningCode(Warning::EMPTY_PATHS); @@ -69,7 +69,7 @@ public function itWarnsAgainstDuplicateServers( array $expected, Partial\OpenAPI $openAPI, ): void { - $sut = new OpenAPI($openAPI); + $sut = OpenAPI::fromPartial($openAPI); self::assertEquals( $expected, @@ -84,7 +84,7 @@ public function itHasADefaultServer(): void { $title = 'My API'; $version = '1.2.1'; - $sut = new OpenAPI(PartialHelper::createOpenAPI( + $sut = OpenAPI::fromPartial(PartialHelper::createOpenAPI( title: $title, version: $version, servers: [], @@ -98,6 +98,25 @@ public function itHasADefaultServer(): void self::assertEquals($expected, $sut->servers); } + #[Test] + #[DataProvider('provideOpenAPIWithoutServers')] + public function itCanCreateANewInstanceWithoutServers( + array $servers, + array $paths, + ): void { + $apiWith = fn($s) => OpenAPI::fromPartial( + PartialHelper::createOpenAPI( + servers: $s, + paths: $paths, + ), + ); + + self::assertEquals( + $apiWith([new Partial\Server('/')]), + $apiWith($servers)->withoutServers(), + ); + } + /** * @return Generator [ + [new Partial\Server('/')], + [], + ]; + + yield 'static server' => [ + [new Partial\Server('hello-world.net/')], + [], + ]; + + yield 'multiple servers' => [ + [ + new Partial\Server('hello-world.net/'), + new Partial\Server('howdy-planet.io/'), + ], + [], + ]; + + yield 'dynamic server' => [ + [new Partial\Server('hello-{world}.net/', [ + new Partial\ServerVariable('world', 'world'), + ])], + [], + ]; + + yield 'path item' => [ + [new Partial\Server('hello-parameter.io/')], + [PartialHelper::createPathItem()] + ]; + } } diff --git a/tests/ValueObject/Valid/V30/OperationTest.php b/tests/ValueObject/Valid/V30/OperationTest.php index 7461774..6ed604e 100644 --- a/tests/ValueObject/Valid/V30/OperationTest.php +++ b/tests/ValueObject/Valid/V30/OperationTest.php @@ -53,7 +53,7 @@ public function itRequiresAnOperationId(): void self::expectException(CannotSupport::class); self::expectExceptionCode(CannotSupport::MISSING_OPERATION_ID); - new Operation(new Identifier(''), [], [], Method::GET, $partialOperation); + Operation::fromPartial(new Identifier(''), [], [], Method::GET, $partialOperation); } /** @@ -68,7 +68,7 @@ public function itOverridesPathParametersOfTheSameName( Method $method, Partial\Operation $partialOperation ): void { - $sut = new Operation($parentIdentifier, [], $pathParameters, $method, $partialOperation); + $sut = Operation::fromPartial($parentIdentifier, [], $pathParameters, $method, $partialOperation); self::assertEquals($expected, $sut->parameters); } @@ -104,13 +104,13 @@ public function itCannotSupportConflictingParameters(): void style: 'form', explode: true, schema: new Partial\Schema(type: 'object') - ) + ), ] ); self::expectExceptionObject(CannotSupport::conflictingParameterStyles(...$parameterIdentifiers)); - new Operation($parentIdentifier, [], [], $method, $partialOperation); + Operation::fromPartial($parentIdentifier, [], [], $method, $partialOperation); } #[Test, DataProvider('provideOperationsToValidate')] @@ -122,7 +122,7 @@ public function itValidatesOperations( ): void { self::expectExceptionObject($expected); - new Operation($parentIdentifier, [], [], $method, $partialOperation); + Operation::fromPartial($parentIdentifier, [], [], $method, $partialOperation); } /** @@ -140,7 +140,7 @@ public function itOverridesPathLevelServers( array $pathServers, array $operationServers, ): void { - $sut = new Operation( + $sut = Operation::fromPartial( $parentIdentifier, $pathServers, [], @@ -163,7 +163,7 @@ public function itWarnsAgainstDuplicateServers( array $expected, Partial\Operation $operation, ): void { - $sut = new Operation( + $sut = Operation::fromPartial( new Identifier('test'), [], [], @@ -176,26 +176,6 @@ public function itWarnsAgainstDuplicateServers( ->findByWarningCode(Warning::IDENTICAL_SERVER_URLS)); } - - #[Test] - #[TestDox('The PathItem object will already warn about its own duplicates')] - public function itWillNotWarnForDuplicatePathLevelServers(): void - { - $identifier = new Identifier('test'); - $server = new Server($identifier, PartialHelper::createServer()); - $sut = new Operation( - $identifier, - [$server, $server], - [], - Method::GET, - PartialHelper::createOperation(), - ); - - self::assertEmpty($sut - ->getWarnings() - ->findByWarningCode(Warning::IDENTICAL_SERVER_URLS)); - } - /** * @param Parameter[] $pathParameters * @param Partial\Parameter[] $operationParameters @@ -206,7 +186,7 @@ public function itWarnsAgainstSimilarParameters( array $pathParameters, array $operationParameters, ): void { - $sut = new Operation( + $sut = Operation::fromPartial( new Identifier('test'), [], $pathParameters, @@ -226,7 +206,7 @@ public function itWillNotWarnAgainstSimilarPathParameters(): void PartialHelper::createParameter() ); - $sut = new Operation( + $sut = Operation::fromPartial( new Identifier('test'), [], [$parameter, $parameter], @@ -237,6 +217,29 @@ public function itWillNotWarnAgainstSimilarPathParameters(): void self::assertEmpty($sut->getWarnings()->findByWarningCode(Warning::SIMILAR_NAMES)); } + #[Test] + #[DataProvider('provideOperationsWithoutServers')] + public function itCanCreateANewInstanceWithoutServers( + array $servers, + array $parameters, + ): void { + $operationWith = fn($s) => Operation::fromPartial( + new Identifier('test'), + [], + [], + Method::GET, + PartialHelper::createOperation( + servers: $s, + parameters: $parameters, + ), + ); + + self::assertEquals( + $operationWith([new Partial\Server('/')]), + $operationWith($servers)->withoutServers(), + ); + } + public static function provideParameters(): Generator { $parentIdentifier = new Identifier('/path'); @@ -252,7 +255,7 @@ public static function provideParameters(): Generator PartialHelper::createOperation( operationId: $operationId, parameters: $operationParameters - ) + ), ]; $unique1 = PartialHelper::createParameter(name: 'unique-name-1'); @@ -312,7 +315,7 @@ public static function provideOperationsToValidate(): Generator PartialHelper::createOperation(...array_merge( ['operationId' => $operationId], $data - )) + )), ]; yield 'duplicate parameters' => $case( @@ -325,7 +328,7 @@ public static function provideOperationsToValidate(): Generator 'parameters' => array_pad([], 2, PartialHelper::createParameter( name: 'duplicate', in: 'path', - )) + )), ] ); } @@ -338,7 +341,7 @@ public static function provideServers(): Generator $identifier = $parentIdentifier->append($operationId, $method->value); $pathServers = [ - new Server($parentIdentifier, PartialHelper::createServer(url: '/')) + new Server($parentIdentifier, PartialHelper::createServer(url: '/')), ]; $case = fn($operationServers) => [ @@ -348,7 +351,7 @@ public static function provideServers(): Generator $operationId, $method, $pathServers, - $operationServers + $operationServers, ]; yield 'no Path Item Servers' => $case([]); @@ -356,7 +359,7 @@ public static function provideServers(): Generator yield 'three Path Item Servers' => $case([ PartialHelper::createServer(url: 'https://server-one.io'), PartialHelper::createServer(url: 'https://server-two.co.uk'), - PartialHelper::createServer(url: 'https://server-three.net') + PartialHelper::createServer(url: 'https://server-three.net'), ]); } @@ -411,4 +414,37 @@ public static function provideSimilarParameters(): Generator yield 'similar operation param names' => $case([], ['param', 'Param']); yield 'similar param names' => $case(['param'], ['Param']); } + + public static function provideOperationsWithoutServers(): Generator + { + yield 'default server' => [ + [new Partial\Server('/')], + [], + ]; + + yield 'static server' => [ + [new Partial\Server('hello-world.net/')], + [], + ]; + + yield 'multiple servers' => [ + [ + new Partial\Server('hello-world.net/'), + new Partial\Server('howdy-planet.io/'), + ], + [], + ]; + + yield 'dynamic server' => [ + [new Partial\Server('hello-{world}.net/', [ + new Partial\ServerVariable('world', 'world'), + ])], + [], + ]; + + yield 'parameter' => [ + [new Partial\Server('hello-parameter.io/')], + [PartialHelper::createParameter()] + ]; + } } diff --git a/tests/ValueObject/Valid/V30/PathItemTest.php b/tests/ValueObject/Valid/V30/PathItemTest.php index 06d9884..085a1ff 100644 --- a/tests/ValueObject/Valid/V30/PathItemTest.php +++ b/tests/ValueObject/Valid/V30/PathItemTest.php @@ -62,7 +62,7 @@ public function itValidatesParameters( $partialExpected )); - $sut = new PathItem($identifier, [], $partialPathItem); + $sut = PathItem::fromPartial($identifier, [], $partialPathItem); self::assertEquals($expected, $sut->parameters); } @@ -86,14 +86,14 @@ public function itInvalidatesDuplicateParameters(): void $paramIdentifier, )); - new PathItem($identifier, [], $pathItem); + PathItem::fromPartial($identifier, [], $pathItem); } #[Test] #[TestDox('Identical names in different locations may serve a purpose')] public function itDoesNotWarnAgainstIdenticalNames(): void { - $sut = new PathItem( + $sut = PathItem::fromPartial( new Identifier('test-path-item'), [], PartialHelper::createPathItem(parameters: [ @@ -111,7 +111,7 @@ public function itDoesNotWarnAgainstIdenticalNames(): void #[TestDox('It warns that similar names, though valid, may be confusing')] public function itWarnsAgainstSimilarNames(): void { - $sut = new PathItem( + $sut = PathItem::fromPartial( new Identifier('test-path-item'), [], PartialHelper::createPathItem(parameters: [ @@ -133,31 +133,13 @@ public function itWarnsAgainstDuplicateServers( array $expected, Partial\PathItem $pathItem, ): void { - $sut = new PathItem(new Identifier('test'), [], $pathItem); + $sut = PathItem::fromPartial(new Identifier('test'), [], $pathItem); self::assertEquals($expected, $sut ->getWarnings() ->findByWarningCode(Warning::IDENTICAL_SERVER_URLS)); } - - #[Test] - #[TestDox('The OpenAPI object will already warn about its own duplicates')] - public function itWillNotWarnForDuplicateRootLevelServers(): void - { - $identifier = new Identifier('test'); - $server = new Server($identifier, PartialHelper::createServer()); - $sut = new PathItem( - $identifier, - [$server, $server], - PartialHelper::createPathItem() - ); - - self::assertEmpty($sut - ->getWarnings() - ->findByWarningCode(Warning::IDENTICAL_SERVER_URLS)); - } - #[Test, DataProvider('provideRedundantMethods')] #[TestDox('it warns that options, head and trace are redundant methods for an OpenAPI')] public function itWarnsAgainstRedundantMethods(string $method): void @@ -166,7 +148,7 @@ public function itWarnsAgainstRedundantMethods(string $method): void $partialPathItem = PartialHelper::createPathItem(...$operations); - $sut = new PathItem(new Identifier('test'), [], $partialPathItem); + $sut = PathItem::fromPartial(new Identifier('test'), [], $partialPathItem); self::assertEquals( new Warning( @@ -183,7 +165,7 @@ public function itWarnsAgainstHavingNoOperations(): void { $partialPathItem = PartialHelper::createPathItem(); - $sut = new PathItem(new Identifier('test'), [], $partialPathItem); + $sut = PathItem::fromPartial(new Identifier('test'), [], $partialPathItem); self::assertEquals( new Warning('No Operations on Path', Warning::EMPTY_PATH), @@ -202,12 +184,10 @@ public function itCanGetAllOperations( Identifier $identifier, array $operations, ): void { - $sut = new PathItem( + $sut = PathItem::fromPartial( $identifier, [], - PartialHelper::createPathItem( - ...$operations - ) + PartialHelper::createPathItem(...$operations) ); self::assertEquals($expected, $sut->getOperations()); @@ -226,7 +206,7 @@ public function itOverridesOpenAPILevelServers( array $openapiServers, array $pathItemServers, ): void { - $sut = new PathItem( + $sut = PathItem::fromPartial( $identifier, $openapiServers, PartialHelper::createPathItem( @@ -237,6 +217,27 @@ public function itOverridesOpenAPILevelServers( self::assertEquals(array_values($expected), $sut->servers); } + #[Test] + #[DataProvider('newPathItemsWithoutServers')] + public function itCanCreateANewInstanceWithoutServers( + array $servers, + array $parameters, + ): void { + $pathWith = fn($s) => PathItem::fromPartial( + new Identifier('test'), + [], + PartialHelper::createPathItem( + servers: $s, + parameters: $parameters, + ), + ); + + self::assertEquals( + $pathWith([new Partial\Server('/')]), + $pathWith($servers)->withoutServers(), + ); + } + public static function providePartialPathItems(): Generator { $p1 = PartialHelper::createParameter(name: 'p1'); @@ -335,7 +336,7 @@ public static function provideOperationsToGet(): Generator operationId: "$method-id" ); - $validOperation = fn($method) => new Operation( + $validOperation = fn($method) => Operation::fromPartial( $identifier, [], [], @@ -407,8 +408,36 @@ public static function provideServers(): Generator ]); } - public static function provideServersToWarnAgainst(): Generator + public static function newPathItemsWithoutServers(): Generator { + yield 'default server' => [ + [new Partial\Server('/')], + [], + ]; + + yield 'static server' => [ + [new Partial\Server('hello-world.net/')], + [], + ]; + yield 'multiple servers' => [ + [ + new Partial\Server('hello-world.net/'), + new Partial\Server('howdy-planet.io/'), + ], + [], + ]; + + yield 'dynamic server' => [ + [new Partial\Server('hello-{world}.net/', [ + new Partial\ServerVariable('world', 'world'), + ])], + [], + ]; + + yield 'parameter' => [ + [new Partial\Server('hello-parameter.io/')], + [PartialHelper::createParameter()] + ]; } } diff --git a/tests/fixtures/Helper/OpenAPIProvider.php b/tests/fixtures/Helper/OpenAPIProvider.php index 13f189b..5d91592 100644 --- a/tests/fixtures/Helper/OpenAPIProvider.php +++ b/tests/fixtures/Helper/OpenAPIProvider.php @@ -45,7 +45,7 @@ public static function minimalV30CebeObject(): Cebe\OpenApi */ public static function minimalV30MembraneObject(): OpenAPI { - return new OpenAPI(PartialHelper::createOpenAPI( + return OpenAPI::fromPartial(PartialHelper::createOpenAPI( openapi: '3.0.0', title: 'My Minimal OpenAPI', version: '1.0.0', @@ -186,7 +186,7 @@ public static function detailedV30CebeObject(): Cebe\OpenApi */ public static function detailedV30MembraneObject(): OpenAPI { - return new OpenAPI(PartialHelper::createOpenAPI( + return OpenAPI::fromPartial(PartialHelper::createOpenAPI( openapi: '3.0.0', title: 'My Detailed OpenAPI', version: '1.0.1',