mirror of
https://github.com/predis/predis.git
synced 2026-08-30 12:15:03 +00:00
Added RESP3 Parser strategy (#1294)
* Added RESP3 Parser strategy * Fixed error messages
This commit is contained in:
committed by
GitHub
parent
0a6f9e7b70
commit
55f6980594
@@ -15,6 +15,7 @@ namespace Predis\Connection;
|
||||
use InvalidArgumentException;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Protocol\Parser\UnexpectedTypeException;
|
||||
use Predis\Response\Error;
|
||||
use Predis\Response\ErrorInterface as ErrorResponseInterface;
|
||||
|
||||
/**
|
||||
@@ -301,6 +302,7 @@ class StreamConnection extends AbstractConnection
|
||||
}
|
||||
|
||||
switch ($parsedData['type']) {
|
||||
case 'push':
|
||||
case 'array':
|
||||
$data = [];
|
||||
|
||||
@@ -311,22 +313,38 @@ class StreamConnection extends AbstractConnection
|
||||
return $data;
|
||||
|
||||
case 'bulkString':
|
||||
$bulkData = '';
|
||||
$size = $parsedData['value'];
|
||||
$bytesLeft = ($size += 2);
|
||||
|
||||
do {
|
||||
$chunk = is_resource($socket) ? fread($socket, min($bytesLeft, 4096)) : false;
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading bytes from the server.');
|
||||
}
|
||||
|
||||
$bulkData .= $chunk;
|
||||
$bytesLeft = $size - strlen($bulkData);
|
||||
} while ($bytesLeft > 0);
|
||||
case 'verbatimString':
|
||||
$bulkData = $this->readByChunks($socket, $parsedData['value']);
|
||||
|
||||
return substr($bulkData, 0, -2);
|
||||
|
||||
case 'blobError':
|
||||
$errorMessage = $this->readByChunks($socket, $parsedData['value']);
|
||||
|
||||
return new Error(substr($errorMessage, 0, -2));
|
||||
|
||||
case 'map':
|
||||
$data = [];
|
||||
|
||||
for ($i = 0; $i < $parsedData['value']; ++$i) {
|
||||
$key = $this->read();
|
||||
$data[$key] = $this->read();
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
case 'set':
|
||||
$data = [];
|
||||
|
||||
for ($i = 0; $i < $parsedData['value']; ++$i) {
|
||||
$element = $this->read();
|
||||
|
||||
if (!in_array($element, $data, true)) {
|
||||
$data[] = $element;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $parsedData;
|
||||
@@ -352,4 +370,30 @@ class StreamConnection extends AbstractConnection
|
||||
|
||||
$this->write($buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads given resource split on chunks with given size.
|
||||
*
|
||||
* @param $resource
|
||||
* @param int $chunkSize
|
||||
* @return string
|
||||
*/
|
||||
private function readByChunks($resource, int $chunkSize): string
|
||||
{
|
||||
$string = '';
|
||||
$bytesLeft = ($chunkSize += 2);
|
||||
|
||||
do {
|
||||
$chunk = is_resource($resource) ? fread($resource, min($bytesLeft, 4096)) : false;
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->onConnectionError('Error while reading bytes from the server.');
|
||||
}
|
||||
|
||||
$string .= $chunk;
|
||||
$bytesLeft = $chunkSize - strlen($string);
|
||||
} while ($bytesLeft > 0);
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class Resp2Strategy implements ParserStrategyInterface
|
||||
/**
|
||||
* Callbacks to process given RESP type.
|
||||
*
|
||||
* @var callable[]
|
||||
* @var string[]
|
||||
*/
|
||||
protected $typeCallbacks = [
|
||||
'+' => 'parseSimpleString',
|
||||
|
||||
@@ -14,4 +14,145 @@ namespace Predis\Protocol\Parser\Strategy;
|
||||
|
||||
class Resp3Strategy extends Resp2Strategy
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $resp3TypeCallbacks = [
|
||||
'_' => 'parseNull',
|
||||
',' => 'parseDouble',
|
||||
'#' => 'parseBoolean',
|
||||
'!' => 'parseBlobError',
|
||||
'=' => 'parseVerbatimString',
|
||||
'(' => 'parseBigNumber',
|
||||
'%' => 'parseMap',
|
||||
'~' => 'parseSet',
|
||||
'>' => 'parsePush',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->typeCallbacks += $this->resp3TypeCallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse null RESP3 type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function parseNull(string $string)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse double RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return float
|
||||
*/
|
||||
protected function parseDouble(string $string): float
|
||||
{
|
||||
if ($string === 'inf' || $string === '-inf') {
|
||||
return INF;
|
||||
}
|
||||
|
||||
return (float) $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse boolean RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return bool
|
||||
*/
|
||||
protected function parseBoolean(string $string): bool
|
||||
{
|
||||
return $string === 't';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse blob error RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
protected function parseBlobError(string $string): array
|
||||
{
|
||||
return [
|
||||
'type' => 'blobError',
|
||||
'value' => (int) $string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse verbatim string RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
protected function parseVerbatimString(string $string): array
|
||||
{
|
||||
return [
|
||||
'type' => 'verbatimString',
|
||||
'value' => (int) $string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse big number RESP3 type.
|
||||
* Depends on PHP environment returns float on numbers that reaches max integer limit.
|
||||
*
|
||||
* @param string $string
|
||||
* @return int|float
|
||||
*/
|
||||
protected function parseBigNumber(string $string)
|
||||
{
|
||||
if (bccomp($string, PHP_INT_MAX) === 1) {
|
||||
return (float) $string;
|
||||
}
|
||||
|
||||
return $this->parseInteger($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse map RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
protected function parseMap(string $string): array
|
||||
{
|
||||
return [
|
||||
'type' => 'map',
|
||||
'value' => (int) $string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse set RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSet(string $string): array
|
||||
{
|
||||
return [
|
||||
'type' => 'set',
|
||||
'value' => (int) $string,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse push RESP3 type.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
protected function parsePush(string $string): array
|
||||
{
|
||||
return [
|
||||
'type' => 'push',
|
||||
'value' => (int) $string,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class FTINFO_Test extends PredisCommandTestCase
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('Unknown Index name');
|
||||
$this->expectExceptionMessage('Unknown index name');
|
||||
|
||||
$redis->ftinfo('index');
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class TSDECRBY_Test extends PredisCommandTestCase
|
||||
);
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('TSDB: for incrby/decrby, timestamp should be newer than the');
|
||||
$this->expectExceptionMessage('TSDB: timestamp must be equal to or higher than the maximum existing timestamp');
|
||||
|
||||
$redis->tsdecrby('temperature:2:32', 27, (new DecrByArguments())->timestamp(123123123122));
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class TSINCRBY_Test extends PredisCommandTestCase
|
||||
);
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('TSDB: for incrby/decrby, timestamp should be newer than the');
|
||||
$this->expectExceptionMessage('TSDB: timestamp must be equal to or higher than the maximum existing timestamp');
|
||||
|
||||
$redis->tsincrby('temperature:2:32', 27, (new IncrByArguments())->timestamp(123123123122));
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class TSMADD_Test extends PredisCommandTestCase
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage("ERR wrong number of arguments for 'TS.MADD' command");
|
||||
$this->expectExceptionMessage("ERR wrong number of arguments for 'ts.madd' command");
|
||||
|
||||
$redis->tsmadd('temperature:2:32', 123123123123, 27, 'temperature:2:33', 123123123124);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2023 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Protocol\Parser\Strategy;
|
||||
|
||||
use PredisTestCase;
|
||||
|
||||
class Resp3StrategyTest extends PredisTestCase
|
||||
{
|
||||
/**
|
||||
* @var ParserStrategyInterface
|
||||
*/
|
||||
protected $strategy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->strategy = new Resp3Strategy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsNullOnNullType(): void
|
||||
{
|
||||
$data = "_\r\n";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertNull($actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsFloatOnDoubleType(): void
|
||||
{
|
||||
$data = ",1.23\r\n";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(1.23, $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider infinityProvider
|
||||
* @group disconnected
|
||||
* @param string $data
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data): void
|
||||
{
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertInfinite($actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider booleanProvider
|
||||
* @group disconnected
|
||||
* @param string $data
|
||||
* @param bool $expectedValue
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsBooleanOnBooleanType(string $data, bool $expectedValue): void
|
||||
{
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame($expectedValue, $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsArrayOnBlobErrorType(): void
|
||||
{
|
||||
$data = "!21\r\nSYNTAX invalid syntax\r\n";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(['type' => 'blobError', 'value' => 21], $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsArrayOnVerbatimStringType(): void
|
||||
{
|
||||
$data = "=15\r\ntxt:Some string\r\n";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(['type' => 'verbatimString', 'value' => 15], $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider bigNumberProvider
|
||||
* @group disconnected
|
||||
* @param string $data
|
||||
* @param int|float $expectedValue
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsIntegerOrFloatOnBigNumberType(string $data, $expectedValue): void
|
||||
{
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame($expectedValue, $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsArrayOnMapType(): void
|
||||
{
|
||||
$data = "%2\r\n+first\r\n:1\r\n+second\r\n:2";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(['type' => 'map', 'value' => 2], $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsArrayOnSetType(): void
|
||||
{
|
||||
$data = "~4\r\n+first\r\n:1\r\n+second\r\n:2";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(['type' => 'set', 'value' => 4], $actualResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @return void
|
||||
*/
|
||||
public function testParseDataReturnsArrayOnPushType(): void
|
||||
{
|
||||
$data = ">4\r\n+pubsub\r\n+message\r\n+somechannel\r\n+this is the message";
|
||||
|
||||
$actualResponse = $this->strategy->parseData($data);
|
||||
|
||||
$this->assertSame(['type' => 'push', 'value' => 4], $actualResponse);
|
||||
}
|
||||
|
||||
public function infinityProvider(): array
|
||||
{
|
||||
return [
|
||||
'positive infinity' => [",inf\r\n"],
|
||||
'negative infinity' => [",-inf\r\n"],
|
||||
];
|
||||
}
|
||||
|
||||
public function booleanProvider(): array
|
||||
{
|
||||
return [
|
||||
'true' => ["#t\r\n", true],
|
||||
'false' => ["#f\r\n", false],
|
||||
];
|
||||
}
|
||||
|
||||
public function bigNumberProvider(): array
|
||||
{
|
||||
return [
|
||||
'greater than integer limit' => [
|
||||
"(3492890328409238509324850943850943825024385\r\n",
|
||||
3492890328409238509324850943850943825024385,
|
||||
],
|
||||
'lower than integer limit' => [
|
||||
"(34928903\r\n",
|
||||
34928903,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user