mirror of
https://github.com/swift-server/RediStack.git
synced 2026-06-02 07:37:33 +00:00
Add hash convenience commands with unit tests
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import NIO
|
||||
|
||||
extension RedisCommandExecutor {
|
||||
/// Sets the hash field stored at the provided key with the value specified.
|
||||
///
|
||||
/// See [https://redis.io/commands/hset](https://redis.io/commands/hset)
|
||||
/// - Returns: `true` if the hash was created, `false` if it was updated.
|
||||
@inlinable
|
||||
public func hset(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
|
||||
return send(command: "HSET", with: [key, field, value])
|
||||
.mapFromRESP(to: Int.self)
|
||||
.map { return $0 == 1 }
|
||||
}
|
||||
|
||||
/// Sets the specified fields to the values provided, overwriting existing values.
|
||||
///
|
||||
/// See [https://redis.io/commands/hmset](https://redis.io/commands/hmset)
|
||||
@inlinable
|
||||
public func hmset(_ key: String, to fields: [String: String]) -> EventLoopFuture<Void> {
|
||||
assert(fields.count > 0, "At least 1 key-value pair should be specified")
|
||||
|
||||
let args: [RESPValueConvertible] = fields.reduce(into: [], { (result, element) in
|
||||
result.append(element.key)
|
||||
result.append(element.value)
|
||||
})
|
||||
|
||||
return send(command: "HMSET", with: [key] + args)
|
||||
.map { _ in () }
|
||||
}
|
||||
|
||||
/// Sets the specified hash field to the value provided only if the field does not exist.
|
||||
///
|
||||
/// See [https://redis.io/commands/hsetnx](https://redis.io/commands/hsetnx)
|
||||
/// - Returns: The success of setting the field's value.
|
||||
@inlinable
|
||||
public func hsetnx(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
|
||||
return send(command: "HSETNX", with: [key, field, value])
|
||||
.mapFromRESP(to: Int.self)
|
||||
.map { return $0 == 1 }
|
||||
}
|
||||
|
||||
/// Gets the value stored in the hash field at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hget](https://redis.io/commands/hget)
|
||||
@inlinable
|
||||
public func hget(_ key: String, field: String) -> EventLoopFuture<String?> {
|
||||
return send(command: "HGET", with: [key, field])
|
||||
.map { return String($0) }
|
||||
}
|
||||
|
||||
/// Returns the values stored in the fields specified at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hmget](https://redis.io/commands/hmget)
|
||||
/// - Returns: A list of values in the same order as the `fields` argument.
|
||||
@inlinable
|
||||
public func hmget(_ key: String, fields: [String]) -> EventLoopFuture<[String?]> {
|
||||
assert(fields.count > 0, "At least 1 field should be specified")
|
||||
|
||||
return send(command: "HMGET", with: [key] + fields)
|
||||
.mapFromRESP(to: [RESPValue].self)
|
||||
.map { return $0.map(String.init) }
|
||||
}
|
||||
|
||||
/// Returns all the fields and values stored at the provided key.
|
||||
///
|
||||
/// See [https://redis.io/commands/hgetall](https://redis.io/commands/hgetall)
|
||||
/// - Returns: A key-value pair list of fields and their values.
|
||||
@inlinable
|
||||
public func hgetall(from key: String) -> EventLoopFuture<[String: String]> {
|
||||
return send(command: "HGETALL", with: [key])
|
||||
.mapFromRESP(to: [String].self)
|
||||
.map(Self.mapHashResponseToDictionary)
|
||||
}
|
||||
|
||||
/// Removes the specified fields from the hash stored at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hdel](https://redis.io/commands/hdel)
|
||||
/// - Returns: The number of fields that were deleted.
|
||||
@inlinable
|
||||
public func hdel(_ key: String, fields: [String]) -> EventLoopFuture<Int> {
|
||||
assert(fields.count > 0, "At least 1 field should be specified")
|
||||
|
||||
return send(command: "HDEL", with: [key] + fields)
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Checks if the provided key and field exist.
|
||||
///
|
||||
/// See [https://redis.io/commands/hexists](https://redis.io/commands/hexists)
|
||||
@inlinable
|
||||
public func hexists(_ key: String, field: String) -> EventLoopFuture<Bool> {
|
||||
return send(command: "HEXISTS", with: [key, field])
|
||||
.mapFromRESP(to: Int.self)
|
||||
.map { return $0 == 1 }
|
||||
}
|
||||
|
||||
/// Returns the number of fields contained in the hash stored at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hlen](https://redis.io/commands/hlen)
|
||||
/// - Returns: The number of fields in the hash, or 0 if the key doesn't exist.
|
||||
@inlinable
|
||||
public func hlen(of key: String) -> EventLoopFuture<Int> {
|
||||
return send(command: "HLEN", with: [key])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Returns hash field's value length as a string, stored at the provided key.
|
||||
///
|
||||
/// See [https://redis.io/commands/hstrlen](https://redis.io/commands/hstrlen)
|
||||
@inlinable
|
||||
public func hstrlen(of key: String, field: String) -> EventLoopFuture<Int> {
|
||||
return send(command: "HSTRLEN", with: [key, field])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Returns all field names in the hash stored at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hkeys](https://redis.io/commands/hkeys)
|
||||
/// - Returns: An array of field names, or an empty array.
|
||||
@inlinable
|
||||
public func hkeys(storedAt key: String) -> EventLoopFuture<[String]> {
|
||||
return send(command: "HKEYS", with: [key])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Returns all of the field values stored in hash at the key provided.
|
||||
///
|
||||
/// See [https://redis.io/commands/hvals](https://redis.io/commands/hvals)
|
||||
@inlinable
|
||||
public func hvals(storedAt key: String) -> EventLoopFuture<[String]> {
|
||||
return send(command: "HVALS", with: [key])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Increments the field value stored at the key provided, and returns the new value.
|
||||
///
|
||||
/// See [https://redis.io/commands/hincrby](https://redis.io/commands/hincrby)
|
||||
@inlinable
|
||||
public func hincrby(_ key: String, field: String, by amount: Int) -> EventLoopFuture<Int> {
|
||||
return send(command: "HINCRBY", with: [key, field, amount])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Increments the field value stored at the key provided, and returns the new value.
|
||||
///
|
||||
/// See [https://redis.io/commands/hincrbyfloat](https://redis.io/commands/hincrbyfloat)
|
||||
@inlinable
|
||||
public func hincrbyfloat<T: BinaryFloatingPoint>(_ key: String, field: String, by amount: T) -> EventLoopFuture<T>
|
||||
where T: RESPValueConvertible
|
||||
{
|
||||
return send(command: "HINCRBYFLOAT", with: [key, field, amount])
|
||||
.mapFromRESP()
|
||||
}
|
||||
|
||||
/// Incrementally iterates over all fields in the hash stored at the key provided.
|
||||
///
|
||||
/// [https://redis.io/commands/scan](https://redis.io/commands/scan)
|
||||
/// - Parameters:
|
||||
/// - key: The key of the hash.
|
||||
/// - atPosition: The position to start the scan from.
|
||||
/// - count: The number of elements to advance by. Redis default is 10.
|
||||
/// - matching: A glob-style pattern to filter values to be selected from the result set.
|
||||
/// - Returns: A cursor position for additional invocations with a limited collection of values stored at the keys.
|
||||
@inlinable
|
||||
public func hscan(
|
||||
_ key: String,
|
||||
atPosition pos: Int = 0,
|
||||
count: Int? = nil,
|
||||
matching match: String? = nil) -> EventLoopFuture<(Int, [String: String])>
|
||||
{
|
||||
return _scan(command: "HSCAN", resultType: [String].self, key, pos, count, match)
|
||||
.map {
|
||||
let values = Self.mapHashResponseToDictionary($0.1)
|
||||
return ($0.0, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RedisCommandExecutor {
|
||||
@inline(__always)
|
||||
@usableFromInline
|
||||
static func mapHashResponseToDictionary(_ values: [String]) -> [String: String] {
|
||||
guard values.count > 0 else { return [:] }
|
||||
|
||||
var result: [String: String] = [:]
|
||||
|
||||
var index = 0
|
||||
repeat {
|
||||
let field = values[index]
|
||||
let value = values[index + 1]
|
||||
result[field] = value
|
||||
index += 2
|
||||
} while (index < values.count)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
@testable import NIORedis
|
||||
import XCTest
|
||||
|
||||
final class HashCommandsTests: XCTestCase {
|
||||
private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1))
|
||||
deinit { try? redis.terminate() }
|
||||
|
||||
private var connection: RedisConnection!
|
||||
|
||||
override func setUp() {
|
||||
do {
|
||||
connection = try redis.makeConnection().wait()
|
||||
} catch {
|
||||
XCTFail("Failed to create NIORedisConnection!")
|
||||
}
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
_ = try? connection.send(command: "FLUSHALL").wait()
|
||||
connection.close()
|
||||
connection = nil
|
||||
}
|
||||
|
||||
func test_hset() throws {
|
||||
var result = try connection.hset(#function, field: "test", to: "\(#line)").wait()
|
||||
XCTAssertTrue(result)
|
||||
result = try connection.hset(#function, field: "test", to: "\(#line)").wait()
|
||||
XCTAssertFalse(result)
|
||||
}
|
||||
|
||||
func test_hmset() throws {
|
||||
XCTAssertNoThrow(try connection.hmset(#function, to: ["field": "30"]).wait())
|
||||
let value = try connection.hget(#function, field: "field").wait()
|
||||
XCTAssertEqual(value, "30")
|
||||
}
|
||||
|
||||
func test_hsetnx() throws {
|
||||
var success = try connection.hsetnx(#function, field: "field", to: "foo").wait()
|
||||
XCTAssertTrue(success)
|
||||
success = try connection.hsetnx(#function, field: "field", to: "30").wait()
|
||||
XCTAssertFalse(success)
|
||||
|
||||
let value = try connection.hget(#function, field: "field").wait()
|
||||
XCTAssertEqual(value, "foo")
|
||||
}
|
||||
|
||||
func test_hget() throws {
|
||||
_ = try connection.hset(#function, field: "test", to: "30").wait()
|
||||
let value = try connection.hget(#function, field: "test").wait()
|
||||
XCTAssertEqual(value, "30")
|
||||
}
|
||||
|
||||
func test_hmget() throws {
|
||||
_ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait()
|
||||
let values = try connection.hmget(#function, fields: ["first", "second", "fake"]).wait()
|
||||
XCTAssertEqual(values[0], "foo")
|
||||
XCTAssertEqual(values[1], "bar")
|
||||
XCTAssertNil(values[2])
|
||||
}
|
||||
|
||||
func test_hgetall() throws {
|
||||
let dataset = ["first": "foo", "second": "bar"]
|
||||
_ = try connection.hmset(#function, to: dataset).wait()
|
||||
let hashes = try connection.hgetall(from: #function).wait()
|
||||
XCTAssertEqual(hashes, dataset)
|
||||
}
|
||||
|
||||
func test_hdel() throws {
|
||||
_ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait()
|
||||
let count = try connection.hdel(#function, fields: ["first", "second", "fake"]).wait()
|
||||
XCTAssertEqual(count, 2)
|
||||
}
|
||||
|
||||
func test_hexists() throws {
|
||||
var exists = try connection.hexists(#function, field: "foo").wait()
|
||||
XCTAssertFalse(exists)
|
||||
_ = try connection.hset(#function, field: "foo", to: "\(#line)").wait()
|
||||
exists = try connection.hexists(#function, field: "foo").wait()
|
||||
XCTAssertTrue(exists)
|
||||
}
|
||||
|
||||
func test_hlen() throws {
|
||||
var count = try connection.hlen(of: #function).wait()
|
||||
XCTAssertEqual(count, 0)
|
||||
_ = try connection.hset(#function, field: "first", to: "\(#line)").wait()
|
||||
count = try connection.hlen(of: #function).wait()
|
||||
XCTAssertEqual(count, 1)
|
||||
_ = try connection.hset(#function, field: "second", to: "\(#line)").wait()
|
||||
count = try connection.hlen(of: #function).wait()
|
||||
XCTAssertEqual(count, 2)
|
||||
}
|
||||
|
||||
func test_hstrlen() throws {
|
||||
_ = try connection.hset(#function, field: "first", to: "foo").wait()
|
||||
var size = try connection.hstrlen(of: #function, field: "first").wait()
|
||||
XCTAssertEqual(size, 3)
|
||||
_ = try connection.hset(#function, field: "second", to: "300").wait()
|
||||
size = try connection.hstrlen(of: #function, field: "second").wait()
|
||||
XCTAssertEqual(size, 3)
|
||||
}
|
||||
|
||||
func test_hkeys() throws {
|
||||
let dataset = [
|
||||
"first": "3",
|
||||
"second": "foo"
|
||||
]
|
||||
_ = try connection.hmset(#function, to: dataset).wait()
|
||||
let keys = try connection.hkeys(storedAt: #function).wait()
|
||||
XCTAssertEqual(Array(dataset.keys), keys)
|
||||
}
|
||||
|
||||
func test_hvals() throws {
|
||||
let dataset = [
|
||||
"first": "3",
|
||||
"second": "foo"
|
||||
]
|
||||
_ = try connection.hmset(#function, to: dataset).wait()
|
||||
let values = try connection.hvals(storedAt: #function).wait()
|
||||
XCTAssertEqual(Array(dataset.values), values)
|
||||
}
|
||||
|
||||
func test_hincrby() throws {
|
||||
_ = try connection.hset(#function, field: "first", to: "3").wait()
|
||||
var value = try connection.hincrby(#function, field: "first", by: 10).wait()
|
||||
XCTAssertEqual(value, 13)
|
||||
value = try connection.hincrby(#function, field: "first", by: -15).wait()
|
||||
XCTAssertEqual(value, -2)
|
||||
}
|
||||
|
||||
func test_hincrbyfloat() throws {
|
||||
_ = try connection.hset(#function, field: "first", to: "3.14").wait()
|
||||
|
||||
let double = try connection.hincrbyfloat(#function, field: "first", by: Double(3.14)).wait()
|
||||
XCTAssertEqual(double, 6.28)
|
||||
|
||||
let float = try connection.hincrbyfloat(#function, field: "first", by: Float(-10.23523)).wait()
|
||||
XCTAssertEqual(float, -3.95523)
|
||||
}
|
||||
|
||||
func test_hscan() throws {
|
||||
var dataset: [String: String] = [:]
|
||||
for index in 1...15 {
|
||||
let key = "key\(index)\(index % 2 == 0 ? "_even" : "_odd")"
|
||||
dataset[key] = "\(index)"
|
||||
}
|
||||
_ = try connection.hmset(#function, to: dataset).wait()
|
||||
|
||||
var (cursor, fields) = try connection.hscan(#function, count: 5).wait()
|
||||
XCTAssertGreaterThanOrEqual(cursor, 0)
|
||||
XCTAssertGreaterThanOrEqual(fields.count, 5)
|
||||
|
||||
(_, fields) = try connection.hscan(#function, atPosition: cursor, count: 8).wait()
|
||||
XCTAssertGreaterThanOrEqual(fields.count, 8)
|
||||
|
||||
(cursor, fields) = try connection.hscan(#function, matching: "*_odd").wait()
|
||||
XCTAssertGreaterThanOrEqual(cursor, 0)
|
||||
XCTAssertGreaterThanOrEqual(fields.count, 1)
|
||||
XCTAssertLessThanOrEqual(fields.count, 8)
|
||||
|
||||
(cursor, fields) = try connection.hscan(#function, matching: "*_ev*").wait()
|
||||
XCTAssertGreaterThanOrEqual(cursor, 0)
|
||||
XCTAssertGreaterThanOrEqual(fields.count, 1)
|
||||
XCTAssertLessThanOrEqual(fields.count, 7)
|
||||
}
|
||||
|
||||
static var allTests = [
|
||||
("test_hset", test_hset),
|
||||
("test_hmset", test_hmset),
|
||||
("test_hsetnx", test_hsetnx),
|
||||
("test_hget", test_hget),
|
||||
("test_hmget", test_hmget),
|
||||
("test_hgetall", test_hgetall),
|
||||
("test_hdel", test_hdel),
|
||||
("test_hexists", test_hexists),
|
||||
("test_hlen", test_hlen),
|
||||
("test_hstrlen", test_hstrlen),
|
||||
("test_hkeys", test_hkeys),
|
||||
("test_hvals", test_hvals),
|
||||
("test_hincrby", test_hincrby),
|
||||
("test_hincrbyfloat", test_hincrbyfloat),
|
||||
("test_hscan", test_hscan),
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,8 @@ public func allTests() -> [XCTestCaseEntry] {
|
||||
testCase(RESPEncoderParsingTests.allTests),
|
||||
testCase(BasicCommandsTests.allTests),
|
||||
testCase(SetCommandsTests.allTests),
|
||||
testCase(RedisPipelineTests.allTests)
|
||||
testCase(RedisPipelineTests.allTests),
|
||||
testCase(HashCommandsTests.allTests)
|
||||
]
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user