Skip to content

Commit 72bf5c4

Browse files
committed
Initial commit of JSON RPC Helper classes along with a ReactPHP stream decoder
1 parent 07000fe commit 72bf5c4

File tree

6 files changed

+399
-0
lines changed

6 files changed

+399
-0
lines changed

composer.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "edgetelemetrics/php-json-rpc",
3+
"description": "JSON-RPC helper classes for PHP",
4+
"keywords": ["json-rpc", "react"],
5+
"license": "MIT",
6+
"authors": [ { "name": "James Lucas", "email": "james@lucas.net.au" } ],
7+
"type": "library",
8+
"require": {
9+
"php": ">=7.2",
10+
"ext-json": "*",
11+
"evenement/evenement": "^3.0",
12+
"clue/ndjson-react": "^1.0"
13+
},
14+
"require-dev": {
15+
"kelunik/fqn-check": "^0.1.3"
16+
},
17+
"autoload": {
18+
"psr-4": {"EdgeTelemetrics\\JSON_RPC\\": "src/EdgeTelemetrics/JSON_RPC"}
19+
}
20+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace EdgeTelemetrics\JSON_RPC;
4+
5+
class Error implements \JsonSerializable
6+
{
7+
/**
8+
* @var int
9+
*/
10+
protected $code;
11+
12+
/**
13+
* @var string
14+
*/
15+
protected $message;
16+
17+
protected $data;
18+
19+
public function __construct(int $code, string $message, $data = null)
20+
{
21+
$this->setCode($code);
22+
$this->setMessage($message);
23+
$this->setData($data);
24+
}
25+
26+
public function setCode(int $code)
27+
{
28+
$this->code = $code;
29+
}
30+
31+
public function getCode()
32+
{
33+
return $this->code;
34+
}
35+
36+
public function setMessage(string $message)
37+
{
38+
$this->message = $message;
39+
}
40+
41+
public function getMessage()
42+
{
43+
return $this->message;
44+
}
45+
46+
public function setData($data)
47+
{
48+
$this->data = $data;
49+
}
50+
51+
public function getData()
52+
{
53+
return $this->data;
54+
}
55+
56+
public function jsonSerialize()
57+
{
58+
$record = ['code' => $this->code,
59+
'message' => $this->message];
60+
61+
if (null !== $this->data)
62+
{
63+
$record['data'] = $this->data;
64+
}
65+
66+
return $record;
67+
}
68+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace EdgeTelemetrics\JSON_RPC;
4+
5+
class Notification implements \JsonSerializable {
6+
7+
const JSONRPC_VERSION = '2.0';
8+
9+
/**
10+
* @var string
11+
*/
12+
protected $method = '';
13+
14+
/**
15+
* @var array
16+
*/
17+
protected $params = [];
18+
19+
public function setMethod(string $method)
20+
{
21+
$this->method = $method;
22+
}
23+
24+
public function getMethod() : string
25+
{
26+
return $this->method;
27+
}
28+
29+
public function setParams(array $params)
30+
{
31+
$this->params = $params;
32+
}
33+
34+
public function setParam(string $name, $value)
35+
{
36+
$this->params[$name] = $value;
37+
}
38+
39+
public function getParams()
40+
{
41+
return $this->params;
42+
}
43+
44+
public function getParam(string $name)
45+
{
46+
return $this->params[$name];
47+
}
48+
49+
public function jsonSerialize()
50+
{
51+
$record = ['jsonrpc' => self::JSONRPC_VERSION,
52+
'method' => $this->method];
53+
if (!empty($this->params))
54+
{
55+
$record['params'] = $this->params;
56+
}
57+
return $record;
58+
}
59+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace EdgeTelemetrics\JSON_RPC\React;
4+
5+
use Evenement\EventEmitter;
6+
use React\Stream\ReadableStreamInterface;
7+
use React\Stream\WritableStreamInterface;
8+
use React\Stream\Util;
9+
use RuntimeException;
10+
use Clue\React\NDJson\Decoder as NDJsonDecoder;
11+
use EdgeTelemetrics\JSON_RPC\Notification;
12+
use EdgeTelemetrics\JSON_RPC\Request;
13+
use EdgeTelemetrics\JSON_RPC\Response;
14+
use EdgeTelemetrics\JSON_RPC\Error;
15+
16+
/**
17+
* The Decoder / Parser reads from a NDJSON stream and emits JSON-RPC notifications/requests/responses
18+
*/
19+
class Decoder extends EventEmitter implements ReadableStreamInterface
20+
{
21+
/**
22+
* @var \Clue\React\NDJson\Decoder
23+
*/
24+
protected $ndjson_decoder;
25+
26+
private $closed = false;
27+
28+
public function __construct(ReadableStreamInterface $input)
29+
{
30+
$this->ndjson_decoder = new NDJsonDecoder($input, true);
31+
32+
$this->ndjson_decoder->on('data', array($this, 'handleData'));
33+
$this->ndjson_decoder->on('end', array($this, 'handleEnd'));
34+
$this->ndjson_decoder->on('error', array($this, 'handleError'));
35+
$this->ndjson_decoder->on('close', array($this, 'close'));
36+
}
37+
38+
public function close()
39+
{
40+
$this->closed = true;
41+
$this->ndjson_decoder->close();
42+
$this->emit('close');
43+
$this->removeAllListeners();
44+
}
45+
46+
public function isReadable()
47+
{
48+
return $this->ndjson_decoder->isReadable();
49+
}
50+
51+
public function pause()
52+
{
53+
$this->ndjson_decoder->pause();
54+
}
55+
56+
public function resume()
57+
{
58+
$this->ndjson_decoder->resume();
59+
}
60+
61+
public function pipe(WritableStreamInterface $dest, array $options = array())
62+
{
63+
Util::pipe($this, $dest, $options);
64+
return $dest;
65+
}
66+
67+
//@TODO Handle json-rpc batch (array of data)
68+
public function handleData($data)
69+
{
70+
if (!isset($data['jsonrpc']))
71+
{
72+
throw new RuntimeException('Unable to decode. Missing required jsonrpc field');
73+
}
74+
75+
if ($data['jsonrpc'] != Notification::JSONRPC_VERSION)
76+
{
77+
throw new RuntimeException('Unknown JSON-RPC version string');
78+
}
79+
80+
if (isset($data['method']))
81+
{
82+
if (isset($data['id']))
83+
{
84+
$jsonrpc = new Request();
85+
$jsonrpc->setId($data['id']);
86+
}
87+
else
88+
{
89+
$jsonrpc = new Notification();
90+
}
91+
$jsonrpc->setMethod($data['method']);
92+
if (isset($data['params']))
93+
{
94+
$jsonrpc->setParams($data['params']);
95+
}
96+
}
97+
elseif (isset($data['result']) || isset($data['error']))
98+
{
99+
$jsonrpc = new Response();
100+
$jsonrpc->setId($data['id']);
101+
if (isset($data['result']))
102+
{
103+
$jsonrpc->setResult($data['result']);
104+
}
105+
else
106+
{
107+
$error = new Error();
108+
$error->setCode($data['error']['code']);
109+
$error->setMessage($data['error']['message']);
110+
if (isset($data['error']['data']))
111+
{
112+
$error->setData($data['error']['data']);
113+
}
114+
$jsonrpc->setError($error);
115+
}
116+
}
117+
else
118+
{
119+
throw new RuntimeException('Unable to decode json rpc packet');
120+
}
121+
122+
$this->emit('data', [$jsonrpc]);
123+
}
124+
125+
/** @internal */
126+
public function handleEnd()
127+
{
128+
if (!$this->closed) {
129+
$this->emit('end');
130+
$this->close();
131+
}
132+
}
133+
134+
/** @internal */
135+
public function handleError(\Exception $error)
136+
{
137+
$this->emit('error', array($error));
138+
$this->close();
139+
}
140+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace EdgeTelemetrics\JSON_RPC;
4+
5+
class Request extends Notification implements \JsonSerializable {
6+
7+
/**
8+
* @var string|int|null
9+
*/
10+
protected $id;
11+
12+
public function setId($id)
13+
{
14+
$this->id = $id;
15+
}
16+
17+
public function getId()
18+
{
19+
return $this->id;
20+
}
21+
22+
public function jsonSerialize()
23+
{
24+
$record = parent::jsonSerialize();
25+
$record['id'] = $this->id;
26+
return $record;
27+
}
28+
}

0 commit comments

Comments
 (0)