From 02423dfb547ddd2d176438b67358b436f2a81df5 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Fri, 14 May 2021 20:06:58 +0300 Subject: [PATCH] callback serializer decorator, first version of response builder, WIP: runtime serializer support --- composer.json | 7 +- src/Exception/JsonException.php | 22 ++ src/Factory/AbstractSerializerFactory.php | 11 + src/Factory/JsonSerializerFactory.php | 20 ++ src/Factory/XmlSerializerFactory.php | 20 ++ src/PockBuilder.php | 39 +++- src/PockResponseBuilder.php | 209 ++++++++++++++++++ .../CallbackSerializerDecorator.php | 51 +++++ .../CallbackSerializerDecoratorTest.php | 29 +++ .../Factory/AbstractSerializerFactoryTest.php | 50 +++++ 10 files changed, 449 insertions(+), 9 deletions(-) create mode 100644 src/Exception/JsonException.php create mode 100644 src/PockResponseBuilder.php create mode 100644 src/Serializer/CallbackSerializerDecorator.php create mode 100644 tests/src/Decorator/CallbackSerializerDecoratorTest.php create mode 100644 tests/src/Factory/AbstractSerializerFactoryTest.php diff --git a/composer.json b/composer.json index 09c9dcd..43fddd0 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,11 @@ }, "require": { "php": ">=7.2.0", + "ext-json": "*", "psr/http-client": "^1.0", "psr/http-message": "^1.0", - "php-http/httplug": "^1.0 || ^2.0" + "php-http/httplug": "^1.0 || ^2.0", + "nyholm/psr7": "^1.4" }, "require-dev": { "squizlabs/php_codesniffer": "^3.6", @@ -35,8 +37,7 @@ "jms/serializer": "^2 | ^3.12", "symfony/phpunit-bridge": "^5.2", "symfony/var-dumper": "^5.2", - "symfony/serializer": "^5.2", - "nyholm/psr7": "^1.4" + "symfony/serializer": "^5.2" }, "provide": { "psr/http-client-implementation": "1.0", diff --git a/src/Exception/JsonException.php b/src/Exception/JsonException.php new file mode 100644 index 0000000..ed249f4 --- /dev/null +++ b/src/Exception/JsonException.php @@ -0,0 +1,22 @@ +responseBuilder) { + $this->responseBuilder = new PockResponseBuilder($statusCode); + + return $this->responseBuilder; + } + + return $this->responseBuilder->withStatusCode($statusCode); + } + /** * Resets the builder. * @@ -128,7 +144,7 @@ class PockBuilder public function reset(): PockBuilder { $this->matcher = new MultipleMatcher(); - $this->response = null; + $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; $this->mocks = []; @@ -159,14 +175,25 @@ class PockBuilder private function closePrevious(): void { - if (null !== $this->response || null !== $this->throwable) { + if (null !== $this->responseBuilder || null !== $this->throwable) { if (0 === count($this->matcher)) { $this->matcher->addMatcher(new AnyRequestMatcher()); } - $this->mocks[] = new Mock($this->matcher, $this->response, $this->throwable, $this->maxHits); + $response = null; + + if (null !== $this->responseBuilder) { + $response = $this->responseBuilder->getResponse(); + } + + $this->mocks[] = new Mock( + $this->matcher, + $response, + $this->throwable, + $this->maxHits + ); $this->matcher = new MultipleMatcher(); - $this->response = null; + $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; } diff --git a/src/PockResponseBuilder.php b/src/PockResponseBuilder.php new file mode 100644 index 0000000..3034e23 --- /dev/null +++ b/src/PockResponseBuilder.php @@ -0,0 +1,209 @@ +factory = new Psr17Factory(); + $this->response = $this->factory->createResponse($statusCode); + } + + /** + * Reply with specified status code. + * + * @param int $statusCode + * + * @return \Pock\PockResponseBuilder + */ + public function withStatusCode(int $statusCode = 200): PockResponseBuilder + { + $this->response = $this->response->withStatus($statusCode); + + return $this; + } + + /** + * Reply with specified body. It can be: + * - PSR-7 StreamInterface - it will be used without any changes. + * - string - it will be used as stream contents. + * - resource - it's data will be used as stream contents. + * + * @param \Psr\Http\Message\StreamInterface|resource|string $stream + * + * @return $this + */ + public function withBody($stream): PockResponseBuilder + { + if (is_string($stream)) { + $stream = $this->factory->createStream($stream); + } + + if (is_resource($stream)) { + $stream = $this->factory->createStreamFromResource($stream); + } + + $this->response = $this->response->withBody($stream); + + return $this; + } + + /** + * @param mixed $data + * + * @return $this + * @throws \Pock\Exception\JsonException + */ + public function withJson($data): PockResponseBuilder + { + if (is_string($data) || is_numeric($data)) { + return $this->withBody((string) $data); + } + + if (is_array($data)) { + return $this->withBody(static::jsonEncode($data)); + } + + if (is_object($data)) { + if ($data instanceof JsonSerializable) { + return $this->withBody(static::jsonEncode($data)); + } + + return $this->withBody(static::jsonSerializer()->serialize($data)); + } + + throw new InvalidArgumentException('Cannot serialize data with type ' . gettype($data)); + } + + /** + * @param mixed $data + * + * @return $this + * @throws \Pock\Exception\JsonException + */ + public function withXml($data): PockResponseBuilder + { + if (is_string($data)) { + return $this->withBody($data); + } + + if (is_array($data) || is_object($data)) { + return $this->withBody(static::xmlSerializer()->serialize($data)); + } + + throw new InvalidArgumentException('Cannot serialize data with type ' . gettype($data)); + } + + /** + * @return \Psr\Http\Message\ResponseInterface + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } + + /** + * Encode JSON, throw an exception on error. + * + * @param mixed $data + * + * @return string + * @throws \Pock\Exception\JsonException + */ + private static function jsonEncode($data): string + { + $data = json_encode($data); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new JsonException(json_last_error_msg(), json_last_error()); + } + + return (string) $data; + } + + /** + * @return \Pock\Serializer\SerializerInterface + * @throws \Pock\Exception\JsonException + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private static function jsonSerializer(): SerializerInterface + { + if (null !== static::$jsonSerializer) { + return static::$jsonSerializer; + } + + $serializer = JsonSerializerFactory::create(); + + if (null === $serializer) { + throw new JsonException('No JSON serializer available'); + } + + static::$jsonSerializer = $serializer; + + return $serializer; + } + + /** + * @return \Pock\Serializer\SerializerInterface + * @throws \Pock\Exception\JsonException + * + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private static function xmlSerializer(): SerializerInterface + { + if (null !== static::$xmlSerializer) { + return static::$xmlSerializer; + } + + $serializer = XmlSerializerFactory::create(); + + if (null === $serializer) { + throw new JsonException('No XML serializer available'); + } + + static::$xmlSerializer = $serializer; + + return $serializer; + } +} diff --git a/src/Serializer/CallbackSerializerDecorator.php b/src/Serializer/CallbackSerializerDecorator.php new file mode 100644 index 0000000..82e3e4d --- /dev/null +++ b/src/Serializer/CallbackSerializerDecorator.php @@ -0,0 +1,51 @@ +callback = $callback; + } + + /** + * @inheritDoc + */ + public function serialize($data): string + { + $result = call_user_func($this->callback, $data); + + if (is_string($result)) { + return $result; + } + + throw new RuntimeException(sprintf( + 'Invalid data from serialization callback: expected string, %s given', + gettype($result) + )); + } +} diff --git a/tests/src/Decorator/CallbackSerializerDecoratorTest.php b/tests/src/Decorator/CallbackSerializerDecoratorTest.php new file mode 100644 index 0000000..c3365ef --- /dev/null +++ b/tests/src/Decorator/CallbackSerializerDecoratorTest.php @@ -0,0 +1,29 @@ +serialize('{}')); + } +} diff --git a/tests/src/Factory/AbstractSerializerFactoryTest.php b/tests/src/Factory/AbstractSerializerFactoryTest.php new file mode 100644 index 0000000..ee29587 --- /dev/null +++ b/tests/src/Factory/AbstractSerializerFactoryTest.php @@ -0,0 +1,50 @@ +