From 3f377e292c20925efc38675599268eafa41fa07d Mon Sep 17 00:00:00 2001 From: MacFJA Date: Sun, 27 Mar 2022 14:34:53 +0200 Subject: [PATCH] Improve paginated response Rework paginated response (close #31) Throw exception is client is not defined in Paginated response (close #30) Increase the code coverage --- .gitignore | 5 +- CHANGELOG.md | 13 ++ docs/BuildQuery.md | 27 +++ docs/UsingResult.md | 70 +++++++ src/Exception/MissingClientException.php | 33 +++ src/Redis/Client/AmpRedisClient.php | 6 + src/Redis/Client/CheprasovRedisClient.php | 3 + src/Redis/Client/CredisClient.php | 3 + src/Redis/Client/PredisClient.php | 3 + src/Redis/Client/RedisentClient.php | 3 + .../Rediska/RediskaRediSearchCommand.php | 3 + src/Redis/Client/RediskaClient.php | 3 + src/Redis/Client/TinyRedisClient.php | 3 + src/Redis/Response/AggregateResponseItem.php | 18 +- src/Redis/Response/ArrayResponseTrait.php | 7 +- src/Redis/Response/ClientAware.php | 31 +++ src/Redis/Response/ClientAwareTrait.php | 42 ++++ src/Redis/Response/CursorResponse.php | 27 +-- src/Redis/Response/PaginatedResponse.php | 31 ++- src/Redis/Response/ResponseItem.php | 37 ++++ src/Redis/Response/SearchResponseItem.php | 2 +- tests/Redis/Command/CursorReadTest.php | 37 ++++ tests/Redis/Command/IndexListTest.php | 9 + .../Option/CustomValidatorOptionTest.php | 19 ++ .../Redis/Response/ArrayResponseTraitTest.php | 118 +++++++++++ tests/Redis/Response/InfoResponseTest.php | 54 +++++ .../Redis/Response/PaginatedResponseTest.php | 190 ++++++++++++++++++ .../Redis/Command/FakePaginatedCommand.php | 62 ++++++ .../Response/ArrayResponseTraitTester.php | 69 +++++++ tests/integration/DockerTest.php | 17 +- 30 files changed, 900 insertions(+), 45 deletions(-) create mode 100644 docs/BuildQuery.md create mode 100644 docs/UsingResult.md create mode 100644 src/Exception/MissingClientException.php create mode 100644 src/Redis/Response/ClientAware.php create mode 100644 src/Redis/Response/ClientAwareTrait.php create mode 100644 src/Redis/Response/ResponseItem.php create mode 100644 tests/Redis/Response/ArrayResponseTraitTest.php create mode 100644 tests/Redis/Response/PaginatedResponseTest.php create mode 100644 tests/fixtures/Redis/Command/FakePaginatedCommand.php create mode 100644 tests/fixtures/Redis/Response/ArrayResponseTraitTester.php diff --git a/.gitignore b/.gitignore index 7e3383a..48607c0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,5 @@ ### PHPUnit /.phpunit.result.cache -### Documentation -/docs - ### Local test -/test*.php \ No newline at end of file +/test*.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd63a7..6ee8c83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased v2.x] +### Added + +- Documentation on paginated responses +- Throw custom exception is client is missing + +### Changed + +- Rework paginated responses + +### Deprecated + +- `\MacFJA\RediSearch\Redis\Response\AggregateResponseItem::getValue` (use `\MacFJA\RediSearch\Redis\Response\AggregateResponseItem::getFieldValue` instead) + ## [2.1.2] ### Fixed diff --git a/docs/BuildQuery.md b/docs/BuildQuery.md new file mode 100644 index 0000000..f525e59 --- /dev/null +++ b/docs/BuildQuery.md @@ -0,0 +1,27 @@ +# Building a Query + +To help create RediSearch, the library offer a builder. + +It all start with the class `\MacFJA\RediSearch\Query\Builder`. From there you can add groups and query elements. + +--- + +For example, for search all people named `John`, and preferably with the last name `Doe` that work as an accountant in the 10 km around Washington DC, and the resume contain text `cake` the request will be: +```php +use MacFJA\RediSearch\Query\Builder; +use MacFJA\RediSearch\Query\Builder\GeoFacet; +use MacFJA\RediSearch\Query\Builder\TextFacet; +use MacFJA\RediSearch\Query\Builder\Optional; +use MacFJA\RediSearch\Redis\Command\SearchCommand\GeoFilterOption; + +$queryBuilder = new Builder(); +$query = $queryBuilder + ->addTextFacet('firstname', 'John') // people named `John` + ->addElement(new Optional(new TextFacet(['lastname'], 'Doe'))) // preferably with the last name `Doe` + ->addTextFacet('job', 'accountant') // work as an accountant + ->addGeoFacet('address', -77.0365427, 38.8950368, 10, GeoFilterOption::UNIT_KILOMETERS) // in the 10 km around Washington DC + ->addString('cake') // the resume contain text `cake` + ->render(); + +// @firstname:John ~@lastname:Doe @job:accountant @address:[-77.0365427 38.8950368 10 km] cake +``` diff --git a/docs/UsingResult.md b/docs/UsingResult.md new file mode 100644 index 0000000..e590606 --- /dev/null +++ b/docs/UsingResult.md @@ -0,0 +1,70 @@ +# Using result + +As you can set an offset and limit on `FT.SEARCH` and `FT.AGGREGATE`, they are paginated commands. +The result of those commands is a page, that can contain one or more document. + +--- + +So in this library, the result of `\MacFJA\RediSearch\Redis\Command\Search` and `\MacFJA\RediSearch\Redis\Command\Aggregate` is also a paginated object: +either a `\MacFJA\RediSearch\Redis\Response\PaginatedResponse` (for Search and Aggregate) or a `\MacFJA\RediSearch\Redis\Response\CursorResponse` (for Aggregate) + +The 2 classes are representing a page. + +Both `\MacFJA\RediSearch\Redis\Response\PaginatedResponse` and `\MacFJA\RediSearch\Redis\Response\CursorResponse` can be read inside a `foreach` loop or with any iterator function. +(As they implement the [`\Iterator` interface](https://www.php.net/manual/en/class.iterator.php)) +What you are iterating over, are the pages (not the documents inside the page). + +```php +// If your RediSearch search have 15 results in total, but you set the pagination to 10 per page: +/** @var PaginatedResponse $results */ + +/** @var array $items */ +$items = $results->current(); +// $items is a list of 10 SearchResponseItem + +$results->next(); // To be able to class `->next()`, you need to call `->setClient()` first ! + +/** @var array $items */ +$items = $results->current(); +// $items is a list of 5 SearchResponseItem +``` + +## Get all items on all pages + +```php +/** @var \MacFJA\RediSearch\Redis\Client $client */ +/** @var \MacFJA\RediSearch\Redis\Command\Search $search */ + +/** @var \MacFJA\RediSearch\Redis\Response\PaginatedResponse $results */ +$results = $client->execute($search); +$results->setClient($client); + +$items = []; +foreach ($results as $pageIndex => $pageContent) { + $items = array_merge($items, $pageContent); +} +/** @var \MacFJA\RediSearch\Redis\Response\SearchResponseItem $item */ +foreach ($items as $item) { + doSomething($item); +} +``` +Be careful, this will load all items in the memory. +This will also call Redis multiple time if there are several pages. + +--- + +If you need to avoid loading all items in the memory, you can use [`ResponseItemIterator`](https://github.com/MacFJA/php-redisearch-integration/blob/main/src/Iterator/ResponseItemIterator.php) from the sister project [macfja/redisearch-integration](https://github.com/MacFJA/php-redisearch-integration). +This class it a custom iterator that will load in memory only one page at the time. + +```php +/** @var \MacFJA\RediSearch\Redis\Client $client */ +/** @var \MacFJA\RediSearch\Redis\Command\Search $search */ + +$results = $client->execute($search); +$allItems = new \MacFJA\RediSearch\Integration\Iterator\ResponseItemIterator($results, $client); +/** @var \MacFJA\RediSearch\Redis\Response\SearchResponseItem $item */ +foreach ($allItems as $item) { + // Only one page exist in the memory + doSomething($item); +} +``` diff --git a/src/Exception/MissingClientException.php b/src/Exception/MissingClientException.php new file mode 100644 index 0000000..498cbd1 --- /dev/null +++ b/src/Exception/MissingClientException.php @@ -0,0 +1,33 @@ +|float|int|string> */ private $fields = []; @@ -49,8 +49,24 @@ public function getFields(): array * @param null|array|float|int|string $default * * @return null|array|float|int|string + * + * @deprecated Use ::getFieldValue instead + * @codeCoverageIgnore + * @psalm-suppress + * @SuppressWarnings */ public function getValue(string $fieldName, $default = null) + { + trigger_error(sprintf( + 'Method %s is deprecated from version 2.2.0, use %s::getFieldValue instead', + __METHOD__, + __CLASS__ + ), E_USER_DEPRECATED); + // @phpstan-ignore-next-line + return $this->getFieldValue($fieldName, $default); + } + + public function getFieldValue(string $fieldName, $default = null) { return $this->fields[$fieldName] ?? $default; } diff --git a/src/Redis/Response/ArrayResponseTrait.php b/src/Redis/Response/ArrayResponseTrait.php index ed6f916..d5d1a20 100644 --- a/src/Redis/Response/ArrayResponseTrait.php +++ b/src/Redis/Response/ArrayResponseTrait.php @@ -86,9 +86,12 @@ private static function getNextValue(array $notKeyedArray, string $key) */ private static function getKeys(array $notKeyedArray): array { - $keys = array_filter(array_values($notKeyedArray), static function (int $key): bool { + if (count($notKeyedArray) % 2 > 0) { + array_pop($notKeyedArray); + } + $keys = array_values(array_filter(array_values($notKeyedArray), static function (int $key): bool { return 0 === $key % 2; - }, ARRAY_FILTER_USE_KEY); + }, ARRAY_FILTER_USE_KEY)); return array_map(static function ($key): string { return (string) $key; diff --git a/src/Redis/Response/ClientAware.php b/src/Redis/Response/ClientAware.php new file mode 100644 index 0000000..d221e52 --- /dev/null +++ b/src/Redis/Response/ClientAware.php @@ -0,0 +1,31 @@ +client; + } + + public function setClient(Client $client): ClientAware + { + $this->client = $client; + + return $this; + } +} diff --git a/src/Redis/Response/CursorResponse.php b/src/Redis/Response/CursorResponse.php index a477a46..7bbdc26 100644 --- a/src/Redis/Response/CursorResponse.php +++ b/src/Redis/Response/CursorResponse.php @@ -22,7 +22,9 @@ namespace MacFJA\RediSearch\Redis\Response; use function count; +use Countable; use Iterator; +use MacFJA\RediSearch\Exception\MissingClientException; use MacFJA\RediSearch\Redis\Client; use MacFJA\RediSearch\Redis\Command\AbstractCommand; use MacFJA\RediSearch\Redis\Command\CursorRead; @@ -31,17 +33,16 @@ /** * @implements Iterator */ -class CursorResponse implements Response, Iterator +class CursorResponse implements Response, Iterator, Countable, ClientAware { + use ClientAwareTrait; + /** @var array */ private $items; /** @var int */ private $totalCount; - /** @var Client */ - private $client; - /** @var bool */ private $doNext = false; @@ -73,26 +74,23 @@ public function __construct(int $cursorId, int $totalCount, array $items, int $s $this->cursorId = $cursorId; } - public function setClient(Client $client): self - { - $this->client = $client; - - return $this; - } - /** * @return array */ public function current() { if ($this->doNext) { + if (!($this->getClient() instanceof Client)) { + throw new MissingClientException(); + } + $cursorRead = new CursorRead($this->redisVersion); $cursorRead->setIndex($this->index); $cursorRead->setCursorId($this->cursorId); $cursorRead->setCount($this->getPageSize()); /** @var CursorResponse $next */ - $next = $this->client->execute($cursorRead); + $next = $this->getClient()->execute($cursorRead); $this->cursorId = $next->cursorId; $this->offset += count($this->items); $this->items = $next->items; @@ -145,4 +143,9 @@ public function getTotalCount(): int { return $this->totalCount; } + + public function count() + { + return $this->getPageCount(); + } } diff --git a/src/Redis/Response/PaginatedResponse.php b/src/Redis/Response/PaginatedResponse.php index d139526..945c8dc 100644 --- a/src/Redis/Response/PaginatedResponse.php +++ b/src/Redis/Response/PaginatedResponse.php @@ -25,16 +25,19 @@ use Countable; use function is_int; use Iterator; +use MacFJA\RediSearch\Exception\MissingClientException; use MacFJA\RediSearch\Redis\Client; use MacFJA\RediSearch\Redis\Command\PaginatedCommand; use MacFJA\RediSearch\Redis\Response; /** - * @implements Iterator + * @implements Iterator */ -class PaginatedResponse implements Response, Iterator, Countable +class PaginatedResponse implements Response, Iterator, Countable, ClientAware { - /** @var array|array */ + use ClientAwareTrait; + + /** @var array */ private $items; /** @var PaginatedCommand */ @@ -43,9 +46,6 @@ class PaginatedResponse implements Response, Iterator, Countable /** @var int */ private $totalCount; - /** @var Client */ - private $client; - /** @var null|int */ private $requestedOffset; @@ -53,7 +53,7 @@ class PaginatedResponse implements Response, Iterator, Countable private $requestedSize; /** - * @param AggregateResponseItem[]|SearchResponseItem[] $items + * @param ResponseItem[] $items */ public function __construct(PaginatedCommand $command, int $totalCount, array $items) { @@ -62,15 +62,8 @@ public function __construct(PaginatedCommand $command, int $totalCount, array $i $this->lastCommand = $command; } - public function setClient(Client $client): PaginatedResponse - { - $this->client = $client; - - return $this; - } - /** - * @return array|array + * @return array */ public function current() { @@ -141,17 +134,21 @@ public function getTotalCount(): int public function count() { - return $this->getTotalCount(); + return $this->getPageCount(); } private function updateWithLimit(int $offset, int $size): void { + if (!($this->getClient() instanceof Client)) { + throw new MissingClientException(); + } + /** @var PaginatedCommand $nextCommand */ $nextCommand = clone $this->lastCommand; $nextCommand->setLimit($offset, $size); /** @var PaginatedResponse $paginated */ - $paginated = $this->client->execute($nextCommand); + $paginated = $this->getClient()->execute($nextCommand); $this->lastCommand = $nextCommand; $this->totalCount = $paginated->totalCount; $this->items = $paginated->items; diff --git a/src/Redis/Response/ResponseItem.php b/src/Redis/Response/ResponseItem.php new file mode 100644 index 0000000..a8f234d --- /dev/null +++ b/src/Redis/Response/ResponseItem.php @@ -0,0 +1,37 @@ +|float|int|string> + */ + public function getFields(): array; + + /** + * @param null|array|bool|float|int|string $default + * + * @return null|array|bool|float|int|string + */ + public function getFieldValue(string $fieldName, $default = null); +} diff --git a/src/Redis/Response/SearchResponseItem.php b/src/Redis/Response/SearchResponseItem.php index 0db7f35..6b1fa27 100644 --- a/src/Redis/Response/SearchResponseItem.php +++ b/src/Redis/Response/SearchResponseItem.php @@ -24,7 +24,7 @@ /** * @codeCoverageIgnore Simple value object */ -class SearchResponseItem +class SearchResponseItem implements ResponseItem { /** @var string */ private $hash; diff --git a/tests/Redis/Command/CursorReadTest.php b/tests/Redis/Command/CursorReadTest.php index da7c0fd..b2b1e18 100644 --- a/tests/Redis/Command/CursorReadTest.php +++ b/tests/Redis/Command/CursorReadTest.php @@ -21,7 +21,9 @@ namespace MacFJA\RediSearch\tests\Redis\Command; +use MacFJA\RediSearch\Exception\UnexpectedServerResponseException; use MacFJA\RediSearch\Redis\Command\CursorRead; +use MacFJA\RediSearch\Redis\Response\CursorResponse; use PHPUnit\Framework\TestCase; /** @@ -32,6 +34,8 @@ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption * @uses \MacFJA\RediSearch\Redis\Command\Option\NamedOption * @uses \MacFJA\RediSearch\Redis\Command\Option\FlagOption + * @uses \MacFJA\RediSearch\Redis\Response\CursorResponse + * @uses \MacFJA\RediSearch\Exception\UnexpectedServerResponseException * * @internal */ @@ -63,4 +67,37 @@ public function testFullOption(): void $command->setCount(30); static::assertSame(['READ', 'idx', 398, 'COUNT', 30], $command->getArguments()); } + + public function testParseResponseWithError(): void + { + $this->expectException(UnexpectedServerResponseException::class); + + $command = new CursorRead(); + $command->parseResponse(''); + } + + public function testParseResponse(): void + { + $rawResponse = [ + 0 => [ + 0 => 0, + 1 => [ + 0 => 'f1', + 1 => 'bar', + ], + 2 => [ + 0 => 'f1', + 1 => 'baz', + ], + ], + 1 => 976015094, + ]; + $command = new CursorRead(); + $command->setIndex('idx') + ->setCursorId(976015094) + ; + $response = $command->parseResponse($rawResponse); + + static::assertInstanceOf(CursorResponse::class, $response); + } } diff --git a/tests/Redis/Command/IndexListTest.php b/tests/Redis/Command/IndexListTest.php index 97b5737..0504d1d 100644 --- a/tests/Redis/Command/IndexListTest.php +++ b/tests/Redis/Command/IndexListTest.php @@ -36,4 +36,13 @@ public function testGetId(): void $command = new IndexList(); static::assertSame('FT._LIST', $command->getId()); } + + public function testGetRediSearchVersion(): void + { + $command = new IndexList(); + + static::assertSame('2.0.0', $command->getRediSearchVersion()); + $command->setRediSearchVersion('2.2.0'); + static::assertSame('2.2.0', $command->getRediSearchVersion()); + } } diff --git a/tests/Redis/Command/Option/CustomValidatorOptionTest.php b/tests/Redis/Command/Option/CustomValidatorOptionTest.php index 925d40d..0d2df9f 100644 --- a/tests/Redis/Command/Option/CustomValidatorOptionTest.php +++ b/tests/Redis/Command/Option/CustomValidatorOptionTest.php @@ -21,6 +21,7 @@ namespace MacFJA\RediSearch\tests\Redis\Command\Option; +use BadMethodCallException; use Generator; use MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption; use MacFJA\RediSearch\Redis\Command\Option\NamelessOption; @@ -61,6 +62,24 @@ public function testShorthands(): void static::assertFalse($invalidList->isValid()); } + public function testInvalidMethodName(): void + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Call undefined method invalidName'); + + $decorator = new CustomValidatorOption(new NamelessOption('foo'), new Length(4)); + // @phpstan-ignore-next-line + $decorator->invalidName(); + } + + public function testVersionConstraint(): void + { + $decorator = new CustomValidatorOption(new NamelessOption('foo'), new Length(4)); + static::assertSame('*', $decorator->getVersionConstraint()); + $decorator = new CustomValidatorOption(new NamelessOption('foo', '>=1.0.0'), new Length(4)); + static::assertSame('>=1.0.0', $decorator->getVersionConstraint()); + } + public function dataProvider(string $testName): Generator { $invalidMinLengthOption = new CustomValidatorOption(new NamelessOption('foo'), new Length(4)); diff --git a/tests/Redis/Response/ArrayResponseTraitTest.php b/tests/Redis/Response/ArrayResponseTraitTest.php new file mode 100644 index 0000000..19bd0df --- /dev/null +++ b/tests/Redis/Response/ArrayResponseTraitTest.php @@ -0,0 +1,118 @@ +getInlineArrayResponse(); + + static::assertSame(['1' => 2], $inlineArrayResponse->publicGetPairs($data)); + } + + public function testEvenGetPairs(): void + { + $data = [1, 2, 3, 4]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(['1' => 2, '3' => 4], $inlineArrayResponse->publicGetPairs($data)); + } + + public function testOddGetKeys(): void + { + $data = [1, 2, 3]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(['1'], $inlineArrayResponse->publicGetKeys($data)); + } + + public function testEvenGetKeys(): void + { + $data = [1, 2, 3, 4]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(['1', '3'], $inlineArrayResponse->publicGetKeys($data)); + } + + public function testOddGetValue(): void + { + $data = [1, 2, 3]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(2, $inlineArrayResponse->publicGetValue($data, '1')); + static::assertNull($inlineArrayResponse->publicGetValue($data, '3')); + } + + public function testEvenGetValue(): void + { + $data = [1, 2, 3, 4]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(2, $inlineArrayResponse->publicGetValue($data, '1')); + static::assertSame(4, $inlineArrayResponse->publicGetValue($data, '3')); + static::assertNull($inlineArrayResponse->publicGetValue($data, '6')); + } + + public function testOddGetNextValue(): void + { + $data = [1, 2, 3]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(2, $inlineArrayResponse->publicGetNextValue($data, '1')); + static::assertSame(3, $inlineArrayResponse->publicGetNextValue($data, '2')); + static::assertNull($inlineArrayResponse->publicGetNextValue($data, '3')); + } + + public function testEvenGetNextValue(): void + { + $data = [1, 2, 3, 4]; + + $inlineArrayResponse = $this->getInlineArrayResponse(); + + static::assertSame(2, $inlineArrayResponse->publicGetNextValue($data, '1')); + static::assertSame(3, $inlineArrayResponse->publicGetNextValue($data, '2')); + static::assertSame(4, $inlineArrayResponse->publicGetNextValue($data, '3')); + static::assertNull($inlineArrayResponse->publicGetNextValue($data, '6')); + } + + private function getInlineArrayResponse(): ArrayResponseTraitTester + { + return new ArrayResponseTraitTester(); + } +} diff --git a/tests/Redis/Response/InfoResponseTest.php b/tests/Redis/Response/InfoResponseTest.php index c86f904..747bec8 100644 --- a/tests/Redis/Response/InfoResponseTest.php +++ b/tests/Redis/Response/InfoResponseTest.php @@ -23,6 +23,10 @@ use BadMethodCallException; use InvalidArgumentException; +use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption; use MacFJA\RediSearch\Redis\Command\Info; use MacFJA\RediSearch\Redis\Response\InfoResponse; use PHPUnit\Framework\TestCase; @@ -36,6 +40,9 @@ * @uses \MacFJA\RediSearch\Redis\Command\Option\NamelessOption * @uses \MacFJA\RediSearch\Redis\Command\AbstractCommand::__construct * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption + * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption + * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption + * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption * @uses \MacFJA\RediSearch\Redis\Command\Option\CustomValidatorOption * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait @@ -79,6 +86,22 @@ public function testResponse(): void 3 => new Status('WEIGHT'), 4 => '1', ], + 1 => [ + 0 => new Status('labels'), + 1 => new Status('type'), + 2 => new Status('TAG'), + ], + 2 => [ + 0 => new Status('place'), + 1 => new Status('type'), + 2 => new Status('GEO'), + ], + 3 => [ + 0 => new Status('count'), + 1 => new Status('type'), + 2 => new Status('NUMERIC'), + 3 => new Status('NOINDEX'), + ], ], 8 => new Status('num_docs'), 9 => '4', @@ -176,6 +199,13 @@ public function doTestGetFieldsAsOption(InfoResponse $parsed): void $response = $parsed->getFieldsAsOption(); static::assertSame(['text', 'TEXT', 'WEIGHT', '1'], $response[0]->render()); + static::assertInstanceOf(TextFieldOption::class, $response[0]); + static::assertSame(['labels', 'TAG'], $response[1]->render()); + static::assertInstanceOf(TagFieldOption::class, $response[1]); + static::assertSame(['place', 'GEO'], $response[2]->render()); + static::assertInstanceOf(GeoFieldOption::class, $response[2]); + static::assertSame(['count', 'NUMERIC', 'NOINDEX'], $response[3]->render()); + static::assertInstanceOf(NumericFieldOption::class, $response[3]); } public function doTestGetIndexDefinition(InfoResponse $parsed): void @@ -232,4 +262,28 @@ public function testNotANumber(): void static::assertNull($response->getNumDocs()); } + + public function testDataCasting(): void + { + $response = new InfoResponse([ + 'records_per_doc_avg', '1.5', + 'max_doc_id', '50', + 'index_name', 'idx', + 'indexing', '1', + 'non_existent_string', 'hello', + 'non_existent_float', 1.2, + 'non_existent_int', 1, + ]); + + static::assertSame(1.5, $response->getRecordsPerDocAvg()); + static::assertSame(50, $response->getMaxDocId()); + static::assertSame('idx', $response->getIndexName()); + static::assertTrue($response->getIndexing()); + // @phpstan-ignore-next-line + static::assertSame('hello', $response->getNonExistentString()); + // @phpstan-ignore-next-line + static::assertSame(1.2, $response->getNonExistentFloat()); + // @phpstan-ignore-next-line + static::assertSame(1, $response->getNonExistentInt()); + } } diff --git a/tests/Redis/Response/PaginatedResponseTest.php b/tests/Redis/Response/PaginatedResponseTest.php new file mode 100644 index 0000000..d85ac3e --- /dev/null +++ b/tests/Redis/Response/PaginatedResponseTest.php @@ -0,0 +1,190 @@ +getClientAndCommand(); + + /** @var PaginatedResponse $response */ + $response = $client->execute($command); + + static::assertInstanceOf(PaginatedResponse::class, $response); + + static::assertTrue($response->valid()); + static::assertSame('foo', $response->current()[0]->getFieldValue('f1')); + static::assertCount(4, $response); + static::assertEquals(2, $response->getPageSize()); + static::assertEquals(4, $response->getPageCount()); + static::assertEquals(7, $response->getTotalCount()); + static::assertEquals(0, $response->key()); + + $response->setClient($client); + + $response->next(); + + static::assertTrue($response->valid()); + static::assertSame('bob', $response->current()[0]->getFieldValue('f1')); + static::assertCount(4, $response); + static::assertEquals(2, $response->getPageSize()); + static::assertEquals(4, $response->getPageCount()); + static::assertEquals(7, $response->getTotalCount()); + static::assertEquals(1, $response->key()); + + $response->next(); + + static::assertTrue($response->valid()); + static::assertSame('lorem', $response->current()[0]->getFieldValue('f1')); + static::assertCount(4, $response); + static::assertEquals(2, $response->getPageSize()); + static::assertEquals(4, $response->getPageCount()); + static::assertEquals(7, $response->getTotalCount()); + static::assertEquals(2, $response->key()); + + $response->next(); + + static::assertTrue($response->valid()); + static::assertSame('odd', $response->current()[0]->getFieldValue('f1')); + static::assertCount(4, $response); + static::assertEquals(2, $response->getPageSize()); + static::assertEquals(4, $response->getPageCount()); + static::assertEquals(7, $response->getTotalCount()); + static::assertEquals(3, $response->key()); + + $response->next(); + + static::assertFalse($response->valid()); + } + + public function testMultiplePagesIterator(): void + { + [$client, $command] = $this->getClientAndCommand(); + + /** @var PaginatedResponse $response */ + $response = $client->execute($command); + + static::assertInstanceOf(PaginatedResponse::class, $response); + $response->setClient($client); + + foreach ($response as $index => $page) { + if (0 === $index) { + static::assertSame('foo', $response->current()[0]->getFieldValue('f1')); + } + if (1 === $index) { + static::assertSame('bob', $response->current()[0]->getFieldValue('f1')); + } + if (2 === $index) { + static::assertSame('lorem', $response->current()[0]->getFieldValue('f1')); + } + if (3 === $index) { + static::assertSame('odd', $response->current()[0]->getFieldValue('f1')); + } + + static::assertEquals(2, $response->getPageSize()); + static::assertEquals(4, $response->getPageCount()); + static::assertEquals(7, $response->getTotalCount()); + } + } + + public function testMissingClient(): void + { + $this->expectException(MissingClientException::class); + [$client, $command] = $this->getClientAndCommand(); + + /** @var PaginatedResponse $response */ + $response = $client->execute($command); + + static::assertInstanceOf(PaginatedResponse::class, $response); + + static::assertTrue($response->valid()); + + $response->next(); + $response->current(); + } + + public function testPageSizeSetToZero(): void + { + $command = new FakePaginatedCommand([]); + $command->setLimit(0, 0); + $response = new PaginatedResponse($command, 2, []); + + static::assertSame(0, $response->key()); + static::assertSame(0, $response->getPageCount()); + static::assertCount(0, $response); + } + + /** + * @return array{0:Client,1:FakePaginatedCommand} + */ + private function getClientAndCommand(): array + { + $command = new FakePaginatedCommand([]); + + $client = $this->createMock(Client::class); + $client->method('execute')->willReturnOnConsecutiveCalls( + self::getResponses($command, 0, 2), + self::getResponses($command, 2, 4), + self::getResponses($command, 4, 6), + self::getResponses($command, 6, 7) + ); + + return [$client, $command]; + } + + private static function getResponses(PaginatedCommand $command, int $from, int $to): PaginatedResponse + { + $response = [ + new SearchResponseItem('foobar1', ['f1' => 'foo', 'f2' => 'bar']), + new SearchResponseItem('foobar2', ['f1' => 'baz', 'f2' => 'alice']), + new SearchResponseItem('foobar3', ['f1' => 'bob', 'f2' => 'charlie']), + new SearchResponseItem('foobar4', ['f1' => 'john', 'f2' => 'doe']), + new SearchResponseItem('foobar5', ['f1' => 'lorem', 'f2' => 'ipsum']), + new SearchResponseItem('foobar6', ['f1' => 'foobar', 'f2' => 'foobaz']), + new SearchResponseItem('foobar7', ['f1' => 'odd', 'f2' => 'page']), + ]; + + return new PaginatedResponse( + $command, + 7, + array_slice($response, $from, $to - $from, false) + ); + } +} diff --git a/tests/fixtures/Redis/Command/FakePaginatedCommand.php b/tests/fixtures/Redis/Command/FakePaginatedCommand.php new file mode 100644 index 0000000..607ac74 --- /dev/null +++ b/tests/fixtures/Redis/Command/FakePaginatedCommand.php @@ -0,0 +1,62 @@ +offset; + } + + public function getSize(): ?int + { + return $this->size; + } + + public function setLimit(int $offset, int $size): PaginatedCommand + { + $this->offset = $offset; + $this->size = $size; + + return $this; + } + + protected function getRequiredOptions(): array + { + return []; + } +} diff --git a/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php b/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php new file mode 100644 index 0000000..e6a9708 --- /dev/null +++ b/tests/fixtures/Redis/Response/ArrayResponseTraitTester.php @@ -0,0 +1,69 @@ + $notKeyedArray + * + * @return array + */ + public function publicGetPairs($notKeyedArray) + { + return self::getPairs($notKeyedArray); + } + + /** + * @param array $notKeyedArray + * + * @return array + */ + public function publicGetKeys($notKeyedArray) + { + return self::getKeys($notKeyedArray); + } + + /** + * @param array $notKeyedArray + * + * @return null|array|bool|float|int|string + */ + public function publicGetValue(array $notKeyedArray, string $key) + { + return self::getValue($notKeyedArray, $key); + } + + /** + * @param array $notKeyedArray + * + * @return null|bool|float|int|string + */ + public function publicGetNextValue(array $notKeyedArray, string $key) + { + return self::getNextValue($notKeyedArray, $key); + } +} diff --git a/tests/integration/DockerTest.php b/tests/integration/DockerTest.php index 2d9234b..5426537 100644 --- a/tests/integration/DockerTest.php +++ b/tests/integration/DockerTest.php @@ -58,7 +58,6 @@ * @covers \MacFJA\RediSearch\Redis\Client\CredisClient * @covers \MacFJA\RediSearch\Redis\Client\PredisClient * @covers \MacFJA\RediSearch\Redis\Client\RedisentClient - * @covers \MacFJA\RediSearch\Redis\Client\Rediska\RediskaRediSearchCommand * @covers \MacFJA\RediSearch\Redis\Client\RediskaClient * @covers \MacFJA\RediSearch\Redis\Client\TinyRedisClient * @covers \MacFJA\RediSearch\Redis\Command\Aggregate @@ -214,8 +213,9 @@ public function testIntegration($clientBuilder): void /** @var PaginatedResponse $result */ $result = $client->execute($search); - static::assertCount(3, $result); + static::assertCount(1, $result); static::assertSame(1, $result->getPageCount()); + static::assertSame(3, $result->getTotalCount()); /** @var array $page */ foreach ($result as $page) { @@ -255,12 +255,13 @@ public function testIntegration($clientBuilder): void ->addGroupBy(new GroupByOption([], [ReduceOption::toList('age', 'list')])) ); - static::assertCount(3, $result[0]); - static::assertEquals('1', current($result[1])[0]->getValue('count')); - static::assertEquals('2', current($result[1])[1]->getValue('count')); - static::assertEquals('3', current($result[2])[0]->getValue('count')); - static::assertEquals('3', current($result[3])[0]->getValue('count')); - static::assertEquals(['30'], current($result[4])[0]->getValue('list')); + static::assertCount(1, $result[0]); + static::assertSame(3, $result[0]->getTotalCount()); + static::assertEquals('1', current($result[1])[0]->getFieldValue('count')); + static::assertEquals('2', current($result[1])[1]->getFieldValue('count')); + static::assertEquals('3', current($result[2])[0]->getFieldValue('count')); + static::assertEquals('3', current($result[3])[0]->getFieldValue('count')); + static::assertEquals(['30'], current($result[4])[0]->getFieldValue('list')); $client->execute((new DropIndex())->setIndex('testDoc')->setDeleteDocument()); $client->executeRaw('del', ...$docToRemove);