From 2274f103f9f210664f546f504e4559d772a81fee Mon Sep 17 00:00:00 2001 From: Asad Ali Date: Mon, 8 Jul 2024 16:50:08 +0500 Subject: [PATCH] feat: add support for nullable response types (#61) --- src/Response/Context.php | 9 ++ src/Response/ResponseHandler.php | 31 ++++++- src/Response/Types/ResponseMultiType.php | 2 +- src/Response/Types/ResponseType.php | 2 +- tests/ApiCallTest.php | 112 ++++++++++++++++++++++- tests/Mocking/Response/MockResponse.php | 2 +- 6 files changed, 150 insertions(+), 8 deletions(-) diff --git a/src/Response/Context.php b/src/Response/Context.php index 2f6e1a3..00be3b1 100644 --- a/src/Response/Context.php +++ b/src/Response/Context.php @@ -65,6 +65,15 @@ public function isFailure(): bool return $statusCode !== min(max($statusCode, 200), 208); // [200,208] = HTTP OK } + /** + * Is response body missing. + */ + public function isBodyMissing(): bool + { + $rawBody = $this->response->getRawBody(); + return trim($rawBody) === ''; + } + /** * Returns JsonHelper object. */ diff --git a/src/Response/ResponseHandler.php b/src/Response/ResponseHandler.php index ae5638c..e51a199 100644 --- a/src/Response/ResponseHandler.php +++ b/src/Response/ResponseHandler.php @@ -19,6 +19,7 @@ class ResponseHandler private $responseMultiType; private $responseError; private $useApiResponse = false; + private $nullableType = false; public function __construct() { @@ -56,6 +57,15 @@ public function nullOn404(): self return $this; } + /** + * Sets the return type as nullable. + */ + public function nullableType(): self + { + $this->nullableType = true; + return $this; + } + /** * Sets the deserializer method to the one provided, for deserializableType. */ @@ -82,8 +92,8 @@ public function type(string $responseClass, int $dimensions = 0): self /** * Sets response type to the one provided and format to XML. * - * @param string $responseClass Response type class - * @param string $rootName + * @param string $responseClass Type of Response class + * @param string $rootName Name of the root in xml response */ public function typeXml(string $responseClass, string $rootName): self { @@ -97,6 +107,9 @@ public function typeXml(string $responseClass, string $rootName): self /** * Sets response type to the one provided and format to XML. + * + * @param string $responseClass Type of Response class + * @param string $rootName Name of the root for map in xml response */ public function typeXmlMap(string $responseClass, string $rootName): self { @@ -110,6 +123,10 @@ public function typeXmlMap(string $responseClass, string $rootName): self /** * Sets response type to the one provided and format to XML. + * + * @param string $responseClass Type of Response class + * @param string $rootName Name of the root for array in xml response + * @param string $itemName Name of each item in array */ public function typeXmlArray(string $responseClass, string $rootName, string $itemName): self { @@ -126,7 +143,6 @@ public function typeXmlArray(string $responseClass, string $rootName, string $it * @param string[] $typeGroupDeserializers Methods required for deserialization of specific types in * in the provided typeGroup, should be an array in the format: * ['path/to/method returnType', ...]. Default: [] - * @return $this */ public function typeGroup(string $typeGroup, array $typeGroupDeserializers = []): self { @@ -155,10 +171,19 @@ public function getResult(Context $context) if ($context->isFailure()) { return $this->responseError->getResult($context); } + if ($this->nullableType && $context->isBodyMissing()) { + return $this->getResponse($context, null); + } $result = $this->deserializableType->getFrom($context); $result = $result ?? $this->responseType->getFrom($context); $result = $result ?? $this->responseMultiType->getFrom($context); $result = $result ?? $context->getResponseBody(); + + return $this->getResponse($context, $result); + } + + private function getResponse(Context $context, $result) + { if ($this->useApiResponse) { return $context->toApiResponse($result); } diff --git a/src/Response/Types/ResponseMultiType.php b/src/Response/Types/ResponseMultiType.php index a00e4ae..a904043 100644 --- a/src/Response/Types/ResponseMultiType.php +++ b/src/Response/Types/ResponseMultiType.php @@ -39,7 +39,7 @@ public function setDeserializers(array $deserializers): void */ public function getFrom(Context $context) { - if (is_null($this->typeGroup)) { + if (is_null($this->typeGroup) || $context->isBodyMissing()) { return null; } $responseBody = $context->getResponse()->getBody(); diff --git a/src/Response/Types/ResponseType.php b/src/Response/Types/ResponseType.php index 6ae6c97..05d2e66 100644 --- a/src/Response/Types/ResponseType.php +++ b/src/Response/Types/ResponseType.php @@ -53,7 +53,7 @@ public function setDimensions(int $dimensions): void */ public function getFrom(Context $context) { - if (is_null($this->responseClass)) { + if (is_null($this->responseClass) || $context->isBodyMissing()) { return null; } if (isset($this->xmlDeserializer)) { diff --git a/tests/ApiCallTest.php b/tests/ApiCallTest.php index a25b3e2..f70e39b 100644 --- a/tests/ApiCallTest.php +++ b/tests/ApiCallTest.php @@ -35,6 +35,8 @@ class ApiCallTest extends TestCase { + private const DUMMY_BODY = ['res' => 'This is raw body']; + /** * @param string $query Just the query path of the url * @return array @@ -864,10 +866,11 @@ public function testApiResponseWith400() { $response = new MockResponse(); $response->setStatusCode(400); + $response->setBody(self::DUMMY_BODY); $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); $result = MockHelper::responseHandler()->type(MockClass::class)->returnApiResponse()->getResult($context); $this->assertInstanceOf(MockApiResponse::class, $result); - $this->assertEquals(['res' => 'This is raw body'], $result->getResult()); + $this->assertEquals(self::DUMMY_BODY, $result->getResult()); $this->assertTrue($result->isError()); } @@ -875,10 +878,11 @@ public function testApiResponseWith100() { $response = new MockResponse(); $response->setStatusCode(100); + $response->setBody(self::DUMMY_BODY); $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); $result = MockHelper::responseHandler()->type(MockClass::class)->returnApiResponse()->getResult($context); $this->assertInstanceOf(MockApiResponse::class, $result); - $this->assertEquals(['res' => 'This is raw body'], $result->getResult()); + $this->assertEquals(self::DUMMY_BODY, $result->getResult()); $this->assertTrue($result->isError()); } @@ -921,6 +925,110 @@ public function testNullOn404WithStatus400() MockHelper::responseHandler()->nullOn404()->getResult($context); } + public function testNullableTypeWithMissingBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(''); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->nullableType()->getResult($context); + $this->assertNull($result); + } + + public function testNullableTypeWithMissingBodyAndApiResponse() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(''); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->nullableType()->returnApiResponse()->getResult($context); + $this->assertInstanceOf(MockApiResponse::class, $result); + $this->assertNull($result->getResult()); + $this->assertFalse($result->isError()); + } + + public function testNullableTypeWithNullBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(null); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->nullableType()->getResult($context); + $this->assertNull($result); + } + + public function testNullableTypeWithWhiteSpacedBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(' '); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->nullableType()->getResult($context); + $this->assertNull($result); + } + + public function testNullableTypeWithBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(214); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->nullableType()->getResult($context); + $this->assertEquals(214, $result); + } + + public function testNonNullableTypeWithMissingBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(''); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->getResult($context); + $this->assertEquals('', $result); + } + + public function testNonNullableTypeWithWhiteSpacedBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(' '); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->getResult($context); + $this->assertEquals(' ', $result); + } + + public function testNonNullableTypeWithMissingBodyAndApiResponse() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(''); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->returnApiResponse()->getResult($context); + $this->assertInstanceOf(MockApiResponse::class, $result); + $this->assertEquals('', $result->getResult()); + $this->assertFalse($result->isError()); + } + + public function testNonNullableTypeWithNullBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(null); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->getResult($context); + $this->assertNull($result); + } + + public function testNonNullableTypeWithBody() + { + $response = new MockResponse(); + $response->setStatusCode(200); + $response->setBody(214); + $context = new Context(MockHelper::getClient()->getGlobalRequest(), $response, MockHelper::getClient()); + $result = MockHelper::responseHandler()->getResult($context); + $this->assertEquals(214, $result); + } + public function testGlobalMockException() { $this->expectException(MockException::class); diff --git a/tests/Mocking/Response/MockResponse.php b/tests/Mocking/Response/MockResponse.php index 172e4ab..f63f0f1 100644 --- a/tests/Mocking/Response/MockResponse.php +++ b/tests/Mocking/Response/MockResponse.php @@ -71,7 +71,7 @@ public function setBody($body): void public function getBody() { - return $this->body ?? ["res" => "This is raw body"]; + return $this->body; } public function convert(ConverterInterface $converter)