Added support for ZMPOP command (#831)

Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local>
This commit is contained in:
Vladyslav Vildanov
2022-12-01 22:10:20 +02:00
committed by GitHub
parent cfd6f030bc
commit 7904fed6da
8 changed files with 479 additions and 0 deletions
+1
View File
@@ -113,6 +113,7 @@ use Predis\Command\CommandInterface;
* @method $this zdiffstore(string $destination, array $keys)
* @method $this zincrby($key, $increment, $member)
* @method $this zinterstore($destination, array|string $keys, array $options = null)
* @method $this zmpop(array $keys, string $modifier = 'min', int $count = 1)
* @method $this zmscore(string $key, string ...$member)
* @method $this zrandmember(string $key, int $count = 1, bool $withScores = false)
* @method $this zrange($key, $start, $stop, array $options = null)
+1
View File
@@ -129,6 +129,7 @@ use Predis\Response\Status;
* @method int zdiffstore(string $destination, array $keys)
* @method string zincrby(string $key, int $increment, string $member)
* @method int zinterstore(string $destination, array|string $keys, array $options = null)
* @method array zmpop(array $keys, string $modifier = 'min', int $count = 1)
* @method array zmscore(string $key, string ...$member)
* @method array zpopmin(string $key, int $count = 1)
* @method array zpopmax(string $key, int $count = 1)
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\Count;
use Predis\Command\Traits\MinMaxModifier;
use Predis\Command\Traits\Numkeys;
use Predis\Command\Traits\Keys;
class ZMPOP extends RedisCommand
{
use Numkeys {
Numkeys::setArguments as setNumkeys;
}
use Count {
Count::setArguments as setCount;
}
use MinMaxModifier;
use Keys;
protected static $keysArgumentPositionOffset = 0;
protected static $countArgumentPositionOffset = 2;
public function getId()
{
return 'ZMPOP';
}
public function setArguments(array $arguments)
{
$this->setCount($arguments);
$arguments = $this->getArguments();
$this->setNumkeys($arguments);
$arguments = $this->getArguments();
$this->resolveModifier(2, $arguments);
$this->unpackKeysArray(self::$keysArgumentPositionOffset + 1, $arguments);
parent::setArguments($arguments);
}
public function parseResponse($data)
{
$key = array_shift($data);
if (null === $key) {
return [$key];
}
$data = $data[0];
$parsedData = [];
for ($i = 0, $iMax = count($data); $i < $iMax; $i++) {
for ($j = 0, $jMax = count($data[$i]); $j < $jMax; ++$j) {
if ($data[$i][$j + 1] ?? false) {
$parsedData[$data[$i][$j]] = $data[$i][++$j];
}
}
}
return array_combine([$key], [$parsedData]);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Predis\Command\Traits;
use UnexpectedValueException;
trait Count
{
private $countModifier = 'COUNT';
public function setArguments(array $arguments)
{
$argumentsLength = count($arguments);
if (static::$countArgumentPositionOffset >= $argumentsLength) {
parent::setArguments($arguments);
return;
}
if ($arguments[static::$countArgumentPositionOffset] < 1) {
throw new UnexpectedValueException('Wrong count argument value or position offset');
}
$countArgument = $arguments[static::$countArgumentPositionOffset];
$argumentsBefore = array_slice($arguments, 0, static::$countArgumentPositionOffset);
$argumentsAfter = array_slice($arguments, static::$countArgumentPositionOffset + 1);
parent::setArguments(array_merge(
$argumentsBefore,
[$this->countModifier],
[$countArgument],
$argumentsAfter
));
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Predis\Command\Traits;
use UnexpectedValueException;
trait MinMaxModifier
{
/**
* @var array{string: string}
*/
private $modifierEnum = [
'min' => 'MIN',
'max' => 'MAX',
];
public function resolveModifier(int $offset, array &$arguments): void
{
if ($offset >= count($arguments)) {
$arguments[$offset] = $this->modifierEnum['min'];
return;
}
if (!is_string($arguments[$offset]) || !array_key_exists($arguments[$offset], $this->modifierEnum)) {
throw new UnexpectedValueException('Wrong type of modifier given');
}
$arguments[$offset] = $this->modifierEnum[$arguments[$offset]];
}
}
+209
View File
@@ -0,0 +1,209 @@
<?php
namespace Predis\Command\Redis;
use Predis\Response\ServerException;
use UnexpectedValueException;
class ZMPOP_Test extends PredisCommandTestCase
{
/**
* @inheritDoc
*/
protected function getExpectedCommand(): string
{
return ZMPOP::class;
}
/**
* @inheritDoc
*/
protected function getExpectedId(): string
{
return 'ZMPOP';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group disconnected
* @dataProvider responsesProvider
*/
public function testParseResponse(array $actualResponse, array $expectedResponse): void
{
$this->assertSame($expectedResponse, $this->getCommand()->parseResponse($actualResponse));
}
/**
* @group connected
* @dataProvider sortedSetsProvider
* @param array $sortedSetDictionary
* @param string $key
* @param string $modifier
* @param int $count
* @param array $expectedResponse
* @param array $expectedModifiedSortedSet
* @return void
* @requiresRedisVersion >= 7.0
*/
public function testReturnsPoppedElementsFromGivenSortedSet(
array $sortedSetDictionary,
string $key,
string $modifier,
int $count,
array $expectedResponse,
array $expectedModifiedSortedSet
): void {
$redis = $this->getClient();
$redis->zadd($key, ...$sortedSetDictionary);
$actualResponse = $redis->zmpop([$key], $modifier, $count);
$this->assertSame($expectedResponse, $actualResponse);
$this->assertSame($expectedModifiedSortedSet, $redis->zrange($key, 0, -1));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.0
*/
public function testReturnsPoppedElementsFromFirstNonEmptySortedSet(): void
{
$emptySortedSetKey = 'empty';
$sortedSetKey = 'key';
$sortedSet = [1, 'member1', 2, 'member2', 3, 'member3'];
$redis = $this->getClient();
$redis->zadd($sortedSetKey, ...$sortedSet);
$actualResponse = $redis->zmpop([$emptySortedSetKey, $sortedSetKey]);
$this->assertSame([$sortedSetKey], array_keys($actualResponse));
$this->assertSame(['member2', 'member3'], $redis->zrange($sortedSetKey, 0, -1));
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $keys
* @param string $modifier
* @param int $count
* @param string $expectedExceptionMessage
* @return void
* @requiresRedisVersion >= 7.0
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $keys,
string $modifier,
int $count,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->zmpop($keys, $modifier, $count);
}
/**
* @group connected
* @requiresRedisVersion >= 7.0
*/
public function testThrowsExceptionOnWrongType(): void
{
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Operation against a key holding the wrong kind of value');
$redis = $this->getClient();
$redis->set('zmpop_foo', 'bar');
$redis->zmpop(['zmpop_foo']);
}
public function argumentsProvider(): array
{
return [
'with one key' => [
[['key1'], 'min', 1],
[1, 'key1', 'MIN', 'COUNT', 1]
],
'with multiple keys' => [
[['key1', 'key2', 'key3'], 'max', 1],
[3, 'key1', 'key2', 'key3', 'MAX', 'COUNT', 1]
],
];
}
public function responsesProvider(): array
{
return [
'null-element array' => [
[null],
[null]
],
'two-element array' => [
['key', [['member1', 1, 'member2', 2, 'member3', 3]]],
['key' => ['member1' => 1, 'member2' => 2, 'member3' => 3]]
],
];
}
public function sortedSetsProvider(): array
{
return [
'with MIN modifier' => [
[1, 'member1', 2, 'member2', 3, 'member3'],
'test-zmpop',
'min',
1,
['test-zmpop' => ['member1' => '1']],
['member2', 'member3'],
],
'with MAX modifier' => [
[1, 'member1', 2, 'member2', 3, 'member3'],
'test-zmpop',
'max',
1,
['test-zmpop' => ['member3' => '3']],
['member1', 'member2'],
],
'with non-default COUNT' => [
[1, 'member1', 2, 'member2', 3, 'member3'],
'test-zmpop',
'max',
2,
['test-zmpop' => ['member3' => '3', 'member2' => '2']],
['member1'],
]
];
}
public function unexpectedValuesProvider(): array
{
return [
'wrong modifier' => [
['key1', 'key2'],
'wrong modifier',
1,
'Wrong type of modifier given'
],
'wrong count' => [
['key1', 'key2'],
'min',
0,
'Wrong count argument value or position offset'
],
];
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace Predis\Command\Traits;
use PredisTestCase;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class CountTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use Count;
public static $countArgumentPositionOffset = 2;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $arguments
* @param array $expectedResponse
* @return void
*/
public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void
{
$this->testClass::$countArgumentPositionOffset = $offset;
$this->testClass->setArguments($arguments);
$this->assertSameValues($expectedResponse, $this->testClass->getArguments());
}
public function testThrowsErrorOnWrongCountValue(): void
{
$this->testClass::$countArgumentPositionOffset = 0;
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Wrong count argument value or position offset');
$this->testClass->setArguments([0]);
}
public function argumentsProvider(): array
{
return [
'with count argument' => [
0,
[2],
['COUNT', 2]
],
'without count argument' => [
2,
['argument1', 'argument2'],
['argument1', 'argument2']
]
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace Predis\Command\Traits;
use PredisTestCase;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class MinMaxModifierTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use MinMaxModifier;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testResolveModifierModifyArrayCorrect(
int $offset,
array $actualArguments,
array $expectedArguments
): void {
$this->testClass->resolveModifier($offset, $actualArguments);
$this->assertSame($expectedArguments, $actualArguments);
}
public function testThrowsExceptionOnWrongModifierValue(): void
{
$arguments = ['argument1', 'wrong modifier'];
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Wrong type of modifier given');
$this->testClass->resolveModifier(1, $arguments);
}
public function argumentsProvider(): array
{
return [
'with modifier' => [
0,
['max'],
['MAX']
],
'without modifier' => [
2,
['argument1', 'argument2'],
['argument1', 'argument2', 'MIN']
]
];
}
}