summaryrefslogtreecommitdiffstats
path: root/vendor/gipfl/protocol-jsonrpc
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:23:16 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:23:16 +0000
commit3e97c51418e6d27e9a81906f347fcb7c78e27d4f (patch)
treeee596ce1bc9840661386f96f9b8d1f919a106317 /vendor/gipfl/protocol-jsonrpc
parentInitial commit. (diff)
downloadicingaweb2-module-incubator-3e97c51418e6d27e9a81906f347fcb7c78e27d4f.tar.xz
icingaweb2-module-incubator-3e97c51418e6d27e9a81906f347fcb7c78e27d4f.zip
Adding upstream version 0.20.0.upstream/0.20.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gipfl/protocol-jsonrpc')
-rw-r--r--vendor/gipfl/protocol-jsonrpc/LICENSE21
-rw-r--r--vendor/gipfl/protocol-jsonrpc/composer.json34
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Connection.php310
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Error.php199
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php28
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php23
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php217
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php241
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Notification.php98
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Packet.php226
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php11
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Request.php59
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/Response.php128
-rw-r--r--vendor/gipfl/protocol-jsonrpc/src/TestCase.php44
14 files changed, 1639 insertions, 0 deletions
diff --git a/vendor/gipfl/protocol-jsonrpc/LICENSE b/vendor/gipfl/protocol-jsonrpc/LICENSE
new file mode 100644
index 0000000..dd88e09
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2018 Thomas Gelf
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/gipfl/protocol-jsonrpc/composer.json b/vendor/gipfl/protocol-jsonrpc/composer.json
new file mode 100644
index 0000000..4a202eb
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "gipfl/protocol-jsonrpc",
+ "description": "JsonRPC Connection implementation",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Thomas Gelf",
+ "email": "thomas@gelf.net"
+ }
+ ],
+ "config": {
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "gipfl\\Protocol\\JsonRpc\\": "src"
+ }
+ },
+ "require": {
+ "php": ">=5.6.0",
+ "ext-json": "*",
+ "gipfl/json": ">=0.1",
+ "gipfl/openrpc": "^0.2.1",
+ "gipfl/protocol": ">=0.2",
+ "psr/log": ">=1.1",
+ "react/promise": ">=2.7",
+ "react/stream": ">=1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3 || ^7.5 || ^6.5 || ^5.7",
+ "squizlabs/php_codesniffer": "^3.6"
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Connection.php b/vendor/gipfl/protocol-jsonrpc/src/Connection.php
new file mode 100644
index 0000000..be4b33f
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Connection.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use Evenement\EventEmitterTrait;
+use Exception;
+use gipfl\Json\JsonEncodeException;
+use InvalidArgumentException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use React\Promise\Deferred;
+use React\Promise\Promise;
+use React\Stream\DuplexStreamInterface;
+use React\Stream\Util;
+use RuntimeException;
+use function call_user_func_array;
+use function is_object;
+use function mt_rand;
+use function preg_quote;
+use function preg_split;
+use function React\Promise\reject;
+use function sprintf;
+
+/**
+ * @deprecated Please use JsonRpcConection
+ */
+class Connection implements LoggerAwareInterface
+{
+ use EventEmitterTrait;
+ use LoggerAwareTrait;
+
+ /** @var DuplexStreamInterface */
+ protected $connection;
+
+ /** @var array */
+ protected $handlers = [];
+
+ /** @var Deferred[] */
+ protected $pending = [];
+
+ protected $nsSeparator = '.';
+
+ protected $nsRegex = '/\./';
+
+ protected $unknownErrorCount = 0;
+
+ public function handle(DuplexStreamInterface $connection)
+ {
+ $this->connection = $connection;
+ $this->connection->on('data', function ($data) {
+ try {
+ $this->handlePacket(Packet::decode($data));
+ } catch (Exception $error) {
+ echo $error->getMessage() . "\n";
+ $this->unknownErrorCount++;
+ if ($this->unknownErrorCount === 3) {
+ $this->close();
+ }
+ $response = new Response();
+ $response->setError(Error::forException($error));
+ $this->connection->write($response->toString());
+ }
+ });
+ $connection->on('close', function () {
+ $this->rejectAllPendingRequests('Connection closed');
+ });
+ // TODO: figure out whether and how to deal with the pipe event
+ Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']);
+ }
+
+ public function setNamespaceSeparator($separator)
+ {
+ $this->nsSeparator = $separator;
+ $this->nsRegex = '/' . preg_quote($separator, '/') . '/';
+
+ return $this;
+ }
+
+ /**
+ * @param Packet $packet
+ */
+ protected function handlePacket(Packet $packet)
+ {
+ if ($packet instanceof Response) {
+ $this->handleResponse($packet);
+ } elseif ($packet instanceof Request) {
+ $this->handleRequest($packet);
+ } elseif ($packet instanceof Notification) {
+ $this->handleNotification($packet);
+ } else {
+ // Will not happen as long as there is no bug in Packet
+ throw new RuntimeException('Packet was neither Request/Notification nor Response');
+ }
+ }
+
+ protected function handleResponse(Response $response)
+ {
+ $id = $response->getId();
+ if (isset($this->pending[$id])) {
+ $promise = $this->pending[$id];
+ unset($this->pending[$id]);
+ $promise->resolve($response);
+ } else {
+ $this->handleUnmatchedResponse($response);
+ }
+ }
+
+ protected function handleUnmatchedResponse(Response $response)
+ {
+ // Ignore. Log?
+ }
+
+ protected function handleRequest(Request $request)
+ {
+ $result = $this->handleNotification($request);
+ $this->sendResultForRequest($request, $result);
+ }
+
+ protected function sendResultForRequest(Request $request, $result)
+ {
+ if ($result instanceof Error) {
+ $response = Response::forRequest($request);
+ $response->setError($result);
+
+ $this->connection->write($response->toString());
+ } elseif ($result instanceof Promise) {
+ $result->then(function ($result) use ($request) {
+ $this->sendResultForRequest($request, $result);
+ })->otherwise(function ($error) use ($request) {
+ $response = Response::forRequest($request);
+ if ($error instanceof Exception) {
+ $response->setError(Error::forException($error));
+ } else {
+ $response->setError(new Error(Error::INTERNAL_ERROR, $error));
+ }
+ // TODO: Double-check, this used to loop
+ $this->connection->write($response->toString());
+ });
+ } else {
+ $response = Response::forRequest($request);
+ $response->setResult($result);
+ $this->connection->write($response->toString());
+ }
+ }
+
+ /**
+ * @param Notification $notification
+ * @return Error|mixed
+ */
+ protected function handleNotification(Notification $notification)
+ {
+ $method = $notification->getMethod();
+ if (\strpos($method, $this->nsSeparator) === false) {
+ $namespace = null;
+ } else {
+ list($namespace, $method) = preg_split($this->nsRegex, $method, 2);
+ }
+
+ try {
+ $response = $this->call($namespace, $method, $notification);
+
+ return $response;
+ } catch (Exception $exception) {
+ return Error::forException($exception);
+ }
+ }
+
+ /**
+ * @param Request $request
+ * @return \React\Promise\PromiseInterface
+ */
+ public function sendRequest(Request $request)
+ {
+ $id = $request->getId();
+ if ($id === null) {
+ $id = $this->getNextRequestId();
+ $request->setId($id);
+ }
+ if (isset($this->pending[$id])) {
+ throw new InvalidArgumentException(
+ "A request with id '$id' is already pending"
+ );
+ }
+ if (!$this->connection->isWritable()) {
+ return reject(new Exception('Cannot write to socket'));
+ }
+ try {
+ $this->connection->write($request->toString());
+ } catch (JsonEncodeException $e) {
+ return reject($e->getMessage());
+ }
+ $deferred = new Deferred();
+ $this->pending[$id] = $deferred;
+
+ return $deferred->promise()->then(function (Response $response) use ($deferred) {
+ if ($response->isError()) {
+ $deferred->reject(new RuntimeException($response->getError()->getMessage()));
+ } else {
+ $deferred->resolve($response->getResult());
+ }
+ }, function (Exception $e) use ($deferred) {
+ $deferred->reject($e);
+ });
+ }
+
+ public function request($method, $params = null)
+ {
+ return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params));
+ }
+
+ protected function getNextRequestId()
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $id = mt_rand(1, 1000000000);
+ if (!isset($this->pending[$id])) {
+ return $id;
+ }
+ }
+
+ throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts');
+ }
+
+ /**
+ * @param Request|mixed $request
+ */
+ public function forgetRequest($request)
+ {
+ if ($request instanceof Request) {
+ unset($this->pending[$request->getId()]);
+ } else {
+ unset($this->pending[$request]);
+ }
+ }
+
+ /**
+ * @param Notification $packet
+ */
+ public function sendNotification(Notification $packet)
+ {
+ $this->connection->write($packet->toString());
+ }
+
+ /**
+ * @param string $method
+ * @param null $params
+ */
+ public function notification($method, $params = null)
+ {
+ $notification = new Notification($method, $params);
+ $this->sendNotification($notification);
+ }
+
+ /**
+ * @param $namespace
+ * @param $handler
+ * @return Connection
+ */
+ public function setHandler($handler, $namespace = null)
+ {
+ $this->handlers[$namespace] = $handler;
+
+ return $this;
+ }
+
+ protected function call($namespace, $method, Notification $packet)
+ {
+ if (isset($this->handlers[$namespace])) {
+ $handler = $this->handlers[$namespace];
+ if ($handler instanceof PacketHandler) {
+ return $handler->handle($packet);
+ }
+
+ // Legacy handlers, deprecated:
+ $params = $packet->getParams();
+ if (is_object($params)) {
+ return $handler->$method($params);
+ }
+
+ return call_user_func_array([$handler, $method], $params);
+ }
+
+ $error = new Error(Error::METHOD_NOT_FOUND);
+ $error->setMessage(sprintf(
+ '%s: %s%s%s',
+ $error->getMessage(),
+ $namespace,
+ $this->nsSeparator,
+ $method
+ ));
+
+ return $error;
+ }
+
+ protected function rejectAllPendingRequests($message)
+ {
+ foreach ($this->pending as $pending) {
+ $pending->reject(new Exception());
+ }
+ $this->pending = [];
+ }
+
+ public function close()
+ {
+ if ($this->connection) {
+ $this->connection->close();
+ $this->handlers = [];
+ $this->connection = null;
+ }
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Error.php b/vendor/gipfl/protocol-jsonrpc/src/Error.php
new file mode 100644
index 0000000..dc1d639
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Error.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use Exception;
+use JsonSerializable;
+use TypeError;
+
+class Error implements JsonSerializable
+{
+ const PARSE_ERROR = -32700;
+
+ const INVALID_REQUEST = -32600;
+
+ const METHOD_NOT_FOUND = -32601;
+
+ const INVALID_PARAMS = 32602;
+
+ const INTERNAL_ERROR = 32603;
+
+ // Reserved for implementation-defined server-errors:
+ const MIN_CUSTOM_ERROR = -32000;
+
+ const MAX_CUSTOM_ERROR = -32099;
+
+ protected static $wellKnownErrorCodes = [
+ self::PARSE_ERROR,
+ self::INVALID_REQUEST,
+ self::METHOD_NOT_FOUND,
+ self::INVALID_PARAMS,
+ self::INTERNAL_ERROR,
+ ];
+
+ protected static $errorMessages = [
+ self::PARSE_ERROR => 'Invalid JSON was received by the server.',
+ self::INVALID_REQUEST => 'The JSON sent is not a valid Request object',
+ self::METHOD_NOT_FOUND => 'The method does not exist / is not available',
+ self::INVALID_PARAMS => 'Invalid method parameter(s)',
+ self::INTERNAL_ERROR => 'Internal JSON-RPC error',
+ ];
+
+ protected static $defaultCustomMessage = 'Server error. Reserved for implementation-defined server-errors.';
+
+ /** @var int */
+ protected $code;
+
+ /** @var string */
+ protected $message;
+
+ /** @var mixed|null */
+ protected $data;
+
+ /**
+ * Error constructor.
+ * @param int $code
+ * @param string $message
+ * @param mixed $data
+ */
+ public function __construct($code, $message = null, $data = null)
+ {
+ if ($message === null) {
+ if ($this->isCustomErrorCode($code)) {
+ $message = self::$defaultCustomMessage;
+ } elseif (static::isWellKnownErrorCode($code)) {
+ $message = self::$errorMessages[$code];
+ } else {
+ $message = 'Unknown error';
+ }
+ }
+ $this->code = $code;
+ $this->message = $message;
+ $this->data = $data;
+ }
+
+ public static function forException(Exception $exception)
+ {
+ $code = $exception->getCode();
+ if (! static::isCustomErrorCode($code)
+ && ! static::isWellKnownErrorCode($code)
+ ) {
+ $code = self::INTERNAL_ERROR;
+ }
+ if (static::isWellKnownErrorCode($code) && $code !== self::INTERNAL_ERROR) {
+ $data = null;
+ } else {
+ $data = $exception->getTraceAsString();
+ }
+ if (function_exists('iconv')) {
+ $data = iconv('UTF-8', 'UTF-8//IGNORE', $data);
+ }
+
+ return new Error($code, sprintf(
+ '%s in %s(%d)',
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine()
+ ), $data);
+ }
+
+ public static function forTypeError(TypeError $error)
+ {
+ $code = self::INVALID_PARAMS;
+
+ return new Error($code, sprintf(
+ '%s in %s(%d)',
+ $error->getMessage(),
+ $error->getFile(),
+ $error->getLine()
+ ));
+ }
+
+ /**
+ * @return int
+ */
+ public function getCode()
+ {
+ return $this->code;
+ }
+
+ /**
+ * @param int $code
+ * @return $this
+ */
+ public function setCode($code)
+ {
+ $this->code = $code;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * @param string $message
+ * @return $this
+ */
+ public function setMessage($message)
+ {
+ $this->message = $message;
+ return $this;
+ }
+
+ /**
+ * @return mixed|null
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * @param mixed|null $data
+ * @return $this
+ */
+ public function setData($data)
+ {
+ $this->data = $data;
+ return $this;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $result = [
+ 'code' => $this->code,
+ 'message' => $this->message,
+ ];
+
+ if ($this->data !== null) {
+ $result['data'] = $this->data;
+ }
+
+ return (object) $result;
+ }
+
+ public static function isWellKnownErrorCode($code)
+ {
+ return isset(self::$errorMessages[$code]);
+ }
+
+ public static function isCustomErrorCode($code)
+ {
+ return $code >= self::MIN_CUSTOM_ERROR && $code <= self::MAX_CUSTOM_ERROR;
+ }
+
+ /**
+ * @deprecated please use jsonSerialize()
+ * @return mixed
+ */
+ public function toPlainObject()
+ {
+ return $this->jsonSerialize();
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php
new file mode 100644
index 0000000..0c04ac7
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/FailingPacketHandler.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc\Handler;
+
+use gipfl\Protocol\JsonRpc\Error;
+use gipfl\Protocol\JsonRpc\Notification;
+use gipfl\Protocol\JsonRpc\Request;
+
+class FailingPacketHandler implements JsonRpcHandler
+{
+ /** @var Error */
+ protected $error;
+
+ public function __construct(Error $error)
+ {
+ $this->error = $error;
+ }
+
+ public function processNotification(Notification $notification)
+ {
+ // We silently ignore them
+ }
+
+ public function processRequest(Request $request)
+ {
+ return $this->error;
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php
new file mode 100644
index 0000000..f64bc68
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/JsonRpcHandler.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc\Handler;
+
+use gipfl\Protocol\JsonRpc\Error;
+use gipfl\Protocol\JsonRpc\Notification;
+use gipfl\Protocol\JsonRpc\Request;
+use React\Promise\PromiseInterface;
+
+interface JsonRpcHandler
+{
+ /**
+ * @param Request $request
+ * @return Error|PromiseInterface|mixed
+ */
+ public function processRequest(Request $request);
+
+ /**
+ * @param Notification $notification
+ * @return void
+ */
+ public function processNotification(Notification $notification);
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php
new file mode 100644
index 0000000..6e0655b
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Handler/NamespacedPacketHandler.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc\Handler;
+
+use Exception;
+use gipfl\Json\JsonSerialization;
+use gipfl\OpenRpc\Reflection\MetaDataClass;
+use gipfl\OpenRpc\Reflection\MetaDataMethod;
+use gipfl\Protocol\JsonRpc\Error;
+use gipfl\Protocol\JsonRpc\Notification;
+use gipfl\Protocol\JsonRpc\Request;
+use RuntimeException;
+use TypeError;
+use function call_user_func_array;
+use function method_exists;
+use function preg_split;
+use function sprintf;
+use function strpos;
+
+class NamespacedPacketHandler implements JsonRpcHandler
+{
+ protected $nsSeparator = '.';
+
+ protected $nsRegex = '/\./';
+
+ protected $handlers = [];
+
+ /**
+ * @var MetaDataMethod[]
+ */
+ protected $knownMethods = [];
+
+ public function processNotification(Notification $notification)
+ {
+ list($namespace, $method) = $this->splitMethod($notification->getMethod());
+ try {
+ $this->call($namespace, $method, $notification);
+ } catch (Exception $exception) {
+ // Well... we might want to log this
+ } catch (TypeError $exception) {
+ // Well... we might want to log this
+ }
+ }
+
+ public function processRequest(Request $request)
+ {
+ list($namespace, $method) = $this->splitMethod($request->getMethod());
+
+ try {
+ return $this->call($namespace, $method, $request);
+ } catch (Exception $exception) {
+ return Error::forException($exception);
+ } catch (TypeError $error) {
+ return Error::forTypeError($error);
+ }
+ }
+
+ /**
+ * @param string $namespace
+ * @param object $handler
+ */
+ public function registerNamespace($namespace, $handler)
+ {
+ if (isset($this->handlers[$namespace])) {
+ throw new RuntimeException("Cannot register a namespace twice: '$namespace'");
+ }
+ $this->handlers[$namespace] = $handler;
+ $this->analyzeNamespace($namespace, $handler);
+ }
+
+ protected function analyzeNamespace($namespace, $handler)
+ {
+ $meta = MetaDataClass::analyze(get_class($handler));
+ foreach ($meta->getMethods() as $method) {
+ $this->knownMethods[$namespace . $this->nsSeparator . $method->getName()] = $method;
+ }
+ }
+
+ /**
+ * @param string $namespace
+ */
+ public function removeNamespace($namespace)
+ {
+ unset($this->handlers[$namespace]);
+ }
+
+ public function setNamespaceSeparator($separator)
+ {
+ $this->nsSeparator = $separator;
+ $this->nsRegex = '/' . preg_quote($separator, '/') . '/';
+
+ return $this;
+ }
+
+ protected function call($namespace, $method, Notification $notification)
+ {
+ if (! isset($this->handlers[$namespace])) {
+ return $this->notFound($notification, ', no handler for ' . $namespace);
+ }
+
+ $handler = $this->handlers[$namespace];
+ if ($handler instanceof JsonRpcHandler) {
+ if ($notification instanceof Request) {
+ return $handler->processRequest($notification);
+ } else {
+ $handler->processNotification($notification);
+ }
+ }
+
+ $params = $notification->getParams();
+ if (! is_array($params)) {
+ try {
+ $params = $this->prepareParams($notification->getMethod(), $params);
+ } catch (Exception $e) {
+ return Error::forException($e);
+ }
+ }
+ if ($notification instanceof Request) {
+ $rpcMethod = $method . 'Request';
+ if (is_callable([$handler, $rpcMethod])) {
+ return call_user_func_array([$handler, $rpcMethod], $params);
+ }
+
+ return $this->notFound($notification, ', no ' . $rpcMethod);
+ } else {
+ $rpcMethod = $method . 'Notification';
+ if (is_callable([$handler, $rpcMethod])) {
+ call_user_func_array([$handler, $rpcMethod], $params);
+ }
+
+ return null;
+ }
+ }
+
+ protected function prepareParams($method, $params)
+ {
+ if (! isset($this->knownMethods[$method])) {
+ throw new Exception('Cannot map params for unknown method');
+ }
+
+ $meta = $this->knownMethods[$method];
+ $result = [];
+ foreach ($meta->getParameters() as $parameter) {
+ $name = $parameter->getName();
+ if (property_exists($params, $name)) {
+ $value = $params->$name;
+ if ($value === null) {
+ // TODO: check if required
+ $result[] = $value;
+ continue;
+ }
+ switch ($parameter->getType()) {
+ case 'int':
+ $result[] = (int) $value;
+ break;
+ case 'float':
+ $result[] = (float) $value;
+ break;
+ case 'string':
+ $result[] = (string) $value;
+ break;
+ case 'array':
+ $result[] = (array) $value;
+ break;
+ case 'bool':
+ case 'boolean':
+ $result[] = (bool) $value;
+ break;
+ case 'object':
+ $result[] = (object) $value;
+ break;
+ default:
+ $type = $parameter->getType();
+ if (class_exists($type)) {
+ foreach (class_implements($type) as $implement) {
+ if ($implement === JsonSerialization::class) {
+ $result[] = $type::fromSerialization($value);
+ break 2;
+ }
+ }
+ }
+ throw new Exception(sprintf(
+ 'Unsupported parameter type for %s: %s',
+ $method,
+ $parameter->getType()
+ ));
+ }
+ } else {
+ // TODO: isRequired? Set null
+ throw new Exception("Missing parameter for $method: $name");
+ }
+ }
+
+ return $result;
+ }
+
+ protected function splitMethod($method)
+ {
+ if (strpos($method, $this->nsSeparator) === false) {
+ return [null, $method];
+ }
+
+ return preg_split($this->nsRegex, $method, 2);
+ }
+
+ protected function notFound(Notification $notification, $suffix = '')
+ {
+ $error = new Error(Error::METHOD_NOT_FOUND);
+ $error->setMessage(sprintf(
+ '%s: %s' . $suffix,
+ $error->getMessage(),
+ $notification->getMethod()
+ ));
+
+ return $error;
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php
new file mode 100644
index 0000000..88c6f5b
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/JsonRpcConnection.php
@@ -0,0 +1,241 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use Evenement\EventEmitterTrait;
+use Exception;
+use gipfl\Json\JsonEncodeException;
+use gipfl\Protocol\JsonRpc\Handler\JsonRpcHandler;
+use InvalidArgumentException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\NullLogger;
+use React\Promise\Deferred;
+use React\Promise\Promise;
+use React\Stream\DuplexStreamInterface;
+use React\Stream\Util;
+use RuntimeException;
+use function mt_rand;
+use function React\Promise\reject;
+use function React\Promise\resolve;
+
+class JsonRpcConnection implements LoggerAwareInterface
+{
+ use EventEmitterTrait;
+ use LoggerAwareTrait;
+
+ /** @var DuplexStreamInterface */
+ protected $connection;
+
+ /** @var ?JsonRpcHandler */
+ protected $handler;
+
+ /** @var Deferred[] */
+ protected $pending = [];
+
+ protected $unknownErrorCount = 0;
+
+ public function __construct(DuplexStreamInterface $connection, JsonRpcHandler $handler = null)
+ {
+ $this->setLogger(new NullLogger());
+ $this->connection = $connection;
+ $this->setHandler($handler);
+ $this->connection->on('data', function ($data) {
+ try {
+ $this->handlePacket(Packet::decode($data));
+ } catch (\Exception $error) {
+ $this->logger->error($error->getMessage());
+ $this->unknownErrorCount++;
+ if ($this->unknownErrorCount === 3) {
+ // e.g.: decoding errors
+ // TODO: should we really close? Or just send error responses for every Exception?
+ $this->close();
+ }
+ $response = new Response();
+ $response->setError(Error::forException($error));
+ $this->connection->write($response->toString());
+ }
+ });
+ $connection->on('close', function () {
+ $this->rejectAllPendingRequests('Connection closed');
+ });
+ // Hint: Util::pipe takes care of the pipe event
+ Util::forwardEvents($connection, $this, ['end', 'error', 'close', 'drain']);
+ }
+
+ /**
+ * @param Packet $packet
+ */
+ protected function handlePacket(Packet $packet)
+ {
+ if ($packet instanceof Response) {
+ $this->handleResponse($packet);
+ } elseif ($packet instanceof Request) {
+ if ($this->handler) {
+ $result = $this->handler->processRequest($packet);
+ } else {
+ $result = new Error(Error::METHOD_NOT_FOUND);
+ $result->setMessage($result->getMessage() . ': ' . $packet->getMethod());
+ }
+ $this->sendResultForRequest($packet, $result);
+ } elseif ($packet instanceof Notification) {
+ if ($this->handler) {
+ $this->handler->processNotification($packet);
+ }
+ } else {
+ // Will not happen as long as there is no bug in Packet
+ throw new RuntimeException('Packet was neither Request/Notification nor Response');
+ }
+ }
+
+ protected function handleResponse(Response $response)
+ {
+ $id = $response->getId();
+ if (isset($this->pending[$id])) {
+ $promise = $this->pending[$id];
+ unset($this->pending[$id]);
+ $promise->resolve($response);
+ } else {
+ $this->handleUnmatchedResponse($response);
+ }
+ }
+
+ protected function handleUnmatchedResponse(Response $response)
+ {
+ $this->logger->error('Unmatched Response: ' . $response->toString());
+ }
+
+ protected function sendResultForRequest(Request $request, $result)
+ {
+ if ($result instanceof Error) {
+ $response = Response::forRequest($request);
+ $response->setError($result);
+ if ($this->connection && $this->connection->isWritable()) {
+ $this->connection->write($response->toString());
+ } else {
+ $this->logger->error('Failed to send response, have no writable connection');
+ }
+ } elseif ($result instanceof Promise) {
+ $result->then(function ($result) use ($request) {
+ $this->sendResultForRequest($request, $result);
+ }, function ($error) use ($request) {
+ $response = Response::forRequest($request);
+ if ($error instanceof Exception || $error instanceof \Throwable) {
+ $response->setError(Error::forException($error));
+ } else {
+ $response->setError(new Error(Error::INTERNAL_ERROR, $error));
+ }
+ // TODO: Double-check, this used to loop
+ $this->connection->write($response->toString());
+ })->done();
+ } else {
+ $response = Response::forRequest($request);
+ $response->setResult($result);
+ if ($this->connection && $this->connection->isWritable()) {
+ $this->connection->write($response->toString());
+ } else {
+ $this->logger->error('Failed to send response, have no writable connection');
+ }
+ }
+ }
+
+ /**
+ * @param Request $request
+ * @return \React\Promise\PromiseInterface
+ */
+ public function sendRequest(Request $request)
+ {
+ $id = $request->getId();
+ if ($id === null) {
+ $id = $this->getNextRequestId();
+ $request->setId($id);
+ }
+ if (isset($this->pending[$id])) {
+ throw new InvalidArgumentException(
+ "A request with id '$id' is already pending"
+ );
+ }
+ if (!$this->connection->isWritable()) {
+ return reject(new Exception('Cannot write to socket'));
+ }
+ try {
+ $this->connection->write($request->toString());
+ } catch (JsonEncodeException $e) {
+ return reject($e->getMessage());
+ }
+ $deferred = new Deferred(function () use ($id) {
+ unset($this->pending[$id]);
+ });
+ $this->pending[$id] = $deferred;
+
+ return $deferred->promise()->then(function (Response $response) {
+ if ($response->isError()) {
+ return reject(new RuntimeException($response->getError()->getMessage()));
+ }
+
+ return resolve($response->getResult());
+ });
+ }
+
+ public function request($method, $params = null)
+ {
+ return $this->sendRequest(new Request($method, $this->getNextRequestId(), $params));
+ }
+
+ protected function getNextRequestId()
+ {
+ for ($i = 0; $i < 100; $i++) {
+ $id = mt_rand(1, 1000000000);
+ if (!isset($this->pending[$id])) {
+ return $id;
+ }
+ }
+
+ throw new RuntimeException('Unable to generate a free random request ID, gave up after 100 attempts');
+ }
+
+ /**
+ * @param Notification $packet
+ */
+ public function sendNotification(Notification $packet)
+ {
+ $this->connection->write($packet->toString());
+ }
+
+ /**
+ * @param string $method
+ * @param null $params
+ */
+ public function notification($method, $params = null)
+ {
+ $notification = new Notification($method, $params);
+ $this->sendNotification($notification);
+ }
+
+ /**
+ * @param PacketHandler $handler
+ * @return $this
+ */
+ public function setHandler(JsonRpcHandler $handler = null)
+ {
+ $this->handler = $handler;
+ return $this;
+ }
+
+ protected function rejectAllPendingRequests($message)
+ {
+ foreach ($this->pending as $pending) {
+ $pending->reject(new Exception($message));
+ }
+ $this->pending = [];
+ }
+
+ public function close()
+ {
+ if ($this->connection) {
+ $this->connection->close();
+ $this->handler = null;
+ $this->connection = null;
+ }
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Notification.php b/vendor/gipfl/protocol-jsonrpc/src/Notification.php
new file mode 100644
index 0000000..338efc1
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Notification.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+class Notification extends Packet
+{
+ /** @var string */
+ protected $method;
+
+ /** @var \stdClass|array */
+ protected $params;
+
+ public function __construct($method, $params)
+ {
+ $this->setMethod($method);
+ $this->setParams($params);
+ }
+
+ /**
+ * @return string
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * @param string $method
+ */
+ public function setMethod($method)
+ {
+ $this->method = $method;
+ }
+
+ /**
+ * @return object|array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * @param object|array $params
+ */
+ public function setParams($params)
+ {
+ $this->params = $params;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $default
+ * @return mixed|null
+ */
+ public function getParam($name, $default = null)
+ {
+ $p = & $this->params;
+ if (\is_object($p) && \property_exists($p, $name)) {
+ return $p->$name;
+ } elseif (\is_array($p) && \array_key_exists($name, $p)) {
+ return $p[$name];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @return object
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $plain = [
+ 'jsonrpc' => '2.0',
+ 'method' => $this->method,
+ 'params' => $this->params,
+ ];
+
+ if ($this->hasExtraProperties()) {
+ $plain += (array) $this->getExtraProperties();
+ }
+
+ return (object) $plain;
+ }
+
+ /**
+ * @param $method
+ * @param $params
+ * @return static
+ */
+ public static function create($method, $params)
+ {
+ $packet = new Notification($method, $params);
+
+ return $packet;
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Packet.php b/vendor/gipfl/protocol-jsonrpc/src/Packet.php
new file mode 100644
index 0000000..8dca44e
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Packet.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use gipfl\Json\JsonException;
+use gipfl\Json\JsonSerialization;
+use gipfl\Json\JsonString;
+use gipfl\Protocol\Exception\ProtocolError;
+use function property_exists;
+
+abstract class Packet implements JsonSerialization
+{
+ /** @var \stdClass|null */
+ protected $extraProperties;
+
+ /**
+ * @return string
+ * @throws \gipfl\Json\JsonEncodeException
+ */
+ public function toString()
+ {
+ return JsonString::encode($this->jsonSerialize());
+ }
+
+ /**
+ * @return string
+ * @throws \gipfl\Json\JsonEncodeException
+ */
+ public function toPrettyString()
+ {
+ return JsonString::encode($this->jsonSerialize(), JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasExtraProperties()
+ {
+ return $this->extraProperties !== null;
+ }
+
+ /**
+ * @return \stdClass|null
+ */
+ public function getExtraProperties()
+ {
+ return $this->extraProperties;
+ }
+
+ /**
+ * @param \stdClass|null $extraProperties
+ * @return $this
+ * @throws ProtocolError
+ */
+ public function setExtraProperties($extraProperties)
+ {
+ foreach (['id', 'error', 'result', 'jsonrpc', 'method', 'params'] as $key) {
+ if (property_exists($extraProperties, $key)) {
+ throw new ProtocolError("Cannot accept '$key' as an extra property");
+ }
+ }
+ $this->extraProperties = $extraProperties;
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed|null $default
+ * @return mixed|null
+ */
+ public function getExtraProperty($name, $default = null)
+ {
+ if (isset($this->extraProperties->$name)) {
+ return $this->extraProperties->$name;
+ } else {
+ return $default;
+ }
+ }
+
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ * @return $this
+ */
+ public function setExtraProperty($name, $value)
+ {
+ if ($this->extraProperties === null) {
+ $this->extraProperties = (object) [$name => $value];
+ } else {
+ $this->extraProperties->$name = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $string
+ * @return Notification|Request|Response
+ * @throws ProtocolError
+ */
+ public static function decode($string)
+ {
+ try {
+ return self::fromSerialization(JsonString::decode($string));
+ } catch (JsonException $e) {
+ throw new ProtocolError(sprintf(
+ 'JSON decode failed: %s',
+ $e->getMessage()
+ ), Error::PARSE_ERROR);
+ }
+ }
+
+ public static function fromSerialization($any)
+ {
+ $version = static::stripRequiredProperty($any, 'jsonrpc');
+ if ($version !== '2.0') {
+ throw new ProtocolError(
+ "Only JSON-RPC 2.0 is supported, got $version",
+ Error::INVALID_REQUEST
+ );
+ }
+
+ // Hint: we MUST use property_exists here, as a NULL id is allowed
+ // in error responsed in case it wasn't possible to determine a
+ // request id
+ $hasId = property_exists($any, 'id');
+ $id = static::stripOptionalProperty($any, 'id');
+ $error = static::stripOptionalProperty($any, 'error');
+ if (property_exists($any, 'method')) {
+ $method = static::stripRequiredProperty($any, 'method');
+ $params = static::stripRequiredProperty($any, 'params');
+
+ if ($id === null) {
+ $packet = new Notification($method, $params);
+ } else {
+ $packet = new Request($method, $id, $params);
+ }
+ } elseif (! $hasId) {
+ throw new ProtocolError(
+ "Given string is not a valid JSON-RPC 2.0 response: id is missing",
+ Error::INVALID_REQUEST
+ );
+ } else {
+ $packet = new Response($id);
+ if ($error) {
+ $packet->setError(new Error(
+ static::stripOptionalProperty($error, 'code'),
+ static::stripOptionalProperty($error, 'message'),
+ static::stripOptionalProperty($error, 'data')
+ ));
+ } else {
+ $result = static::stripRequiredProperty($any, 'result');
+ $packet->setResult($result);
+ }
+ }
+ if (count((array) $any) > 0) {
+ $packet->setExtraProperties($any);
+ }
+
+ return $packet;
+ }
+
+ /**
+ * @param object $object
+ * @param string $property
+ * @throws ProtocolError
+ */
+ protected static function assertPropertyExists($object, $property)
+ {
+ if (! property_exists($object, $property)) {
+ throw new ProtocolError(
+ "Expected valid JSON-RPC, got no '$property' property",
+ Error::INVALID_REQUEST
+ );
+ }
+ }
+
+ /**
+ * @param \stdClass $object
+ * @param string $property
+ * @return mixed|null
+ */
+ protected static function stripOptionalProperty($object, $property)
+ {
+ if (property_exists($object, $property)) {
+ $value = $object->$property;
+ unset($object->$property);
+
+ return $value;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param \stdClass $object
+ * @param string $property
+ * @return mixed
+ * @throws ProtocolError
+ */
+ protected static function stripRequiredProperty($object, $property)
+ {
+ if (! property_exists($object, $property)) {
+ throw new ProtocolError(
+ "Expected valid JSON-RPC, got no '$property' property",
+ Error::INVALID_REQUEST
+ );
+ }
+
+ $value = $object->$property;
+ unset($object->$property);
+
+ return $value;
+ }
+
+ /**
+ * @deprecated please use jsonSerialize()
+ * @return string
+ */
+ public function toPlainObject()
+ {
+ return $this->jsonSerialize();
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php
new file mode 100644
index 0000000..e3f23c2
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/PacketHandler.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+/**
+ * @deprecated
+ */
+interface PacketHandler
+{
+ public function handle(Notification $notification);
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Request.php b/vendor/gipfl/protocol-jsonrpc/src/Request.php
new file mode 100644
index 0000000..2061a41
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Request.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use gipfl\Protocol\Exception\ProtocolError;
+
+class Request extends Notification
+{
+ /** @var mixed */
+ protected $id;
+
+ /**
+ * Request constructor.
+ * @param string $method
+ * @param mixed $id
+ * @param null $params
+ */
+ public function __construct($method, $id = null, $params = null)
+ {
+ parent::__construct($method, $params);
+
+ $this->id = $id;
+ }
+
+ /**
+ * @return object
+ * @throws ProtocolError
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ if ($this->id === null) {
+ throw new ProtocolError(
+ 'A request without an ID is not valid'
+ );
+ }
+
+ $plain = parent::jsonSerialize();
+ $plain->id = $this->id;
+
+ return $plain;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param mixed $id
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/Response.php b/vendor/gipfl/protocol-jsonrpc/src/Response.php
new file mode 100644
index 0000000..3a5ad90
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/Response.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+class Response extends Packet
+{
+ /** @var mixed|null This could be null when sending a parse error */
+ protected $id;
+
+ /** @var mixed */
+ protected $result;
+
+ /** @var Error|null */
+ protected $error;
+
+ /** @var string */
+ protected $message;
+
+ public function __construct($id = null)
+ {
+ $this->id = $id;
+ }
+
+ /**
+ * @param Request $request
+ * @return Response
+ */
+ public static function forRequest(Request $request)
+ {
+ return new Response($request->getId());
+ }
+
+ /**
+ * @return object
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $plain = [
+ 'jsonrpc' => '2.0',
+ ];
+ if ($this->hasExtraProperties()) {
+ $plain += (array) $this->getExtraProperties();
+ }
+
+ if ($this->id !== null) {
+ $plain['id'] = $this->id;
+ }
+
+ if ($this->error === null) {
+ $plain['result'] = $this->result;
+ } else {
+ if (! isset($plain['id'])) {
+ $plain['id'] = null;
+ }
+ $plain['error'] = $this->error;
+ }
+
+ return (object) $plain;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getResult()
+ {
+ return $this->result;
+ }
+
+ /**
+ * @param $result
+ * @return $this
+ */
+ public function setResult($result)
+ {
+ $this->result = $result;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasId()
+ {
+ return null !== $this->id;
+ }
+
+ /**
+ * @return null|int|string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param $id
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ }
+
+ public function isError()
+ {
+ return $this->error !== null;
+ }
+
+ /**
+ * @return Error|null
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * @param $error
+ * @return $this;
+ */
+ public function setError(Error $error)
+ {
+ $this->error = $error;
+
+ return $this;
+ }
+}
diff --git a/vendor/gipfl/protocol-jsonrpc/src/TestCase.php b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php
new file mode 100644
index 0000000..05f54ba
--- /dev/null
+++ b/vendor/gipfl/protocol-jsonrpc/src/TestCase.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace gipfl\Protocol\JsonRpc;
+
+use PHPUnit\Framework\TestCase as BaseTestCase;
+use React\EventLoop\LoopInterface;
+
+class TestCase extends BaseTestCase
+{
+ protected $examples = [];
+
+ protected function parseExample($key)
+ {
+ return Packet::decode($this->examples[$key]);
+ }
+
+ protected function failAfterSeconds($seconds, LoopInterface $loop)
+ {
+ $loop->addTimer($seconds, function () use ($seconds) {
+ throw new \RuntimeException("Timed out after $seconds seconds");
+ });
+ }
+
+ protected function collectErrorsForNotices(&$errors)
+ {
+ \set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$errors) {
+ if (\error_reporting() === 0) { // @-operator in use
+ return false;
+ }
+ $errors[] = new \ErrorException($errstr, 0, $errno, $errfile, $errline);
+
+ return false; // Always continue with normal error processing
+ }, E_ALL | E_STRICT);
+
+ \error_reporting(E_ALL | E_STRICT);
+ }
+
+ protected function throwEventualErrors(array $errors)
+ {
+ foreach ($errors as $error) {
+ throw $error;
+ }
+ }
+}