diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index c4defbcb..c6d9d5b9 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -119,6 +119,7 @@ use Predis\Command\CommandInterface; * @method $this zrandmember(string $key, int $count = 1, bool $withScores = false) * @method $this zrange($key, $start, $stop, array $options = null) * @method $this zrangebyscore($key, $min, $max, array $options = null) + * @method $this zrangestore(string $destination, string $source, int|string $min, string|int $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0) * @method $this zrank($key, $member) * @method $this zrem($key, $member) * @method $this zremrangebyrank($key, $start, $stop) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index ca28f8e7..f6aca9fd 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -137,6 +137,7 @@ use Predis\Response\Status; * @method mixed zrandmember(string $key, int $count = 1, bool $withScores = false) * @method array zrange(string $key, int|string $start, int|string $stop, array $options = null) * @method array zrangebyscore(string $key, int|string $min, int|string $max, array $options = null) + * @method int zrangestore(string $destination, string $source, int|string $min, int|string $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0) * @method int|null zrank(string $key, string $member) * @method int zrem(string $key, string ...$member) * @method int zremrangebyrank(string $key, int|string $start, int|string $stop) diff --git a/src/Command/Command.php b/src/Command/Command.php index 77e9fff3..dfd84004 100644 --- a/src/Command/Command.php +++ b/src/Command/Command.php @@ -114,4 +114,16 @@ abstract class Command implements CommandInterface return $arguments; } + + /** + * Remove all false values from arguments. + * + * @return void + */ + public function filterArguments(): void + { + $this->arguments = array_filter($this->arguments, static function ($argument) { + return $argument !== false; + }); + } } diff --git a/src/Command/Redis/ZRANGESTORE.php b/src/Command/Redis/ZRANGESTORE.php new file mode 100644 index 00000000..9e1f8748 --- /dev/null +++ b/src/Command/Redis/ZRANGESTORE.php @@ -0,0 +1,47 @@ +setByLexByScoreArgument($arguments); + $arguments = $this->getArguments(); + + $this->setReversedArgument($arguments); + $arguments = $this->getArguments(); + + $this->setLimitArguments($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Traits/ByLexByScore.php b/src/Command/Traits/ByLexByScore.php new file mode 100644 index 00000000..94a37414 --- /dev/null +++ b/src/Command/Traits/ByLexByScore.php @@ -0,0 +1,34 @@ + 'BYLEX', + 'byscore' => 'BYSCORE', + ]; + + public function setArguments(array $arguments) + { + $argument = $arguments[static::$byLexByScoreArgumentPositionOffset]; + + if (false === $argument) { + parent::setArguments($arguments); + return; + } + + if (is_string($argument) && in_array(strtoupper($argument), self::$argumentsEnum)) { + $argument = self::$argumentsEnum[$argument]; + } else { + throw new UnexpectedValueException("By argument accepts only \"bylex\" and \"byscore\" values"); + } + + $argumentsBefore = array_slice($arguments, 0, static::$byLexByScoreArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$byLexByScoreArgumentPositionOffset + 1); + + parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter)); + } +} diff --git a/src/Command/Traits/Limit.php b/src/Command/Traits/Limit.php new file mode 100644 index 00000000..ee17af21 --- /dev/null +++ b/src/Command/Traits/Limit.php @@ -0,0 +1,29 @@ +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 rangesProvider + * @param array $actualSortedSet + * @param int|string $min + * @param int|string $max + * @param string|bool $by + * @param bool $rev + * @param bool $limit + * @param int $offset + * @param int $count + * @param int $expectedResultingElements + * @param array $expectedResponse + * @requiresRedisVersion >= 6.2.0 + * @return void + */ + public function testStoresSortedSetRanges( + array $actualSortedSet, + $min, + $max, + $by, + bool $rev, + bool $limit, + int $offset, + int $count, + int $expectedResultingElements, + array $expectedResponse + ): void { + $redis = $this->getClient(); + + $redis->zadd('source', ...$actualSortedSet); + $actualResponse = $redis->zrangestore( + 'destination', + 'source', + $min, + $max, + $by, + $rev, + $limit, + $offset, + $count + ); + + $this->assertSame($expectedResultingElements, $actualResponse); + $this->assertSame($expectedResponse, $redis->zrange('destination', 0, -1)); + } + + /** + * @group connected + * @dataProvider unexpectedValuesProvider + * @param int|string $min + * @param int|string $max + * @param string|bool $by + * @param $rev + * @param $limit + * @param int $offset + * @param int $count + * @param string $expectedExceptionMessage + * @return void + * @requiresRedisVersion >= 6.2.0 + */ + public function testThrowsExceptionOnUnexpectedValuesGiven( + $min, + $max, + $by, + $rev, + $limit, + int $offset, + int $count, + string $expectedExceptionMessage + ): void { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $redis->zrangestore( + 'destination', + 'source', + $min, + $max, + $by, + $rev, + $limit, + $offset, + $count + ); + } + + public function argumentsProvider(): array + { + return [ + 'without optional arguments' => [ + ['destination', 'source', 0, -1, false, false, false, 0, 0], + ['destination', 'source', 0, -1], + ], + 'with BYLEX argument' => [ + ['destination', 'source', 0, -1, 'bylex', false, false, 0, 0], + ['destination', 'source', 0, -1, 'BYLEX'], + ], + 'with BYSCORE argument' => [ + ['destination', 'source', 0, -1, 'byscore', false, false, 0, 0], + ['destination', 'source', 0, -1, 'BYSCORE'], + ], + 'with REV argument' => [ + ['destination', 'source', 0, -1, false, true, false, 0, 0], + ['destination', 'source', 0, -1, 'REV'], + ], + 'with BYSCORE/BYLEX and LIMIT argument' => [ + ['destination', 'source', 0, -1, 'byscore', false, true, 0, 1], + ['destination', 'source', 0, -1, 'BYSCORE', 'LIMIT', 0, 1], + ], + 'with BYSCORE/BYLEX argument and REV argument' => [ + ['destination', 'source', 0, -1, 'byscore', true, false, 0, 0], + ['destination', 'source', 0, -1, 'BYSCORE', 'REV'], + ], + 'with BYSCORE/BYLEX argument, REV argument and LIMIT' => [ + ['destination', 'source', 0, -1, 'bylex', true, true, 0, 1], + ['destination', 'source', 0, -1, 'BYLEX', 'REV', 'LIMIT', 0, 1], + ] + ]; + } + + public function rangesProvider(): array + { + return [ + 'without optional arguments' => [ + [1, 'member1', 2, 'member2', 3, 'member3'], + 0, + -1, + false, + false, + false, + 0, + 0, + 3, + ['member1', 'member2', 'member3'], + ], + 'with BYLEX argument' => [ + [1, 'abc', 1, 'abb', 1, 'aaa'], + '[aaa', + '[abc', + 'bylex', + false, + false, + 0, + 0, + 3, + ['aaa', 'abb', 'abc'], + ], + 'with BYSCORE argument' => [ + [3, 'member1', 2, 'member2', 1, 'member3'], + '1', + '(4', + 'byscore', + false, + false, + 0, + 0, + 3, + ['member3', 'member2', 'member1'], + ], + 'with REV argument' => [ + [3, 'member1', 2, 'member2', 1, 'member3'], + 0, + 2, + false, + true, + false, + 0, + 0, + 3, + ['member3', 'member2', 'member1'], + ], + 'with BYSCORE/BYLEX and LIMIT argument' => [ + [1, 'member1', 2, 'member2', 3, 'member3'], + '1', + '(4', + 'byscore', + false, + true, + 0, + 1, + 1, + ['member1'], + ], + 'with BYSCORE/BYLEX argument and REV argument' => [ + [3, 'member1', 2, 'member2', 1, 'member3'], + 3, + 0, + 'byscore', + true, + false, + 0, + 0, + 3, + ['member3', 'member2', 'member1'], + ], + 'with BYSCORE/BYLEX argument, REV argument and LIMIT' => [ + [3, 'member1', 2, 'member2', 1, 'member3'], + 3, + 0, + 'byscore', + true, + true, + 0, + 2, + 2, + ['member2', 'member1'], + ], + ]; + } + + public function unexpectedValuesProvider(): array + { + return [ + 'wrong BY argument value' => [ + 0, -1, 'wrong value', false, false, 0, 0, "By argument accepts only \"bylex\" and \"byscore\" values", + ], + 'wrong REV argument type' => [ + 0, -1, false, 'wrong value', false, 0, 0, 'Wrong rev argument type', + ], + 'wrong LIMIT argument type' => [ + 0, -1, false, false, 'wrong value', 0, 0, 'Wrong limit argument type' + ] + ]; + } +} diff --git a/tests/Predis/Command/Traits/ByLexByScoreTest.php b/tests/Predis/Command/Traits/ByLexByScoreTest.php new file mode 100644 index 00000000..6ca51e20 --- /dev/null +++ b/tests/Predis/Command/Traits/ByLexByScoreTest.php @@ -0,0 +1,93 @@ +testClass = new class extends RedisCommand { + use ByLexByScore; + + public static $byLexByScoreArgumentPositionOffset = 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::$byLexByScoreArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + /** + * @dataProvider unexpectedValuesProvider + * @param array $actualArguments + * @return void + */ + public function testThrowsExceptionOnUnexpectedValue(array $actualArguments): void + { + $this->testClass::$byLexByScoreArgumentPositionOffset = 0; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage("By argument accepts only \"bylex\" and \"byscore\" values"); + + $this->testClass->setArguments($actualArguments); + } + + public function argumentsProvider(): array + { + return [ + 'by false argument' => [ + 0, + [false, 'second argument', 'third argument'], + [false, 'second argument', 'third argument'] + ], + 'by argument first and there is arguments after' => [ + 0, + ['bylex', 'second argument', 'third argument'], + ['BYLEX', 'second argument', 'third argument'] + ], + 'by argument last and there is arguments before' => [ + 2, + ['first argument', 'second argument', 'byscore'], + ['first argument', 'second argument', 'BYSCORE'] + ], + 'by argument not the first and not the last' => [ + 1, + ['first argument', 'byscore', 'third argument'], + ['first argument', 'BYSCORE', 'third argument'] + ], + ]; + } + + public function unexpectedValuesProvider(): array + { + return [ + 'true argument' => [[true]], + 'string argument, not BYLEX/BYSCORE' => [['wrong argument']] + ]; + } +} diff --git a/tests/Predis/Command/Traits/LimitTest.php b/tests/Predis/Command/Traits/LimitTest.php new file mode 100644 index 00000000..c40d7d12 --- /dev/null +++ b/tests/Predis/Command/Traits/LimitTest.php @@ -0,0 +1,93 @@ +testClass = new class extends RedisCommand { + use Limit; + + public static $limitArgumentPositionOffset = 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::$limitArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValue(): void + { + $this->testClass::$limitArgumentPositionOffset = 0; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage("Wrong limit argument type"); + + $this->testClass->setArguments(['test']); + } + + public function argumentsProvider(): array + { + return [ + 'limit false argument first and there is arguments after' => [ + 0, + [false, 'second argument', 'third argument'], + [] + ], + 'limit false argument last and there is arguments before' => [ + 2, + ['first argument', 'second argument', false], + ['first argument', 'second argument'], + ], + 'limit false argument not the first and not the last' => [ + 1, + ['first argument', false, 'third argument'], + ['first argument'], + ], + 'limit argument first and there is arguments after' => [ + 0, + [true, 'second argument', 'third argument'], + ['LIMIT', 'second argument', 'third argument'] + ], + 'limit argument last and there is arguments before' => [ + 2, + ['first argument', 'second argument', true], + ['first argument', 'second argument', 'LIMIT'] + ], + 'limit argument not the first and not the last' => [ + 1, + ['first argument', true, 'third argument'], + ['first argument', 'LIMIT', 'third argument'] + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/RevTest.php b/tests/Predis/Command/Traits/RevTest.php new file mode 100644 index 00000000..668b77c6 --- /dev/null +++ b/tests/Predis/Command/Traits/RevTest.php @@ -0,0 +1,83 @@ +testClass = new class extends RedisCommand { + use Rev; + + public static $revArgumentPositionOffset = 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::$revArgumentPositionOffset = $offset; + + $this->testClass->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsExceptionOnUnexpectedValue(): void + { + $this->testClass::$revArgumentPositionOffset = 0; + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage("Wrong rev argument type"); + + $this->testClass->setArguments(['test']); + } + + public function argumentsProvider(): array + { + return [ + 'rev false argument' => [ + 0, + [false, 'second argument', 'third argument'], + [false, 'second argument', 'third argument'] + ], + 'rev argument first and there is arguments after' => [ + 0, + [true, 'second argument', 'third argument'], + ['REV', 'second argument', 'third argument'] + ], + 'rev argument last and there is arguments before' => [ + 2, + ['first argument', 'second argument', true], + ['first argument', 'second argument', 'REV'] + ], + 'rev argument not the first and not the last' => [ + 1, + ['first argument', true, 'third argument'], + ['first argument', 'REV', 'third argument'] + ], + ]; + } +}