Rework SortedSet and List range APIs

Motivation:

The SortedSet and List range commands (LTRIM, LRANGE, ZRANGE, etc.) are stringly-based and not flexible with Swift syntax.

Modifications:

- Add overloads of LTRIM that support the gambit of Range Standard Library types
- Rework LRANGE to mirror LTRIM method signatures
- Rework ZScore Range based commands to be more type-safe with `RedisZScoreBound` enum
- Rework ZLex Range based commands to be more type-safe with `RedisZLexBound` enum
- Rework ZCOUNT, ZLEXCOUNT, ZRANGE, ZREVRANGE, ZREMRANGEBYLEX, ZREMRANGEBYRANK, ZREMRANGEBYSCORE methods to be more type-safe and support Swift Range syntax

Result:

Working with SortedSet ranges should be much more type safe, and expressive with Swift's Range syntax.
parent 47480b80
Pipeline #106268785 passed with stage
in 3 minutes and 23 seconds
......@@ -90,15 +90,19 @@ extension RedisClient {
return send(command: "LREM", with: args)
.convertFromRESPValue()
}
}
// MARK: LTrim
/// Trims a list to only contain elements within the specified inclusive bounds of 0-based indices.
extension RedisClient {
/// Trims a List to only contain elements within the specified inclusive bounds of 0-based indices.
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Parameters:
/// - key: The key of the list to trim.
/// - key: The key of the List to trim.
/// - start: The index of the first element to keep.
/// - stop: The index of the last element to keep.
/// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, before start: Int, after stop: Int) -> EventLoopFuture<Void> {
let args: [RESPValue] = [
......@@ -109,28 +113,295 @@ extension RedisClient {
return send(command: "LTRIM", with: args)
.map { _ in () }
}
/// Trims a List to only contain elements within the specified inclusive bounds of 0-based indices.
///
/// To keep elements 4 through 7:
/// ```swift
/// client.ltrim("myList", keepingIndices: 3...6)
/// ```
///
/// To keep the last 4 through 7 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: (-7)...(-4))
/// ```
///
/// To keep the first and last 4 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: (-4)...3)
/// ```
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Warning: A `ClosedRange` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
/// `ClosedRange` will trigger a precondition failure.
///
/// If you need such a range, use `ltrim(_:before:after:)` instead.
/// - Parameters:
/// - key: The key of the List to trim.
/// - range: The range of indices that should be kept in the List.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, keepingIndices range: ClosedRange<Int>) -> EventLoopFuture<Void> {
return self.ltrim(key, before: range.lowerBound, after: range.upperBound)
}
/// Trims a List to only contain elements starting from the specified index.
///
/// To keep all but the first 3 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: 3...)
/// ```
///
/// To keep the last 4 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: (-4)...)
/// ```
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Parameters:
/// - key: The key of the List to trim.
/// - range: The range of indices that should be kept in the List.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeFrom<Int>) -> EventLoopFuture<Void> {
return self.ltrim(key, before: range.lowerBound, after: -1)
}
/// Trims a List to only contain elements before the specified index.
///
/// To keep the first 3 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: ..<3)
/// ```
///
/// To keep all but the last 4 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: ..<(-4))
/// ```
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Parameters:
/// - key: The key of the List to trim.
/// - range: The range of indices that should be kept in the List.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeUpTo<Int>) -> EventLoopFuture<Void> {
return self.ltrim(key, before: 0, after: range.upperBound - 1)
}
/// Trims a List to only contain elements up to the specified index.
///
/// To keep the first 4 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: ...3)
/// ```
///
/// To keep all but the last 3 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: ...(-4))
/// ```
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Parameters:
/// - key: The key of the List to trim.
/// - range: The range of indices that should be kept in the List.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, keepingIndices range: PartialRangeThrough<Int>) -> EventLoopFuture<Void> {
return self.ltrim(key, before: 0, after: range.upperBound)
}
/// Trims a List to only contain the elements from the specified index up to the index provided.
///
/// To keep the first 4 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: 0..<4)
/// ```
///
/// To keep all but the last 3 elements:
/// ```swift
/// client.ltrim("myList", keepingIndices: 0..<(-3))
/// ```
///
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
/// - Warning: A `Range` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0..<(-1)`,
/// `Range` will trigger a precondition failure.
///
/// If you need such a range, use `ltrim(_:before:after:)` instead.
/// - Parameters:
/// - key: The key of the List to trim.
/// - range: The range of indices that should be kept in the List.
/// - Returns: A `NIO.EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: RedisKey, keepingIndices range: Range<Int>) -> EventLoopFuture<Void> {
return self.ltrim(key, before: range.lowerBound, after: range.upperBound - 1)
}
}
// MARK: LRange
/// Gets all elements from a list within the the specified inclusive bounds of 0-based indices.
extension RedisClient {
/// Gets all elements from a List within the the specified inclusive bounds of 0-based indices.
///
/// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Parameters:
/// - range: The range of inclusive indices of elements to get.
/// - key: The key of the list.
/// - Returns: A list of elements found within the range specified.
/// - key: The key of the List.
/// - firstIndex: The index of the first element to include in the range of elements returned.
/// - lastIndex: The index of the last element to include in the range of elements returned.
/// - Returns: An array of elements found within the range specified.
@inlinable
public func lrange(
within range: (startIndex: Int, endIndex: Int),
from key: RedisKey
) -> EventLoopFuture<[RESPValue]> {
public func lrange(from key: RedisKey, firstIndex: Int, lastIndex: Int) -> EventLoopFuture<[RESPValue]> {
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: range.startIndex),
.init(bulk: range.endIndex)
.init(bulk: firstIndex),
.init(bulk: lastIndex)
]
return send(command: "LRANGE", with: args)
.convertFromRESPValue()
}
/// Gets all elements from a List within the specified inclusive bounds of 0-based indices.
///
/// To get the elements at index 4 through 7:
/// ```swift
/// client.lrange(from: "myList", indices: 4...7)
/// ```
///
/// To get the last 4 elements:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)...(-1))
/// ```
///
/// To get the first and last 4 elements:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)...3)
/// ```
///
/// To get the first element, and the last 4:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)...0))
/// ```
///
/// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Warning: A `ClosedRange` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0...-1`,
/// `ClosedRange` will trigger a precondition failure.
///
/// If you need such a range, use `lrange(from:firstIndex:lastIndex:)` instead.
/// - Parameters:
/// - key: The key of the List to return elements from.
/// - range: The range of inclusive indices of elements to get.
/// - Returns: An array of elements found within the range specified.
@inlinable
public func lrange(from key: RedisKey, indices range: ClosedRange<Int>) -> EventLoopFuture<[RESPValue]> {
return self.lrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound)
}
/// Gets all the elements from a List starting with the first index bound up to, but not including, the element at the last index bound.
///
/// To get the elements at index 4 through 7:
/// ```swift
/// client.lrange(from: "myList", indices: 4..<8)
/// ```
///
/// To get the last 4 elements:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)..<0)
/// ```
///
/// To get the first and last 4 elements:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)..<4)
/// ```
///
/// To get the first element, and the last 4:
/// ```swift
/// client.lrange(from: "myList", indices: (-4)..<1)
/// ```
///
/// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Warning: A `Range` cannot be created where `upperBound` is less than `lowerBound`; so while Redis may support `0..<(-1)`,
/// `Range` will trigger a precondition failure.
///
/// If you need such a range, use `lrange(from:firstIndex:lastIndex:)` instead.
/// - Parameters:
/// - key: The key of the List to return elements from.
/// - range: The range of indices (inclusive lower, exclusive upper) elements to get.
/// - Returns: An array of elements found within the range specified.
@inlinable
public func lrange(from key: RedisKey, indices range: Range<Int>) -> EventLoopFuture<[RESPValue]> {
return self.lrange(from: key, firstIndex: range.lowerBound, lastIndex: range.upperBound - 1)
}
/// Gets all elements from the index specified to the end of a List.
///
/// To get all except the first 2 elements of a List:
/// ```swift
/// client.lrange(from: "myList", fromIndex: 2)
/// ```
///
/// To get the last 4 elements of a List:
/// ```swift
/// client.lrange(from: "myList", fromIndex: -4)
/// ```
///
/// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Parameters:
/// - key: The key of the List to return elements from.
/// - index: The index of the first element that will be in the returned values.
/// - Returns: An array of elements from the List between the index and the end.
@inlinable
public func lrange(from key: RedisKey, fromIndex index: Int) -> EventLoopFuture<[RESPValue]> {
return self.lrange(from: key, firstIndex: index, lastIndex: -1)
}
/// Gets all elements from the the start of a List up to, and including, the element at the index specified.
///
/// To get the first 3 elements of a List:
/// ```swift
/// client.lrange(from: "myList", throughIndex: 2)
/// ```
///
/// To get all except the last 3 elements of a List:
/// ```swift
/// client.lrange(from: "myList", throughIndex: -4)
/// ```
///
/// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Parameters:
/// - key: The key of the List to return elements from.
/// - index: The index of the last element that will be in the returned values.
/// - Returns: An array of elements from the start of a List to the index.
@inlinable
public func lrange(from key: RedisKey, throughIndex index: Int) -> EventLoopFuture<[RESPValue]> {
return self.lrange(from: key, firstIndex: 0, lastIndex: index)
}
/// Gets all elements from the the start of a List up to, but not including, the element at the index specified.
///
/// To get the first 3 elements of a List:
/// ```swift
/// client.lrange(from: "myList", upToIndex: 3)
/// ```
///
/// To get all except the last 3 elements of a List:
/// ```swift
/// client.lrange(from: "myList", upToIndex: -3)
/// ```
///
/// See `lrange(from:indices:)`, `lrange(from:firstIndex:lastIndex:)`, and [https://redis.io/commands/lrange](https://redis.io/commands/lrange)
/// - Parameters:
/// - key: The key of the List to return elements from.
/// - index: The index of the element to not include in the returned values.
/// - Returns: An array of elements from the start of the List and up to the index.
@inlinable
public func lrange(from key: RedisKey, upToIndex index: Int) -> EventLoopFuture<[RESPValue]> {
return self.lrange(from: key, firstIndex: 0, lastIndex: index - 1)
}
}
// MARK: Pop & Push
extension RedisClient {
/// Pops the last element from a source list and pushes it to a destination list.
///
/// See [https://redis.io/commands/rpoplpush](https://redis.io/commands/rpoplpush)
......
......@@ -53,23 +53,28 @@ final class ListCommandsTests: RediStackIntegrationTestCase {
}
func test_lrange() throws {
var elements = try connection.lrange(within: (0, 10), from: #function).wait()
var elements = try connection.lrange(from: #function, indices: 0...10).wait()
XCTAssertEqual(elements.count, 0)
_ = try connection.lpush([5, 4, 3, 2, 1], into: #function).wait()
elements = try connection.lrange(within: (0, 4), from: #function).wait()
elements = try connection.lrange(from: #function, throughIndex: 4).wait()
XCTAssertEqual(elements.count, 5)
XCTAssertEqual(Int(fromRESP: elements[0]), 1)
XCTAssertEqual(Int(fromRESP: elements[4]), 5)
elements = try connection.lrange(from: #function, fromIndex: 1).wait()
XCTAssertEqual(elements.count, 4)
elements = try connection.lrange(from: #function, fromIndex: -3).wait()
XCTAssertEqual(elements.count, 3)
elements = try connection.lrange(within: (2, 0), from: #function).wait()
elements = try connection.lrange(from: #function, firstIndex: 2, lastIndex: 0).wait()
XCTAssertEqual(elements.count, 0)
elements = try connection.lrange(within: (4, 5), from: #function).wait()
elements = try connection.lrange(from: #function, indices: 4...5).wait()
XCTAssertEqual(elements.count, 1)
elements = try connection.lrange(within: (0, -4), from: #function).wait()
elements = try connection.lrange(from: #function, upToIndex: -3).wait()
XCTAssertEqual(elements.count, 2)
}
......@@ -109,13 +114,13 @@ final class ListCommandsTests: RediStackIntegrationTestCase {
_ = try connection.lpush([10], into: #function).wait()
_ = try connection.linsert(20, into: #function, after: 10).wait()
var elements = try connection.lrange(within: (0, 1), from: #function)
var elements = try connection.lrange(from: #function, throughIndex: 1)
.map { response in response.compactMap { Int(fromRESP: $0) } }
.wait()
XCTAssertEqual(elements, [10, 20])
_ = try connection.linsert(30, into: #function, before: 10).wait()
elements = try connection.lrange(within: (0, 2), from: #function)
elements = try connection.lrange(from: #function, throughIndex: 2)
.map { response in response.compactMap { Int(fromRESP: $0) } }
.wait()
XCTAssertEqual(elements, [30, 10, 20])
......@@ -236,4 +241,36 @@ final class ListCommandsTests: RediStackIntegrationTestCase {
.wait()
XCTAssertEqual(element, 10)
}
func test_ltrim() throws {
let setup = {
_ = try self.connection.delete(#function).wait()
_ = try self.connection.lpush([5, 4, 3, 2, 1], into: #function).wait()
}
let getElements = { return try self.connection.lrange(from: #function, fromIndex: 0).wait() }
try setup()
XCTAssertNoThrow(try connection.ltrim(#function, before: 1, after: 3).wait())
XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: 0...1).wait())
var elements = try getElements()
XCTAssertEqual(elements.count, 2)
try setup()
XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: (-3)...).wait())
elements = try getElements()
XCTAssertEqual(elements.count, 3)
try setup()
XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: ...(-4)).wait())
elements = try getElements()
XCTAssertEqual(elements.count, 2)
try setup()
XCTAssertNoThrow(try connection.ltrim(#function, keepingIndices: ..<(-2)).wait())
elements = try getElements()
XCTAssertEqual(elements.count, 3)
}
}
......@@ -126,17 +126,41 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zcount() throws {
var count = try connection.zcount(of: key, within: ("1", "3")).wait()
var count = try connection.zcount(of: key, withScores: 1...3).wait()
XCTAssertEqual(count, 3)
count = try connection.zcount(of: key, within: ("(1", "(3")).wait()
count = try connection.zcount(of: key, withScoresBetween: (.exclusive(1), .exclusive(3))).wait()
XCTAssertEqual(count, 1)
count = try connection.zcount(of: key, withScores: 3..<8).wait()
XCTAssertEqual(count, 5)
count = try connection.zcount(of: key, withMinimumScoreOf: .exclusive(7)).wait()
XCTAssertEqual(count, 3)
count = try connection.zcount(of: key, withMaximumScoreOf: 10).wait()
XCTAssertEqual(count, 10)
count = try connection.zcount(of: key, withScoresBetween: (3, 0)).wait()
XCTAssertEqual(count, 0)
}
func test_zlexcount() throws {
var count = try connection.zlexcount(of: key, within: ("[1", "[3")).wait()
for i in 1...10 {
_ = try connection.zadd((i, 1), to: #function).wait()
}
var count = try connection.zlexcount(of: #function, withValuesBetween: (.inclusive(1), .inclusive(3))).wait()
XCTAssertEqual(count, 4)
count = try connection.zlexcount(of: #function, withValuesBetween: (.exclusive(1), .exclusive(3))).wait()
XCTAssertEqual(count, 2)
count = try connection.zlexcount(of: #function, withMinimumValueOf: .inclusive(2)).wait()
XCTAssertEqual(count, 8)
count = try connection.zlexcount(of: #function, withMaximumValueOf: .exclusive(3)).wait()
XCTAssertEqual(count, 3)
count = try connection.zlexcount(of: key, within: ("(1", "(3")).wait()
XCTAssertEqual(count, 1)
}
func test_zpopmin() throws {
......@@ -254,9 +278,22 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zrange() throws {
var elements = try connection.zrange(within: (1, 3), from: key).wait()
var elements = try connection.zrange(from: key, indices: 1...3).wait()
XCTAssertEqual(elements.count, 3)
elements = try connection.zrange(from: key, indices: 3..<9).wait()
XCTAssertEqual(elements.count, 6)
elements = try connection.zrange(from: key, upToIndex: 4).wait()
XCTAssertEqual(elements.count, 4)
elements = try connection.zrange(from: key, throughIndex: 4).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrange(from: key, fromIndex: 7).wait()
XCTAssertEqual(elements.count, 3)
elements = try connection.zrange(within: (1, 3), from: key, withScores: true).wait()
elements = try connection.zrange(from: key, firstIndex: 1, lastIndex: 3, includeScoresInResponse: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
......@@ -268,9 +305,22 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zrevrange() throws {
var elements = try connection.zrevrange(within: (1, 3), from: key).wait()
var elements = try connection.zrevrange(from: key, indices: 1...3).wait()
XCTAssertEqual(elements.count, 3)
elements = try connection.zrevrange(within: (1, 3), from: key, withScores: true).wait()
elements = try connection.zrevrange(from: key, indices: 3..<9).wait()
XCTAssertEqual(elements.count, 6)
elements = try connection.zrevrange(from: key, upToIndex: 4).wait()
XCTAssertEqual(elements.count, 4)
elements = try connection.zrevrange(from: key, throughIndex: 4).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrevrange(from: key, fromIndex: 7).wait()
XCTAssertEqual(elements.count, 3)
elements = try connection.zrevrange(from: key, firstIndex: 1, lastIndex: 3, includeScoresInResponse: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
......@@ -282,9 +332,19 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zrangebyscore() throws {
var elements = try connection.zrangebyscore(within: ("(1", "3"), from: key).wait()
var elements = try connection.zrangebyscore(from: key, withScoresBetween: (.exclusive(1), 3)).wait()
XCTAssertEqual(elements.count, 2)
elements = try connection.zrangebyscore(within: ("1", "3"), from: key, withScores: true).wait()
elements = try connection.zrangebyscore(from: key, withScores: 7..<10, limitBy: (offset: 2, count: 3)).wait()
XCTAssertEqual(elements.count, 1)
elements = try connection.zrangebyscore(from: key, withMinimumScoreOf: .exclusive(5)).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrangebyscore(from: key, withMaximumScoreOf: 5).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrangebyscore(from: key, withScores: 1...3, includeScoresInResponse: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
......@@ -296,9 +356,19 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zrevrangebyscore() throws {
var elements = try connection.zrevrangebyscore(within: ("(1", "3"), from: key).wait()
var elements = try connection.zrevrangebyscore(from: key, withScoresBetween: (.exclusive(1), 3)).wait()
XCTAssertEqual(elements.count, 2)
elements = try connection.zrevrangebyscore(within: ("1", "3"), from: key, withScores: true).wait()
elements = try connection.zrevrangebyscore(from: key, withScores: 7..<10, limitBy: (offset: 2, count: 3)).wait()
XCTAssertEqual(elements.count, 1)
elements = try connection.zrevrangebyscore(from: key, withMinimumScoreOf: .exclusive(5)).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrevrangebyscore(from: key, withMaximumScoreOf: 5).wait()
XCTAssertEqual(elements.count, 5)
elements = try connection.zrevrangebyscore(from: key, withScores: 1...3, includeScoresInResponse: true).wait()
XCTAssertEqual(elements.count, 6)
let values = try RedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false)
......@@ -310,35 +380,74 @@ final class SortedSetCommandsTests: RediStackIntegrationTestCase {
}
func test_zrangebylex() throws {
_ = try connection.zadd([(1, 0), (2, 0), (3, 0)], to: #function).wait()
var elements = try connection.zrangebylex(within: ("[1", "[2"), from: #function)
for i in 1...10 {