Skip to content

Commit 050ad64

Browse files
committed
Add list convenience commands with unit tests
1 parent 21fc0a4 commit 050ad64

File tree

3 files changed

+362
-1
lines changed

3 files changed

+362
-1
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import NIO
2+
3+
extension RedisCommandExecutor {
4+
/// Returns the length of the list stored at the key provided.
5+
///
6+
/// See [https://redis.io/commands/llen](https://redis.io/commands/llen)
7+
@inlinable
8+
public func llen(of key: String) -> EventLoopFuture<Int> {
9+
return send(command: "LLEN", with: [key])
10+
.mapFromRESP()
11+
}
12+
13+
/// Returns the element at the specified index stored at the key provided.
14+
///
15+
/// See [https://redis.io/commands/llen](https://redis.io/commands/llen)
16+
@inlinable
17+
public func lindex(_ key: String, index: Int) -> EventLoopFuture<RESPValue> {
18+
return send(command: "LINDEX", with: [key, index])
19+
.flatMapThrowing { response in
20+
guard response.isNull else { return response }
21+
throw RedisError(identifier: #function, reason: "Index out of bounds.")
22+
}
23+
}
24+
25+
/// Sets the value at the specified index stored at the key provided.
26+
///
27+
/// See [https://redis.io/commands/lset](https://redis.io/commands/lset)
28+
@inlinable
29+
public func lset(_ key: String, index: Int, to value: RESPValueConvertible) -> EventLoopFuture<Void> {
30+
return send(command: "LSET", with: [key, index, value])
31+
.map { _ in () }
32+
}
33+
34+
/// Removes elements from the list matching the value provided, up to the count specified.
35+
///
36+
/// See [https://redis.io/commands/lrem](https://redis.io/commands/lrem)
37+
/// - Returns: The number of elements removed.
38+
@inlinable
39+
public func lrem(_ value: RESPValueConvertible, from key: String, count: Int) -> EventLoopFuture<Int> {
40+
return send(command: "LREM", with: [key, count, value])
41+
.mapFromRESP()
42+
}
43+
44+
/// Trims the list stored at the key provided to contain elements within the bounds of indexes specified.
45+
///
46+
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
47+
@inlinable
48+
public func ltrim(_ key: String, startIndex start: Int, endIndex end: Int) -> EventLoopFuture<Void> {
49+
return send(command: "LTRIM", with: [key, start, end])
50+
.map { _ in () }
51+
}
52+
53+
/// Returns the elements within the range bounds provided.
54+
///
55+
/// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim)
56+
@inlinable
57+
public func lrange(of key: String, startIndex start: Int, endIndex end: Int) -> EventLoopFuture<[RESPValue]> {
58+
return send(command: "LRANGE", with: [key, start, end])
59+
.mapFromRESP()
60+
}
61+
62+
/// Pops the last element from the source list and pushes it to the destination list.
63+
///
64+
/// See [https://redis.io/commands/rpoplpush](https://redis.io/commands/rpoplpush)
65+
/// - Returns: The element that was moved.
66+
@inlinable
67+
public func rpoplpush(from source: String, to dest: String) -> EventLoopFuture<RESPValue> {
68+
return send(command: "RPOPLPUSH", with: [source, dest])
69+
}
70+
}
71+
72+
// MARK: Insert
73+
74+
extension RedisCommandExecutor {
75+
/// Inserts the value before the first element matching the pivot value provided.
76+
///
77+
/// See [https://redis.io/commands/linsert](https://redis.io/commands/linsert)
78+
/// - Returns: The size of the list after the insert, or -1 if an element matching the pivot value was not found.
79+
@inlinable
80+
public func linsert<T: RESPValueConvertible>(
81+
_ value: T,
82+
into key: String,
83+
before pivot: T) -> EventLoopFuture<Int>
84+
{
85+
return _linsert(pivotKeyword: "BEFORE", value, key, pivot)
86+
}
87+
88+
/// Inserts the value after the first element matching the pivot value provided.
89+
///
90+
/// See [https://redis.io/commands/linsert](https://redis.io/commands/linsert)
91+
/// - Returns: The size of the list after the insert, or -1 if an element matching the pivot value was not found.
92+
@inlinable
93+
public func linsert<T: RESPValueConvertible>(
94+
_ value: T,
95+
into key: String,
96+
after pivot: T) -> EventLoopFuture<Int>
97+
{
98+
return _linsert(pivotKeyword: "AFTER", value, key, pivot)
99+
}
100+
101+
@inline(__always)
102+
@usableFromInline
103+
func _linsert(pivotKeyword: StaticString, _ value: RESPValueConvertible, _ key: String, _ pivot: RESPValueConvertible) -> EventLoopFuture<Int> {
104+
return send(command: "LINSERT", with: [key, pivotKeyword.description, pivot, value])
105+
.mapFromRESP()
106+
}
107+
}
108+
109+
// MARK: Head Operations
110+
111+
extension RedisCommandExecutor {
112+
/// Removes the first element in the list and returns it.
113+
///
114+
/// See [https://redis.io/commands/lpop](https://redis.io/commands/lpop)
115+
@inlinable
116+
public func lpop(from key: String) -> EventLoopFuture<RESPValue?> {
117+
return send(command: "LPOP", with: [key])
118+
.mapFromRESP()
119+
}
120+
121+
/// Inserts all values provided into the list stored at the key specified.
122+
/// - Note: This inserts the values at the head of the list, for the tail see `rpush(_:to:)`.
123+
///
124+
/// See [https://redis.io/commands/lpush](https://redis.io/commands/lpush)
125+
/// - Returns: The length of the list after adding the new elements.
126+
@inlinable
127+
public func lpush(_ values: [RESPValueConvertible], to key: String) -> EventLoopFuture<Int> {
128+
return send(command: "LPUSH", with: [key] + values)
129+
.mapFromRESP()
130+
}
131+
132+
/// Inserts the value at the head of the list only if the key exists and holds a list.
133+
/// - Note: This inserts the values at the head of the list, for the tail see `rpushx(_:to:)`.
134+
///
135+
/// See [https://redis.io/commands/lpushx](https://redis.io/commands/lpushx)
136+
/// - Returns: The length of the list after adding the new elements.
137+
@inlinable
138+
public func lpushx(_ value: RESPValueConvertible, to key: String) -> EventLoopFuture<Int> {
139+
return send(command: "LPUSHX", with: [key, value])
140+
.mapFromRESP()
141+
}
142+
}
143+
144+
// MARK: Tail Operations
145+
146+
extension RedisCommandExecutor {
147+
/// Removes the last element in the list and returns it.
148+
///
149+
/// See [https://redis.io/commands/rpop](https://redis.io/commands/rpop)
150+
@inlinable
151+
public func rpop(from key: String) -> EventLoopFuture<RESPValue?> {
152+
return send(command: "RPOP", with: [key])
153+
.mapFromRESP()
154+
}
155+
156+
/// Inserts all values provided into the list stored at the key specified.
157+
/// - Note: This inserts the values at the tail of the list, for the head see `lpush(_:to:)`.
158+
///
159+
/// See [https://redis.io/commands/rpush](https://redis.io/commands/rpush)
160+
/// - Returns: The size of the list after adding the new elements.
161+
@inlinable
162+
public func rpush(_ values: [RESPValueConvertible], to key: String) -> EventLoopFuture<Int> {
163+
return send(command: "RPUSH", with: [key] + values)
164+
.mapFromRESP()
165+
}
166+
167+
/// Inserts the value at the head of the list only if the key exists and holds a list.
168+
/// - Note: This inserts the values at the tail of the list, for the head see `lpushx(_:to:)`.
169+
///
170+
/// See [https://redis.io/commands/rpushx](https://redis.io/commands/rpushx)
171+
/// - Returns: The length of the list after adding the new elements.
172+
@inlinable
173+
public func rpushx(_ value: RESPValueConvertible, to key: String) -> EventLoopFuture<Int> {
174+
return send(command: "RPUSHX", with: [key, value])
175+
.mapFromRESP()
176+
}
177+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
@testable import NIORedis
2+
import XCTest
3+
4+
final class ListCommandsTests: XCTestCase {
5+
private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1))
6+
deinit { try? redis.terminate() }
7+
8+
private var connection: RedisConnection!
9+
10+
override func setUp() {
11+
do {
12+
connection = try redis.makeConnection().wait()
13+
} catch {
14+
XCTFail("Failed to create NIORedisConnection!")
15+
}
16+
}
17+
18+
override func tearDown() {
19+
_ = try? connection.send(command: "FLUSHALL").wait()
20+
connection.close()
21+
connection = nil
22+
}
23+
24+
func test_llen() throws {
25+
var length = try connection.llen(of: #function).wait()
26+
XCTAssertEqual(length, 0)
27+
_ = try connection.lpush([30], to: #function).wait()
28+
length = try connection.llen(of: #function).wait()
29+
XCTAssertEqual(length, 1)
30+
}
31+
32+
func test_lindex() throws {
33+
XCTAssertThrowsError(try connection.lindex(#function, index: 0).wait())
34+
_ = try connection.lpush([10], to: #function).wait()
35+
let element = try connection.lindex(#function, index: 0).wait()
36+
XCTAssertEqual(Int(element), 10)
37+
}
38+
39+
func test_lset() throws {
40+
XCTAssertThrowsError(try connection.lset(#function, index: 0, to: 30).wait())
41+
_ = try connection.lpush([10], to: #function).wait()
42+
XCTAssertNoThrow(try connection.lset(#function, index: 0, to: 30).wait())
43+
let element = try connection.lindex(#function, index: 0).wait()
44+
XCTAssertEqual(Int(element), 30)
45+
}
46+
47+
func test_lrem() throws {
48+
_ = try connection.lpush([10, 10, 20, 30, 10], to: #function).wait()
49+
var count = try connection.lrem(10, from: #function, count: 2).wait()
50+
XCTAssertEqual(count, 2)
51+
count = try connection.lrem(10, from: #function, count: 2).wait()
52+
XCTAssertEqual(count, 1)
53+
}
54+
55+
func test_lrange() throws {
56+
var elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 10).wait()
57+
XCTAssertEqual(elements.count, 0)
58+
59+
_ = try connection.lpush([5, 4, 3, 2, 1], to: #function).wait()
60+
61+
elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 4).wait()
62+
XCTAssertEqual(elements.count, 5)
63+
XCTAssertEqual(Int(elements[0]), 1)
64+
XCTAssertEqual(Int(elements[4]), 5)
65+
66+
elements = try connection.lrange(of: #function, startIndex: 2, endIndex: 0).wait()
67+
XCTAssertEqual(elements.count, 0)
68+
69+
elements = try connection.lrange(of: #function, startIndex: 4, endIndex: 5).wait()
70+
XCTAssertEqual(elements.count, 1)
71+
72+
elements = try connection.lrange(of: #function, startIndex: 0, endIndex: -4).wait()
73+
XCTAssertEqual(elements.count, 2)
74+
}
75+
76+
func test_rpoplpush() throws {
77+
_ = try connection.lpush([10], to: "first").wait()
78+
_ = try connection.lpush([30], to: "second").wait()
79+
80+
var element = try connection.rpoplpush(from: "first", to: "second").wait()
81+
XCTAssertEqual(Int(element), 10)
82+
XCTAssertEqual(try connection.llen(of: "first").wait(), 0)
83+
XCTAssertEqual(try connection.llen(of: "second").wait(), 2)
84+
85+
element = try connection.rpoplpush(from: "second", to: "first").wait()
86+
XCTAssertEqual(Int(element), 30)
87+
XCTAssertEqual(try connection.llen(of: "second").wait(), 1)
88+
}
89+
90+
func test_linsert() throws {
91+
_ = try connection.lpush([10], to: #function).wait()
92+
93+
_ = try connection.linsert(20, into: #function, after: 10).wait()
94+
var elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 1)
95+
.map { response in response.compactMap { Int($0) } }
96+
.wait()
97+
XCTAssertEqual(elements, [10, 20])
98+
99+
_ = try connection.linsert(30, into: #function, before: 10).wait()
100+
elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 2)
101+
.map { response in response.compactMap { Int($0) } }
102+
.wait()
103+
XCTAssertEqual(elements, [30, 10, 20])
104+
}
105+
106+
func test_lpop() throws {
107+
_ = try connection.lpush([10, 20, 30], to: #function).wait()
108+
109+
let element = try connection.lpop(from: #function).wait()
110+
XCTAssertNotNil(element)
111+
XCTAssertEqual(Int(element ?? .null), 30)
112+
}
113+
114+
func test_lpush() throws {
115+
_ = try connection.rpush([10, 20, 30], to: #function).wait()
116+
117+
let size = try connection.lpush([100], to: #function).wait()
118+
let element = try connection.lindex(#function, index: 0).mapFromRESP(to: Int.self).wait()
119+
XCTAssertEqual(size, 4)
120+
XCTAssertEqual(element, 100)
121+
}
122+
123+
func test_lpushx() throws {
124+
var size = try connection.lpushx(10, to: #function).wait()
125+
XCTAssertEqual(size, 0)
126+
127+
_ = try connection.lpush([10], to: #function).wait()
128+
129+
size = try connection.lpushx(30, to: #function).wait()
130+
XCTAssertEqual(size, 2)
131+
let element = try connection.rpop(from: #function)
132+
.map { return Int($0 ?? .null) }
133+
.wait()
134+
XCTAssertEqual(element, 10)
135+
}
136+
137+
func test_rpop() throws {
138+
_ = try connection.lpush([10, 20, 30], to: #function).wait()
139+
140+
let element = try connection.rpop(from: #function).wait()
141+
XCTAssertNotNil(element)
142+
XCTAssertEqual(Int(element ?? .null), 10)
143+
}
144+
145+
func test_rpush() throws {
146+
_ = try connection.lpush([10, 20, 30], to: #function).wait()
147+
148+
let size = try connection.rpush([100], to: #function).wait()
149+
let element = try connection.lindex(#function, index: 3).mapFromRESP(to: Int.self).wait()
150+
XCTAssertEqual(size, 4)
151+
XCTAssertEqual(element, 100)
152+
}
153+
154+
func test_rpushx() throws {
155+
var size = try connection.rpushx(10, to: #function).wait()
156+
XCTAssertEqual(size, 0)
157+
158+
_ = try connection.rpush([10], to: #function).wait()
159+
160+
size = try connection.rpushx(30, to: #function).wait()
161+
XCTAssertEqual(size, 2)
162+
let element = try connection.lpop(from: #function)
163+
.map { return Int($0 ?? .null) }
164+
.wait()
165+
XCTAssertEqual(element, 10)
166+
}
167+
168+
static var allTests = [
169+
("test_llen", test_llen),
170+
("test_lindex", test_lindex),
171+
("test_lset", test_lset),
172+
("test_lrem", test_lrem),
173+
("test_lrange", test_lrange),
174+
("test_rpoplpush", test_rpoplpush),
175+
("test_linsert", test_linsert),
176+
("test_lpop", test_lpop),
177+
("test_lpush", test_lpush),
178+
("test_lpushx", test_lpushx),
179+
("test_rpop", test_rpop),
180+
("test_rpush", test_rpush),
181+
("test_rpushx", test_rpushx),
182+
]
183+
}

Tests/NIORedisTests/XCTestManifests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ public func allTests() -> [XCTestCaseEntry] {
1212
testCase(BasicCommandsTests.allTests),
1313
testCase(SetCommandsTests.allTests),
1414
testCase(RedisPipelineTests.allTests),
15-
testCase(HashCommandsTests.allTests)
15+
testCase(HashCommandsTests.allTests),
16+
testCase(ListCommandsTests.allTests)
1617
]
1718
}
1819
#endif

0 commit comments

Comments
 (0)