From b61e8692a26a11280d76bb2cd3cf6672bf209e96 Mon Sep 17 00:00:00 2001 From: John Charman Date: Wed, 20 Nov 2024 11:13:25 +0000 Subject: [PATCH 1/3] Add RequestBody --- src/Factory/V30/FromCebe.php | 20 +++++++- src/ValueObject/Partial/Operation.php | 3 +- src/ValueObject/Partial/RequestBody.php | 18 +++++++ src/ValueObject/Valid/V30/Operation.php | 7 +++ src/ValueObject/Valid/V30/RequestBody.php | 62 +++++++++++++++++++++++ 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/ValueObject/Partial/RequestBody.php create mode 100644 src/ValueObject/Valid/V30/RequestBody.php diff --git a/src/Factory/V30/FromCebe.php b/src/Factory/V30/FromCebe.php index f474d9b..b0a115c 100644 --- a/src/Factory/V30/FromCebe.php +++ b/src/Factory/V30/FromCebe.php @@ -10,6 +10,7 @@ use Membrane\OpenAPIReader\ValueObject\Partial\Operation; use Membrane\OpenAPIReader\ValueObject\Partial\Parameter; use Membrane\OpenAPIReader\ValueObject\Partial\PathItem; +use Membrane\OpenAPIReader\ValueObject\Partial\RequestBody; use Membrane\OpenAPIReader\ValueObject\Partial\Schema; use Membrane\OpenAPIReader\ValueObject\Partial\Server; use Membrane\OpenAPIReader\ValueObject\Partial\ServerVariable; @@ -210,7 +211,24 @@ private static function createOperation( return new Operation( operationId: $operation->operationId, servers: self::createServers($operation->servers), - parameters: self::createParameters($operation->parameters) + parameters: self::createParameters($operation->parameters), + requestBody: self::createRequestBody($operation->requestBody), + ); + } + + private static function createRequestBody( + Cebe\Reference|Cebe\RequestBody|null $requestBody + ): ?RequestBody { + assert(! $requestBody instanceof Cebe\Reference); + + if (is_null($requestBody)) { + return null; + } + + return new RequestBody( + $requestBody->description ?? null, + self::createContent($requestBody->content ?? []), + $requestBody->required ?? false, ); } } diff --git a/src/ValueObject/Partial/Operation.php b/src/ValueObject/Partial/Operation.php index 370f9fa..faa142a 100644 --- a/src/ValueObject/Partial/Operation.php +++ b/src/ValueObject/Partial/Operation.php @@ -11,9 +11,10 @@ final class Operation * @param Parameter[] $parameters */ public function __construct( - public ?string $operationId = null, + public string|null $operationId = null, public array $servers = [], public array $parameters = [], + public RequestBody|null $requestBody = null, ) { } } diff --git a/src/ValueObject/Partial/RequestBody.php b/src/ValueObject/Partial/RequestBody.php new file mode 100644 index 0000000..b7ebe72 --- /dev/null +++ b/src/ValueObject/Partial/RequestBody.php @@ -0,0 +1,18 @@ +operationId, [new Server($this->getIdentifier(), new Partial\Server('/'))], $this->parameters, + $this->requestBody, ); } @@ -80,11 +82,16 @@ public static function fromPartial( $operation->parameters ); + $requestBody = isset($operation->requestBody) ? + new RequestBody($identifier, $operation->requestBody) : + null; + return new Operation( $identifier, $operationId, $servers, $parameters, + $requestBody, ); } diff --git a/src/ValueObject/Valid/V30/RequestBody.php b/src/ValueObject/Valid/V30/RequestBody.php new file mode 100644 index 0000000..a26a32d --- /dev/null +++ b/src/ValueObject/Valid/V30/RequestBody.php @@ -0,0 +1,62 @@ + + */ + public readonly array $content; + + public function __construct( + Identifier $parentIdentifier, + Partial\RequestBody $requestBody, + ) { + $identifier = $parentIdentifier->append('requestBody'); + parent::__construct($identifier); + + $this->description = $requestBody->description; + $this->required = $requestBody->required; + + $this->content = $this->validateContent( + $identifier, + $requestBody->content + ); + } + + /** + * @param array $content + * @return array + */ + public function validateContent( + Identifier $identifier, + array $content + ): array { + $result = []; + foreach ($content as $mediaType) { + if (!isset($mediaType->contentType)) { + throw InvalidOpenAPI::contentMissingMediaType($identifier); + } + + $result[$mediaType->contentType] = new MediaType( + $identifier->append($mediaType->contentType), + $mediaType, + ); + } + + return $result; + } +} From 987bebc79e30498d95d29c7b069e8fa7ee2c8d76 Mon Sep 17 00:00:00 2001 From: John Charman Date: Wed, 20 Nov 2024 11:37:31 +0000 Subject: [PATCH 2/3] Revert petstore.yaml to original Some parts had been removed, since they were unsupported This PR intends to support them so, back to the original. --- tests/fixtures/petstore.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/petstore.yaml b/tests/fixtures/petstore.yaml index 477945d..e6e74ac 100644 --- a/tests/fixtures/petstore.yaml +++ b/tests/fixtures/petstore.yaml @@ -45,6 +45,12 @@ paths: operationId: createPets tags: - pets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true responses: '201': description: Null response @@ -67,12 +73,6 @@ paths: description: The id of the pet to retrieve schema: type: string - - name: petId - in: query - required: true - description: The id of the pet to retrieve - schema: - type: string responses: '200': description: Expected response to a valid request From b6c8c7199abb7c416b80b5c80f0057a59ce99a81 Mon Sep 17 00:00:00 2001 From: John Charman Date: Wed, 20 Nov 2024 11:51:27 +0000 Subject: [PATCH 3/3] Test RequestBodies using petstore.yaml --- tests/MembraneReaderTest.php | 21 ++++- tests/fixtures/ProvidesPetstoreApi.php | 102 +++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/ProvidesPetstoreApi.php diff --git a/tests/MembraneReaderTest.php b/tests/MembraneReaderTest.php index 98eb6d7..e8ac3d2 100644 --- a/tests/MembraneReaderTest.php +++ b/tests/MembraneReaderTest.php @@ -6,6 +6,7 @@ use cebe\{openapi\exceptions as CebeException}; use Generator; +use Membrane\OpenAPIReader\Tests\Fixtures\ProvidesPetstoreApi; use Membrane\OpenAPIReader\{FileFormat, OpenAPIVersion}; use Membrane\OpenAPIReader\Exception\{CannotRead, CannotSupport, InvalidOpenAPI}; use Membrane\OpenAPIReader\Factory\V30\FromCebe; @@ -17,7 +18,7 @@ use Membrane\OpenAPIReader\ValueObject\Valid\Identifier; use Membrane\OpenAPIReader\ValueObject\Valid\V30\OpenAPI; use org\bovigo\vfs\vfsStream; -use PHPUnit\Framework\Attributes\{CoversClass, DataProvider, Test, TestDox, UsesClass}; +use PHPUnit\Framework\Attributes\{CoversClass, DataProvider, DataProviderExternal, Test, TestDox, UsesClass}; use PHPUnit\Framework\TestCase; use TypeError; @@ -347,6 +348,24 @@ public function itReadsFromString(OpenAPI $expected, string $openApi): void self::assertEquals($expected, $actual); } + #[Test] + #[DataProviderExternal(ProvidesPetstoreApi::class, 'provideOperations')] + public function itReadsRealExamples( + string $filepath, + string $path, + Method $method, + Valid\V30\Operation $expected + ): void { + $sut = new MembraneReader([OpenAPIVersion::Version_3_0]); + + $api = $sut->readFromAbsoluteFilePath($filepath); + + $actual = $api->paths[$path]?->getOperations()[$method->value]; + + self::assertEquals($expected, $actual); + } + + public static function provideInvalidFormatting(): Generator { yield 'Empty string to be interpreted as json' => ['', FileFormat::Json]; diff --git a/tests/fixtures/ProvidesPetstoreApi.php b/tests/fixtures/ProvidesPetstoreApi.php new file mode 100644 index 0000000..ac9a452 --- /dev/null +++ b/tests/fixtures/ProvidesPetstoreApi.php @@ -0,0 +1,102 @@ + + */ + public static function provideOperations(): Generator + { + yield 'listPets' => [self::API, '/pets', Method::GET, self::listPets()]; + + yield 'createPets' => [self::API, '/pets', Method::POST, self::createPets()]; + + yield 'showPetById' => [self::API, '/pets/{petId}', Method::GET, self::showPetById()]; + } + + private static function listPets(): V30\Operation + { + return V30\Operation::fromPartial( + parentIdentifier: new Identifier('Swagger Petstore(1.0.0)', '/pets'), + pathServers: [new V30\Server( + new Identifier('Swagger Petstore(1.0.0)'), + new Partial\Server(url: 'http://petstore.swagger.io/v1'), + )], + pathParameters: [], + method: Method::GET, + operation: new Partial\Operation('listPets', [], [ + new Partial\Parameter( + name: 'limit', + in: 'query', + schema: new Partial\Schema(type: 'integer', maximum: 100, format: 'int32'), + ), + ]) + ); + } + + private static function createPets(): V30\Operation + { + return V30\Operation::fromPartial( + parentIdentifier: new Identifier('Swagger Petstore(1.0.0)', '/pets'), + pathServers: [new V30\Server( + new Identifier('Swagger Petstore(1.0.0)'), + new Partial\Server(url: 'http://petstore.swagger.io/v1'), + )], + pathParameters: [], + method: Method::POST, + operation: new Partial\Operation('createPets', [], [], new Partial\RequestBody( + content: [new Partial\MediaType( + contentType: 'application/json', + schema: new Partial\Schema( + type: 'object', + required: ['id', 'name'], + properties: [ + 'id' => new Partial\Schema(type: 'integer', format: 'int64'), + 'name' => new Partial\Schema(type: 'string'), + 'tag' => new Partial\Schema(type: 'string'), + ], + ) + )], + required: true, + )), + ); + } + + private static function showPetById(): V30\Operation + { + return V30\Operation::fromPartial( + parentIdentifier: new Identifier('Swagger Petstore(1.0.0)', '/pets/{petId}'), + pathServers: [new V30\Server( + new Identifier('Swagger Petstore(1.0.0)'), + new Partial\Server(url: 'http://petstore.swagger.io/v1'), + )], + pathParameters: [], + method: Method::GET, + operation: new Partial\Operation('showPetById', [], [ + new Partial\Parameter( + name: 'petId', + in: 'path', + required: true, + schema: new Partial\Schema(type: 'string'), + ), + ]) + ); + } +}