diff --git a/src/Cache/CacheClient.php b/src/Cache/CacheClient.php index 16840cd..de20dcb 100644 --- a/src/Cache/CacheClient.php +++ b/src/Cache/CacheClient.php @@ -47,6 +47,7 @@ use Momento\Cache\CacheOperationTypes\SetResponse; use Momento\Cache\CacheOperationTypes\SortedSetFetchResponse; use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreResponse; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreResponse; use Momento\Cache\CacheOperationTypes\SortedSetPutElementResponse; use Momento\Cache\CacheOperationTypes\SortedSetGetScoreResponse; use Momento\Cache\CacheOperationTypes\CreateCacheResponse; @@ -1677,8 +1678,6 @@ public function setLength(string $cacheName, string $setName): SetLengthResponse return $this->setLengthAsync($cacheName, $setName)->wait(); } - // placeholder: sortedSetLengthByScore - /** * Remove an element from a set. * @@ -2196,6 +2195,72 @@ public function sortedSetRemoveElements(string $cacheName, string $sortedSetName return $this->sortedSetRemoveElementsAsync($cacheName, $sortedSetName, $values)->wait(); } + /** + * Get the length of a sorted set, optionally limited by a minimum and maximum score. + * + * @param string $cacheName Name of the cache that contains the sorted set. + * @param string $sortedSetName The name of the sorted set whose length should be returned. + * @param ?float $minScore The minimum score (inclusive) of the + * elements to fetch. Defaults to negative infinity. + * @param ?float $maxScore The maximum score (inclusive) of the + * elements to fetch. Defaults to positive infinity. + * @return ResponseFuture A waitable future which will + * provide the result of the sorted set length by score operation upon a blocking call to wait:
+ * $response = $responseFuture->wait();
+ * The response represents the result of the sorted set length by score operation. + * This result is resolved to a type-safe object of one of the following types:
+ * * SortedSetLengthByScoreHit
+ * * SortedSetLengthByScoreMiss
+ * * SortedSetLengthByScoreError
+ * Pattern matching can be to operate on the appropriate subtype:
+ * + * if ($success = $response->asHit()) { + * return $success->length(); + * } elseif ($response->asMiss()) + * // handle miss response + * } elseif ($error = $response->asError()) + * // handle error condition + * } + * + * If inspection of the response is not required, one need not call wait as + * we implicitly wait for completion of the request on destruction of the + * response future. + */ + public function sortedSetLengthByScoreAsync(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): ResponseFuture + { + return $this->getNextDataClient()->sortedSetLengthByScore($cacheName, $sortedSetName, $minScore, $maxScore); + } + + /** + * Get the length of a sorted set, optionally limited by a minimum and maximum score. + * + * @param string $cacheName Name of the cache that contains the sorted set. + * @param string $sortedSetName The name of the sorted set whose length should be returned. + * @param ?float $minScore The minimum score (inclusive) of the + * elements to fetch. Defaults to negative infinity. + * @param ?float $maxScore The maximum score (inclusive) of the + * elements to fetch. Defaults to positive infinity. + * @return SortedSetLengthByScoreResponse Represents the result of the sorted set length by score operation. + * This result is resolved to a type-safe object of one of the following types:
+ * * SortedSetLengthByScoreHit
+ * * SortedSetLengthByScoreMiss
+ * * SortedSetLengthByScoreError
+ * Pattern matching can be to operate on the appropriate subtype:
+ * + * if ($success = $response->asHit()) { + * return $success->length(); + * } elseif ($response->asMiss()) + * // handle miss response + * } elseif ($error = $response->asError()) + * // handle error condition + * } + * + */ + public function sortedSetLengthByScore(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): SortedSetLengthByScoreResponse + { + return $this->sortedSetLengthByScoreAsync($cacheName, $sortedSetName, $minScore, $maxScore)->wait(); + } + /** * Gets the cache values stored for given keys. * diff --git a/src/Cache/CacheOperationTypes/CacheOperationTypes.php b/src/Cache/CacheOperationTypes/CacheOperationTypes.php index 1069148..f0629b0 100644 --- a/src/Cache/CacheOperationTypes/CacheOperationTypes.php +++ b/src/Cache/CacheOperationTypes/CacheOperationTypes.php @@ -22,10 +22,10 @@ use Cache_client\_SetFetchResponse; use Cache_client\_SetLengthResponse; use Cache_client\_SortedSetFetchResponse; +use Cache_client\_SortedSetLengthByScoreResponse; use Cache_client\ECacheResult; use Closure; use Control_client\_ListCachesResponse; -use Generator; use Momento\Cache\Errors\SdkError; use Momento\Cache\Errors\UnknownError; use Throwable; @@ -3239,7 +3239,104 @@ class SetRemoveElementError extends SetRemoveElementResponse use ErrorBody; } -// placeholder: sortedSetLengthByScore +/** + * Parent response type for a sorted set length by score request. The + * response object is resolved to a type-safe object of one of + * the following subtypes: + * + * * SortedSetLengthByScoreHit + * * SortedSetLengthByScoreMiss + * * SortedSetLengthByScoreError + * + * Pattern matching can be used to operate on the appropriate subtype. + * For example: + * + * if ($success = $response->asHit()) { + * return $success->length(); + * } elseif ($response->asMiss()) + * // handle miss as appropriate + * } elseif ($error = $response->asError()) + * // handle error as appropriate + * } + * + */ +abstract class SortedSetLengthByScoreResponse extends ResponseBase +{ + /** + * @return SortedSetLengthByScoreHit|null Returns the hit subtype if the request returned a hit and null otherwise. + */ + public function asHit(): ?SortedSetLengthByScoreHit + { + if ($this->isHit()) { + return $this; + } + return null; + } + + /** + * @return SortedSetLengthByScoreMiss|null Returns the miss subtype if the request returned a miss and null otherwise. + */ + public function asMiss(): ?SortedSetLengthByScoreMiss + { + if ($this->isMiss()) { + return $this; + } + return null; + } + + /** + * @return SortedSetLengthByScoreError|null Returns the error subtype if the request returned an error and null otherwise. + */ + public function asError(): ?SortedSetLengthByScoreError + { + if ($this->isError()) { + return $this; + } + return null; + } +} + +/** + * Indicates that the request that generated it was successful. + */ +class SortedSetLengthByScoreHit extends SortedSetLengthByScoreResponse +{ + private int $length; + + public function __construct(_SortedSetLengthByScoreResponse $response) + { + parent::__construct(); + $this->length = $response->getFound() ? $response->getFound()->getLength() : 0; + } + + /** + * @return int Length of the specified sorted set. + */ + public function length(): int + { + return $this->length; + } + + public function __toString() + { + return parent::__toString() . ": {$this->length}"; + } +} + +/** + * Indicates that the request that generated it was a cache miss. + */ +class SortedSetLengthByScoreMiss extends SortedSetLengthByScoreResponse +{ +} + +/** + * Contains information about an error returned from the request. + */ +class SortedSetLengthByScoreError extends SortedSetLengthByScoreResponse +{ + use ErrorBody; +} /** * Parent response type for a sorted set put element request. The diff --git a/src/Cache/Internal/ScsDataClient.php b/src/Cache/Internal/ScsDataClient.php index 5d54c8f..69e20d4 100644 --- a/src/Cache/Internal/ScsDataClient.php +++ b/src/Cache/Internal/ScsDataClient.php @@ -36,6 +36,7 @@ use Cache_client\_SortedSetFetchRequest; use Cache_client\_SortedSetGetScoreRequest; use Cache_client\_SortedSetIncrementRequest; +use Cache_client\_SortedSetLengthByScoreRequest; use Cache_client\_SortedSetPutRequest; use Cache_client\_SortedSetRemoveRequest; use Cache_client\_UpdateTtlRequest; @@ -200,6 +201,11 @@ use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreError; use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreResponse; use Momento\Cache\CacheOperationTypes\SortedSetIncrementScoreSuccess; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreError; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreHit; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreMiss; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreResponse; +use Momento\Cache\CacheOperationTypes\SortedSetLengthByScoreSuccess; use Momento\Cache\CacheOperationTypes\SortedSetPutElementError; use Momento\Cache\CacheOperationTypes\SortedSetPutElementResponse; use Momento\Cache\CacheOperationTypes\SortedSetPutElementsError; @@ -1644,6 +1650,58 @@ function () use ($call): SortedSetPutElementsResponse { ); } + /** + * @return ResponseFuture + */ + public function sortedSetLengthByScore(string $cacheName, string $sortedSetName, ?float $minScore = null, ?float $maxScore = null): ResponseFuture + { + try { + validateCacheName($cacheName); + validateSortedSetName($sortedSetName); + validateSortedSetScores($minScore, $maxScore); + $sortedSetLengthByScoreRequest = new _SortedSetLengthByScoreRequest(); + $sortedSetLengthByScoreRequest->setSetName($sortedSetName); + + if (!is_null($minScore)) { + $sortedSetLengthByScoreRequest->setInclusiveMin($minScore); + } else { + $sortedSetLengthByScoreRequest->setUnboundedMin(new _Unbounded()); + } + if (!is_null($maxScore)) { + $sortedSetLengthByScoreRequest->setInclusiveMax($maxScore); + } else { + $sortedSetLengthByScoreRequest->setUnboundedMax(new _Unbounded()); + } + + $call = $this->grpcManager->client->SortedSetLengthByScore( + $sortedSetLengthByScoreRequest, + ["cache" => [$cacheName]], + ["timeout" => $this->timeout], + ); + } catch (SdkError $e) { + return ResponseFuture::createResolved(new SortedSetLengthByScoreError($e)); + } catch (Exception $e) { + return ResponseFuture::createResolved(new SortedSetLengthByScoreError(new UnknownError($e->getMessage()))); + } + + return ResponseFuture::createPending( + function () use ($call): SortedSetLengthByScoreResponse { + try { + $response = $this->processCall($call); + + if ($response->hasFound()) { + return new SortedSetLengthByScoreHit($response); + } + return new SortedSetLengthByScoreMiss(); + } catch (SdkError $e) { + return new SortedSetLengthByScoreError($e); + } catch (Exception $e) { + return new SortedSetLengthByScoreError(new UnknownError($e->getMessage(), 0, $e)); + } + } + ); + } + public function sortedSetIncrementScore(string $cacheName, string $sortedSetName, string $value, float $amount, ?CollectionTtl $ttl): ResponseFuture { try { diff --git a/tests/Cache/CacheClientTest.php b/tests/Cache/CacheClientTest.php index 5139224..386cb59 100644 --- a/tests/Cache/CacheClientTest.php +++ b/tests/Cache/CacheClientTest.php @@ -3936,6 +3936,106 @@ public function testSortedSetGetScore_NonexistentCache() $this->assertEquals(MomentoErrorCode::CACHE_NOT_FOUND_ERROR, $response->asError()->errorCode()); } + public function testSortedSetLengthByScore_HappyPath() + { + $sortedSetName = uniqid(); + + $elements = [ + "foo" => 1.0, + "bar" => 2.0, + "baz" => 3.0, + "qux" => 4.0, + ]; + + // the length is 0 when the set does not yet exist + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asMiss(), "Expected a miss but got: $response"); + + $response = $this->client->sortedSetPutElements($this->TEST_CACHE_NAME, $sortedSetName, $elements); + $this->assertNotNull($response->asSuccess(), "Expected a success but got: $response"); + + // full set + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asHit(), "Expected a success but got: $response"); + + $fetchedLength = $response->asHit()->length(); + $expectedLength = 4; + $this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength"); + + // limit by min score + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 1.1); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asHit(), "Expected a success but got: $response"); + + $fetchedLength = $response->asHit()->length(); + $expectedLength = 3; + $this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength"); + + // limit by max score + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, null, 3.9); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asHit(), "Expected a success but got: $response"); + + $fetchedLength = $response->asHit()->length(); + $expectedLength = 3; + $this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength"); + + // limit by min and max score + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 1.1, 3.9); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asHit(), "Expected a success but got: $response"); + + $fetchedLength = $response->asHit()->length(); + $expectedLength = 2; + $this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength"); + + // no elements in score range + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, 100.0); + $this->assertNull($response->asError(), "Error occurred while fetching sorted set '$sortedSetName'"); + $this->assertNotNull($response->asHit(), "Expected a success but got: $response"); + + $fetchedLength = $response->asHit()->length(); + $expectedLength = 0; + $this->assertEquals($expectedLength, $fetchedLength, "expected length of non-existent sorted set to be $expectedLength, not $fetchedLength"); + } + + public function testSortedSetLengthByScoreWithNonexistantCache_ThrowsException() + { + $cacheName = uniqid(); + $sortedSetName = uniqid(); + $response = $this->client->sortedSetLengthByScore($cacheName, $sortedSetName); + $this->assertNotNull($response->asError(), "Expected error but got: $response"); + $this->assertEquals(MomentoErrorCode::CACHE_NOT_FOUND_ERROR, $response->asError()->errorCode()); + } + + public function testSortedSetLengthByScoreWithNullCacheName_ThrowsException() + { + $sortedSetName = uniqid(); + $response = $this->client->sortedSetLengthByScore((string)null, $sortedSetName); + $this->assertNotNull($response->asError(), "Expected error but got: $response"); + $this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode()); + } + + public function testSortedSetLengthByScoreWithEmptyCacheName_ThrowsException() + { + $sortedSetName = uniqid(); + $response = $this->client->sortedSetLengthByScore("", $sortedSetName); + $this->assertNotNull($response->asError(), "Expected error but got: $response"); + $this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode()); + } + + public function testSortedSetLengthByScoreWithMinScoreLargerThanMaxScore_ThrowsException() + { + $sortedSetName = uniqid(); + $minScore = 100.0; + $maxScore = 1.0; + $response = $this->client->sortedSetLengthByScore($this->TEST_CACHE_NAME, $sortedSetName, $minScore, $maxScore); + $this->assertNotNull($response->asError(), "Expected error but got: $response"); + $this->assertEquals(MomentoErrorCode::INVALID_ARGUMENT_ERROR, $response->asError()->errorCode()); + } + public function testGetBatch_HappyPath() { $cacheName = $this->TEST_CACHE_NAME;