Skip to content

Commit f3a9b32

Browse files
authored
Merge pull request #19 from Mordil/hash-commands
Add hash convenience commands with unit tests
2 parents 17f7736 + 39feecb commit f3a9b32

File tree

3 files changed

+382
-1
lines changed

3 files changed

+382
-1
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import NIO
2+
3+
extension RedisCommandExecutor {
4+
/// Sets the hash field stored at the provided key with the value specified.
5+
///
6+
/// See [https://redis.io/commands/hset](https://redis.io/commands/hset)
7+
/// - Returns: `true` if the hash was created, `false` if it was updated.
8+
@inlinable
9+
public func hset(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
10+
return send(command: "HSET", with: [key, field, value])
11+
.mapFromRESP(to: Int.self)
12+
.map { return $0 == 1 }
13+
}
14+
15+
/// Sets the specified fields to the values provided, overwriting existing values.
16+
///
17+
/// See [https://redis.io/commands/hmset](https://redis.io/commands/hmset)
18+
@inlinable
19+
public func hmset(_ key: String, to fields: [String: String]) -> EventLoopFuture<Void> {
20+
assert(fields.count > 0, "At least 1 key-value pair should be specified")
21+
22+
let args: [RESPValueConvertible] = fields.reduce(into: [], { (result, element) in
23+
result.append(element.key)
24+
result.append(element.value)
25+
})
26+
27+
return send(command: "HMSET", with: [key] + args)
28+
.map { _ in () }
29+
}
30+
31+
/// Sets the specified hash field to the value provided only if the field does not exist.
32+
///
33+
/// See [https://redis.io/commands/hsetnx](https://redis.io/commands/hsetnx)
34+
/// - Returns: The success of setting the field's value.
35+
@inlinable
36+
public func hsetnx(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
37+
return send(command: "HSETNX", with: [key, field, value])
38+
.mapFromRESP(to: Int.self)
39+
.map { return $0 == 1 }
40+
}
41+
42+
/// Gets the value stored in the hash field at the key provided.
43+
///
44+
/// See [https://redis.io/commands/hget](https://redis.io/commands/hget)
45+
@inlinable
46+
public func hget(_ key: String, field: String) -> EventLoopFuture<String?> {
47+
return send(command: "HGET", with: [key, field])
48+
.map { return String($0) }
49+
}
50+
51+
/// Returns the values stored in the fields specified at the key provided.
52+
///
53+
/// See [https://redis.io/commands/hmget](https://redis.io/commands/hmget)
54+
/// - Returns: A list of values in the same order as the `fields` argument.
55+
@inlinable
56+
public func hmget(_ key: String, fields: [String]) -> EventLoopFuture<[String?]> {
57+
assert(fields.count > 0, "At least 1 field should be specified")
58+
59+
return send(command: "HMGET", with: [key] + fields)
60+
.mapFromRESP(to: [RESPValue].self)
61+
.map { return $0.map(String.init) }
62+
}
63+
64+
/// Returns all the fields and values stored at the provided key.
65+
///
66+
/// See [https://redis.io/commands/hgetall](https://redis.io/commands/hgetall)
67+
/// - Returns: A key-value pair list of fields and their values.
68+
@inlinable
69+
public func hgetall(from key: String) -> EventLoopFuture<[String: String]> {
70+
return send(command: "HGETALL", with: [key])
71+
.mapFromRESP(to: [String].self)
72+
.map(Self.mapHashResponseToDictionary)
73+
}
74+
75+
/// Removes the specified fields from the hash stored at the key provided.
76+
///
77+
/// See [https://redis.io/commands/hdel](https://redis.io/commands/hdel)
78+
/// - Returns: The number of fields that were deleted.
79+
@inlinable
80+
public func hdel(_ key: String, fields: [String]) -> EventLoopFuture<Int> {
81+
assert(fields.count > 0, "At least 1 field should be specified")
82+
83+
return send(command: "HDEL", with: [key] + fields)
84+
.mapFromRESP()
85+
}
86+
87+
/// Checks if the provided key and field exist.
88+
///
89+
/// See [https://redis.io/commands/hexists](https://redis.io/commands/hexists)
90+
@inlinable
91+
public func hexists(_ key: String, field: String) -> EventLoopFuture<Bool> {
92+
return send(command: "HEXISTS", with: [key, field])
93+
.mapFromRESP(to: Int.self)
94+
.map { return $0 == 1 }
95+
}
96+
97+
/// Returns the number of fields contained in the hash stored at the key provided.
98+
///
99+
/// See [https://redis.io/commands/hlen](https://redis.io/commands/hlen)
100+
/// - Returns: The number of fields in the hash, or 0 if the key doesn't exist.
101+
@inlinable
102+
public func hlen(of key: String) -> EventLoopFuture<Int> {
103+
return send(command: "HLEN", with: [key])
104+
.mapFromRESP()
105+
}
106+
107+
/// Returns hash field's value length as a string, stored at the provided key.
108+
///
109+
/// See [https://redis.io/commands/hstrlen](https://redis.io/commands/hstrlen)
110+
@inlinable
111+
public func hstrlen(of key: String, field: String) -> EventLoopFuture<Int> {
112+
return send(command: "HSTRLEN", with: [key, field])
113+
.mapFromRESP()
114+
}
115+
116+
/// Returns all field names in the hash stored at the key provided.
117+
///
118+
/// See [https://redis.io/commands/hkeys](https://redis.io/commands/hkeys)
119+
/// - Returns: An array of field names, or an empty array.
120+
@inlinable
121+
public func hkeys(storedAt key: String) -> EventLoopFuture<[String]> {
122+
return send(command: "HKEYS", with: [key])
123+
.mapFromRESP()
124+
}
125+
126+
/// Returns all of the field values stored in hash at the key provided.
127+
///
128+
/// See [https://redis.io/commands/hvals](https://redis.io/commands/hvals)
129+
@inlinable
130+
public func hvals(storedAt key: String) -> EventLoopFuture<[String]> {
131+
return send(command: "HVALS", with: [key])
132+
.mapFromRESP()
133+
}
134+
135+
/// Increments the field value stored at the key provided, and returns the new value.
136+
///
137+
/// See [https://redis.io/commands/hincrby](https://redis.io/commands/hincrby)
138+
@inlinable
139+
public func hincrby(_ key: String, field: String, by amount: Int) -> EventLoopFuture<Int> {
140+
return send(command: "HINCRBY", with: [key, field, amount])
141+
.mapFromRESP()
142+
}
143+
144+
/// Increments the field value stored at the key provided, and returns the new value.
145+
///
146+
/// See [https://redis.io/commands/hincrbyfloat](https://redis.io/commands/hincrbyfloat)
147+
@inlinable
148+
public func hincrbyfloat<T: BinaryFloatingPoint>(_ key: String, field: String, by amount: T) -> EventLoopFuture<T>
149+
where T: RESPValueConvertible
150+
{
151+
return send(command: "HINCRBYFLOAT", with: [key, field, amount])
152+
.mapFromRESP()
153+
}
154+
155+
/// Incrementally iterates over all fields in the hash stored at the key provided.
156+
///
157+
/// [https://redis.io/commands/scan](https://redis.io/commands/scan)
158+
/// - Parameters:
159+
/// - key: The key of the hash.
160+
/// - atPosition: The position to start the scan from.
161+
/// - count: The number of elements to advance by. Redis default is 10.
162+
/// - matching: A glob-style pattern to filter values to be selected from the result set.
163+
/// - Returns: A cursor position for additional invocations with a limited collection of values stored at the keys.
164+
@inlinable
165+
public func hscan(
166+
_ key: String,
167+
atPosition pos: Int = 0,
168+
count: Int? = nil,
169+
matching match: String? = nil) -> EventLoopFuture<(Int, [String: String])>
170+
{
171+
return _scan(command: "HSCAN", resultType: [String].self, key, pos, count, match)
172+
.map {
173+
let values = Self.mapHashResponseToDictionary($0.1)
174+
return ($0.0, values)
175+
}
176+
}
177+
}
178+
179+
extension RedisCommandExecutor {
180+
@inline(__always)
181+
@usableFromInline
182+
static func mapHashResponseToDictionary(_ values: [String]) -> [String: String] {
183+
guard values.count > 0 else { return [:] }
184+
185+
var result: [String: String] = [:]
186+
187+
var index = 0
188+
repeat {
189+
let field = values[index]
190+
let value = values[index + 1]
191+
result[field] = value
192+
index += 2
193+
} while (index < values.count)
194+
195+
return result
196+
}
197+
}
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 HashCommandsTests: 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_hset() throws {
25+
var result = try connection.hset(#function, field: "test", to: "\(#line)").wait()
26+
XCTAssertTrue(result)
27+
result = try connection.hset(#function, field: "test", to: "\(#line)").wait()
28+
XCTAssertFalse(result)
29+
}
30+
31+
func test_hmset() throws {
32+
XCTAssertNoThrow(try connection.hmset(#function, to: ["field": "30"]).wait())
33+
let value = try connection.hget(#function, field: "field").wait()
34+
XCTAssertEqual(value, "30")
35+
}
36+
37+
func test_hsetnx() throws {
38+
var success = try connection.hsetnx(#function, field: "field", to: "foo").wait()
39+
XCTAssertTrue(success)
40+
success = try connection.hsetnx(#function, field: "field", to: "30").wait()
41+
XCTAssertFalse(success)
42+
43+
let value = try connection.hget(#function, field: "field").wait()
44+
XCTAssertEqual(value, "foo")
45+
}
46+
47+
func test_hget() throws {
48+
_ = try connection.hset(#function, field: "test", to: "30").wait()
49+
let value = try connection.hget(#function, field: "test").wait()
50+
XCTAssertEqual(value, "30")
51+
}
52+
53+
func test_hmget() throws {
54+
_ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait()
55+
let values = try connection.hmget(#function, fields: ["first", "second", "fake"]).wait()
56+
XCTAssertEqual(values[0], "foo")
57+
XCTAssertEqual(values[1], "bar")
58+
XCTAssertNil(values[2])
59+
}
60+
61+
func test_hgetall() throws {
62+
let dataset = ["first": "foo", "second": "bar"]
63+
_ = try connection.hmset(#function, to: dataset).wait()
64+
let hashes = try connection.hgetall(from: #function).wait()
65+
XCTAssertEqual(hashes, dataset)
66+
}
67+
68+
func test_hdel() throws {
69+
_ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait()
70+
let count = try connection.hdel(#function, fields: ["first", "second", "fake"]).wait()
71+
XCTAssertEqual(count, 2)
72+
}
73+
74+
func test_hexists() throws {
75+
var exists = try connection.hexists(#function, field: "foo").wait()
76+
XCTAssertFalse(exists)
77+
_ = try connection.hset(#function, field: "foo", to: "\(#line)").wait()
78+
exists = try connection.hexists(#function, field: "foo").wait()
79+
XCTAssertTrue(exists)
80+
}
81+
82+
func test_hlen() throws {
83+
var count = try connection.hlen(of: #function).wait()
84+
XCTAssertEqual(count, 0)
85+
_ = try connection.hset(#function, field: "first", to: "\(#line)").wait()
86+
count = try connection.hlen(of: #function).wait()
87+
XCTAssertEqual(count, 1)
88+
_ = try connection.hset(#function, field: "second", to: "\(#line)").wait()
89+
count = try connection.hlen(of: #function).wait()
90+
XCTAssertEqual(count, 2)
91+
}
92+
93+
func test_hstrlen() throws {
94+
_ = try connection.hset(#function, field: "first", to: "foo").wait()
95+
var size = try connection.hstrlen(of: #function, field: "first").wait()
96+
XCTAssertEqual(size, 3)
97+
_ = try connection.hset(#function, field: "second", to: "300").wait()
98+
size = try connection.hstrlen(of: #function, field: "second").wait()
99+
XCTAssertEqual(size, 3)
100+
}
101+
102+
func test_hkeys() throws {
103+
let dataset = [
104+
"first": "3",
105+
"second": "foo"
106+
]
107+
_ = try connection.hmset(#function, to: dataset).wait()
108+
let keys = try connection.hkeys(storedAt: #function).wait()
109+
XCTAssertEqual(Array(dataset.keys), keys)
110+
}
111+
112+
func test_hvals() throws {
113+
let dataset = [
114+
"first": "3",
115+
"second": "foo"
116+
]
117+
_ = try connection.hmset(#function, to: dataset).wait()
118+
let values = try connection.hvals(storedAt: #function).wait()
119+
XCTAssertEqual(Array(dataset.values), values)
120+
}
121+
122+
func test_hincrby() throws {
123+
_ = try connection.hset(#function, field: "first", to: "3").wait()
124+
var value = try connection.hincrby(#function, field: "first", by: 10).wait()
125+
XCTAssertEqual(value, 13)
126+
value = try connection.hincrby(#function, field: "first", by: -15).wait()
127+
XCTAssertEqual(value, -2)
128+
}
129+
130+
func test_hincrbyfloat() throws {
131+
_ = try connection.hset(#function, field: "first", to: "3.14").wait()
132+
133+
let double = try connection.hincrbyfloat(#function, field: "first", by: Double(3.14)).wait()
134+
XCTAssertEqual(double, 6.28)
135+
136+
let float = try connection.hincrbyfloat(#function, field: "first", by: Float(-10.23523)).wait()
137+
XCTAssertEqual(float, -3.95523)
138+
}
139+
140+
func test_hscan() throws {
141+
var dataset: [String: String] = [:]
142+
for index in 1...15 {
143+
let key = "key\(index)\(index % 2 == 0 ? "_even" : "_odd")"
144+
dataset[key] = "\(index)"
145+
}
146+
_ = try connection.hmset(#function, to: dataset).wait()
147+
148+
var (cursor, fields) = try connection.hscan(#function, count: 5).wait()
149+
XCTAssertGreaterThanOrEqual(cursor, 0)
150+
XCTAssertGreaterThanOrEqual(fields.count, 5)
151+
152+
(_, fields) = try connection.hscan(#function, atPosition: cursor, count: 8).wait()
153+
XCTAssertGreaterThanOrEqual(fields.count, 8)
154+
155+
(cursor, fields) = try connection.hscan(#function, matching: "*_odd").wait()
156+
XCTAssertGreaterThanOrEqual(cursor, 0)
157+
XCTAssertGreaterThanOrEqual(fields.count, 1)
158+
XCTAssertLessThanOrEqual(fields.count, 8)
159+
160+
(cursor, fields) = try connection.hscan(#function, matching: "*_ev*").wait()
161+
XCTAssertGreaterThanOrEqual(cursor, 0)
162+
XCTAssertGreaterThanOrEqual(fields.count, 1)
163+
XCTAssertLessThanOrEqual(fields.count, 7)
164+
}
165+
166+
static var allTests = [
167+
("test_hset", test_hset),
168+
("test_hmset", test_hmset),
169+
("test_hsetnx", test_hsetnx),
170+
("test_hget", test_hget),
171+
("test_hmget", test_hmget),
172+
("test_hgetall", test_hgetall),
173+
("test_hdel", test_hdel),
174+
("test_hexists", test_hexists),
175+
("test_hlen", test_hlen),
176+
("test_hstrlen", test_hstrlen),
177+
("test_hkeys", test_hkeys),
178+
("test_hvals", test_hvals),
179+
("test_hincrby", test_hincrby),
180+
("test_hincrbyfloat", test_hincrbyfloat),
181+
("test_hscan", test_hscan),
182+
]
183+
}

Tests/NIORedisTests/XCTestManifests.swift

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

0 commit comments

Comments
 (0)