From db65e5ebd352c1148df03a86e8ae06ab18b66c8d Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Sun, 29 Jan 2023 21:38:30 +0200 Subject: [PATCH] Extended core support by implementing SORT_RO command (#1044) * Added support for SORT_RO command * Codestyle fixes * Added command description --------- Co-authored-by: Vladyslav Vildanov --- src/ClientContextInterface.php | 2 + src/ClientInterface.php | 2 + .../Argument/Server/LimitInterface.php | 19 ++ .../Argument/Server/LimitOffsetCount.php | 42 ++++ src/Command/Command.php | 2 +- src/Command/Redis/SINTERCARD.php | 2 +- src/Command/Redis/SORT_RO.php | 74 ++++++ src/Command/Redis/ZINTERCARD.php | 2 +- src/Command/Redis/ZRANGESTORE.php | 2 +- src/Command/Traits/By/ByArgument.php | 40 +++ src/Command/Traits/Get/Get.php | 47 ++++ src/Command/Traits/{ => Limit}/Limit.php | 2 +- src/Command/Traits/Limit/LimitObject.php | 50 ++++ tests/Predis/Command/Redis/SORT_RO_Test.php | 233 ++++++++++++++++++ .../Command/Traits/By/ByArgumentTest.php | 74 ++++++ tests/Predis/Command/Traits/Get/GetTest.php | 86 +++++++ .../Command/Traits/Limit/LimitObjectTest.php | 63 +++++ .../Command/Traits/{ => Limit}/LimitTest.php | 2 +- 18 files changed, 738 insertions(+), 6 deletions(-) create mode 100644 src/Command/Argument/Server/LimitInterface.php create mode 100644 src/Command/Argument/Server/LimitOffsetCount.php create mode 100644 src/Command/Redis/SORT_RO.php create mode 100644 src/Command/Traits/By/ByArgument.php create mode 100644 src/Command/Traits/Get/Get.php rename src/Command/Traits/{ => Limit}/Limit.php (97%) create mode 100644 src/Command/Traits/Limit/LimitObject.php create mode 100644 tests/Predis/Command/Redis/SORT_RO_Test.php create mode 100644 tests/Predis/Command/Traits/By/ByArgumentTest.php create mode 100644 tests/Predis/Command/Traits/Get/GetTest.php create mode 100644 tests/Predis/Command/Traits/Limit/LimitObjectTest.php rename tests/Predis/Command/Traits/{ => Limit}/LimitTest.php (98%) diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index c576334d..e556ab53 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -14,6 +14,7 @@ namespace Predis; use Predis\Command\Argument\Geospatial\ByInterface; use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Server\LimitOffsetCount; use Predis\Command\Argument\Server\To; use Predis\Command\CommandInterface; @@ -39,6 +40,7 @@ use Predis\Command\CommandInterface; * @method $this renamenx($key, $target) * @method $this scan($cursor, array $options = null) * @method $this sort($key, array $options = null) + * @method $this sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false) * @method $this ttl($key) * @method $this type($key) * @method $this append($key, $value) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index b2989274..b3263ee1 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -14,6 +14,7 @@ namespace Predis; use Predis\Command\Argument\Geospatial\ByInterface; use Predis\Command\Argument\Geospatial\FromInterface; +use Predis\Command\Argument\Server\LimitOffsetCount; use Predis\Command\Argument\Server\To; use Predis\Command\CommandInterface; use Predis\Command\FactoryInterface; @@ -48,6 +49,7 @@ use Predis\Response\Status; * @method int renamenx(string $key, string $target) * @method array scan($cursor, array $options = null) * @method array sort(string $key, array $options = null) + * @method array sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false) * @method int ttl(string $key) * @method mixed type(string $key) * @method int append(string $key, $value) diff --git a/src/Command/Argument/Server/LimitInterface.php b/src/Command/Argument/Server/LimitInterface.php new file mode 100644 index 00000000..95ebd643 --- /dev/null +++ b/src/Command/Argument/Server/LimitInterface.php @@ -0,0 +1,19 @@ +offset = $offset; + $this->count = $count; + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [self::KEYWORD, $this->offset, $this->count]; + } +} diff --git a/src/Command/Command.php b/src/Command/Command.php index 9deb0c62..68629c45 100644 --- a/src/Command/Command.php +++ b/src/Command/Command.php @@ -120,7 +120,7 @@ abstract class Command implements CommandInterface public function filterArguments(): void { $this->arguments = array_filter($this->arguments, static function ($argument) { - return $argument !== false; + return $argument !== false && $argument !== null; }); } } diff --git a/src/Command/Redis/SINTERCARD.php b/src/Command/Redis/SINTERCARD.php index acd781cf..f841db36 100644 --- a/src/Command/Redis/SINTERCARD.php +++ b/src/Command/Redis/SINTERCARD.php @@ -14,7 +14,7 @@ namespace Predis\Command\Redis; use Predis\Command\Command as RedisCommand; use Predis\Command\Traits\Keys; -use Predis\Command\Traits\Limit; +use Predis\Command\Traits\Limit\Limit; class SINTERCARD extends RedisCommand { diff --git a/src/Command/Redis/SORT_RO.php b/src/Command/Redis/SORT_RO.php new file mode 100644 index 00000000..f302b6e3 --- /dev/null +++ b/src/Command/Redis/SORT_RO.php @@ -0,0 +1,74 @@ +setSorting($arguments); + $arguments = $this->getArguments(); + + $this->setGetArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimit($arguments); + $arguments = $this->getArguments(); + + $this->setBy($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Redis/ZINTERCARD.php b/src/Command/Redis/ZINTERCARD.php index 52a85fdb..e6b2b450 100644 --- a/src/Command/Redis/ZINTERCARD.php +++ b/src/Command/Redis/ZINTERCARD.php @@ -14,7 +14,7 @@ namespace Predis\Command\Redis; use Predis\Command\Command as RedisCommand; use Predis\Command\Traits\Keys; -use Predis\Command\Traits\Limit; +use Predis\Command\Traits\Limit\Limit; /** * @see https://redis.io/commands/zintercard/ diff --git a/src/Command/Redis/ZRANGESTORE.php b/src/Command/Redis/ZRANGESTORE.php index 992b96ba..4f820b06 100644 --- a/src/Command/Redis/ZRANGESTORE.php +++ b/src/Command/Redis/ZRANGESTORE.php @@ -14,7 +14,7 @@ namespace Predis\Command\Redis; use Predis\Command\Command as RedisCommand; use Predis\Command\Traits\By\ByLexByScore; -use Predis\Command\Traits\Limit; +use Predis\Command\Traits\Limit\Limit; use Predis\Command\Traits\Rev; /** diff --git a/src/Command/Traits/By/ByArgument.php b/src/Command/Traits/By/ByArgument.php new file mode 100644 index 00000000..99bd1722 --- /dev/null +++ b/src/Command/Traits/By/ByArgument.php @@ -0,0 +1,40 @@ += $argumentsLength || null === $arguments[static::$byArgumentPositionOffset]) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$byArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$byArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$this->byModifier, $argument], $argumentsAfter)); + } +} diff --git a/src/Command/Traits/Get/Get.php b/src/Command/Traits/Get/Get.php new file mode 100644 index 00000000..256676e9 --- /dev/null +++ b/src/Command/Traits/Get/Get.php @@ -0,0 +1,47 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if (!is_array($arguments[static::$getArgumentPositionOffset])) { + throw new UnexpectedValueException('Wrong get argument type'); + } + + $patterns = []; + + foreach ($arguments[static::$getArgumentPositionOffset] as $pattern) { + $patterns[] = self::$getModifier; + $patterns[] = $pattern; + } + + $argumentsBeforeKeys = array_slice($arguments, 0, static::$getArgumentPositionOffset); + $argumentsAfterKeys = array_slice($arguments, static::$getArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBeforeKeys, $patterns, $argumentsAfterKeys)); + } +} diff --git a/src/Command/Traits/Limit.php b/src/Command/Traits/Limit/Limit.php similarity index 97% rename from src/Command/Traits/Limit.php rename to src/Command/Traits/Limit/Limit.php index e3477770..e2449947 100644 --- a/src/Command/Traits/Limit.php +++ b/src/Command/Traits/Limit/Limit.php @@ -10,7 +10,7 @@ * file that was distributed with this source code. */ -namespace Predis\Command\Traits; +namespace Predis\Command\Traits\Limit; use Predis\Command\Command; use UnexpectedValueException; diff --git a/src/Command/Traits/Limit/LimitObject.php b/src/Command/Traits/Limit/LimitObject.php new file mode 100644 index 00000000..3e47de9a --- /dev/null +++ b/src/Command/Traits/Limit/LimitObject.php @@ -0,0 +1,50 @@ +getLimitArgumentPositionOffset($arguments); + + if (null === $argumentPositionOffset) { + parent::setArguments($arguments); + + return; + } + + $limitObject = $arguments[$argumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset); + $argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + $limitObject->toArray(), + $argumentsAfter + )); + } + + private function getLimitArgumentPositionOffset(array $arguments): ?int + { + foreach ($arguments as $i => $value) { + if ($value instanceof LimitInterface) { + return $i; + } + } + + return null; + } +} diff --git a/tests/Predis/Command/Redis/SORT_RO_Test.php b/tests/Predis/Command/Redis/SORT_RO_Test.php new file mode 100644 index 00000000..dc15d347 --- /dev/null +++ b/tests/Predis/Command/Redis/SORT_RO_Test.php @@ -0,0 +1,233 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse(): void + { + $this->assertSame(1, $this->getCommand()->parseResponse(1)); + } + + /** + * @group connected + * @dataProvider listProvider + * @param array $listArguments + * @param array $sortArguments + * @param array $expectedSortedResponse + * @return void + * @requiresRedisVersion >= 7.0.0 + */ + public function testSortItemsWithinGivenList( + array $listArguments, + array $sortArguments, + array $expectedSortedResponse + ): void { + $redis = $this->getClient(); + + $redis->lpush(...$listArguments); + + $this->assertSame($expectedSortedResponse, $redis->sort_ro(...$sortArguments)); + } + + /** + * @group connected + * @dataProvider listsProvider + * @param array $localKeys + * @param array $externalKeys + * @param array $sortArguments + * @param array $expectedSortedResponse + * @return void + * @requiresRedisVersion >= 7.0.0 + */ + public function testSortItemsWithExternalKeysWithinGivenList( + array $localKeys, + array $externalKeys, + array $sortArguments, + array $expectedSortedResponse + ): void { + $redis = $this->getClient(); + + $redis->lpush(...$localKeys); + $redis->mset(...$externalKeys); + + $this->assertSame($expectedSortedResponse, $redis->sort_ro(...$sortArguments)); + } + + /** + * @group connected + * @dataProvider unexpectedValuesProvider + * @param array $arguments + * @param string $expectedExceptionMessage + * @return void + * @requiresRedisVersion >= 7.0.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven( + array $arguments, + string $expectedExceptionMessage + ): void { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $redis->sort_ro(...$arguments); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key'], + ['key'], + ], + 'with BY argument' => [ + ['key', 'pattern'], + ['key', 'BY', 'pattern'], + ], + 'with LIMIT argument' => [ + ['key', null, new LimitOffsetCount(0, 1)], + ['key', 'LIMIT', 0, 1], + ], + 'with GET patterns' => [ + ['key', null, null, ['pattern1', 'pattern2']], + ['key', 'GET', 'pattern1', 'GET', 'pattern2'], + ], + 'with sorting argument - ASC' => [ + ['key', null, null, [], 'asc'], + ['key', 'ASC'], + ], + 'with sorting argument - DESC' => [ + ['key', null, null, [], 'desc'], + ['key', 'DESC'], + ], + 'with ALPHA argument' => [ + ['key', null, null, [], null, true], + ['key', 'ALPHA'], + ], + 'with all arguments argument' => [ + ['key', 'pattern', new LimitOffsetCount(0, 1), ['pattern1', 'pattern2'], 'asc', true], + ['key', 'BY', 'pattern', 'LIMIT', 0, 1, 'GET', 'pattern1', 'GET', 'pattern2', 'ASC', 'ALPHA'], + ], + ]; + } + + public function listProvider(): array + { + return [ + 'without any modifiers' => [ + ['key', 2, 1], + ['key', null, null, [], null, false], + ['1', '2'], + ], + 'with LIMIT modifier' => [ + ['key', 2, 1, 4, 15, 3, 36], + ['key', null, new LimitOffsetCount(1, 2), [], null, false], + ['2', '3'], + ], + 'with sorting - ASC' => [ + ['key', 2, 1], + ['key', null, null, [], 'asc', false], + ['1', '2'], + ], + 'with sorting - DESC' => [ + ['key', 2, 1], + ['key', null, null, [], 'desc', false], + ['2', '1'], + ], + 'with sorting lexicographically' => [ + ['key', 'abc', 'aab', 'abb'], + ['key', null, null, [], null, true], + ['aab', 'abb', 'abc'], + ], + 'with all arguments for single list' => [ + ['key', 'abc', 'aab', 'abb'], + ['key', null, new LimitOffsetCount(0, 2), [], 'desc', true], + ['abc', 'abb'], + ], + ]; + } + + public function listsProvider(): array + { + return [ + 'sorted by external keys - returns local keys' => [ + ['uid', 1, 2, 3, 4, 5], + ['points_1', 500, 'points_2', 200, 'points_3', 300, 'points_4', 400, 'points_5', 100], + ['uid', 'points_*', null, [], null, false], + ['5', '2', '3', '4', '1'], + ], + 'sorted by external keys - returns external keys' => [ + ['uid', 1, 2, 3, 4, 5], + [ + 'points_1', 500, 'points_2', 200, 'points_3', 300, 'points_4', 400, 'points_5', 100, + 'user_1', 'User1', 'user_2', 'User2', 'user_3', 'User3', 'user_4', 'User4', 'user_5', 'User5', + ], + ['uid', 'points_*', null, ['user_*'], null, false], + ['User5', 'User2', 'User3', 'User4', 'User1'], + ], + ]; + } + + public function unexpectedValuesProvider(): array + { + return [ + 'wrong GET argument type' => [ + ['key', null, null, 'wrong', null, false], + 'Wrong get argument type', + ], + 'wrong sorting argument type' => [ + ['key', null, null, [], 'wrong', false], + 'Sorting argument accepts only: asc, desc values', + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/By/ByArgumentTest.php b/tests/Predis/Command/Traits/By/ByArgumentTest.php new file mode 100644 index 00000000..0fac6ea8 --- /dev/null +++ b/tests/Predis/Command/Traits/By/ByArgumentTest.php @@ -0,0 +1,74 @@ +testClass = new class() extends RedisCommand { + use ByArgument; + + public static $byArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedArguments + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void + { + $this->testClass::$byArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + ['key'], + ['key'], + ], + 'with null value' => [ + 0, + [null], + [null], + ], + 'with any value' => [ + 0, + ['value'], + ['BY', 'value'], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Get/GetTest.php b/tests/Predis/Command/Traits/Get/GetTest.php new file mode 100644 index 00000000..7bf5c839 --- /dev/null +++ b/tests/Predis/Command/Traits/Get/GetTest.php @@ -0,0 +1,86 @@ +testClass = new class() extends RedisCommand { + use Get; + + public static $getArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $actualArguments + * @param array $expectedArguments + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void + { + $this->testClass::$getArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValue(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong get argument type'); + + $this->testClass->setArguments(['wrong']); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + ['value'], + ['value'], + ], + 'with single value' => [ + 0, + [['value']], + ['GET', 'value'], + ], + 'with multiple values' => [ + 0, + [['value1', 'value2']], + ['GET', 'value1', 'GET', 'value2'], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/Limit/LimitObjectTest.php b/tests/Predis/Command/Traits/Limit/LimitObjectTest.php new file mode 100644 index 00000000..2a1779fd --- /dev/null +++ b/tests/Predis/Command/Traits/Limit/LimitObjectTest.php @@ -0,0 +1,63 @@ +testClass = new class() extends RedisCommand { + use LimitObject; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param array $actualArguments + * @param array $expectedArguments + * @return void + */ + public function testReturnsCorrectArguments(array $actualArguments, array $expectedArguments): void + { + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + public function argumentsProvider(): array + { + return [ + 'with non-existing LimitInterface' => [ + ['value'], + ['value'], + ], + 'with existing LimitInterface' => [ + [new LimitOffsetCount(0, 1)], + ['LIMIT', 0, 1], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/LimitTest.php b/tests/Predis/Command/Traits/Limit/LimitTest.php similarity index 98% rename from tests/Predis/Command/Traits/LimitTest.php rename to tests/Predis/Command/Traits/Limit/LimitTest.php index fb15a025..2fd9346c 100644 --- a/tests/Predis/Command/Traits/LimitTest.php +++ b/tests/Predis/Command/Traits/Limit/LimitTest.php @@ -10,7 +10,7 @@ * file that was distributed with this source code. */ -namespace Predis\Command\Traits; +namespace Predis\Command\Traits\Limit; use Predis\Command\Command as RedisCommand; use PredisTestCase;