diff --git a/Sources/NIORedis/Commands/BasicCommands.swift b/Sources/NIORedis/Commands/BasicCommands.swift index a8a47fd..ad88899 100644 --- a/Sources/NIORedis/Commands/BasicCommands.swift +++ b/Sources/NIORedis/Commands/BasicCommands.swift @@ -87,6 +87,16 @@ extension RedisCommandExecutor { .map { return $0.string } } + /// Returns the values of all specified keys, using `.null` to represent non-existant values. + /// + /// See [https://redis.io/commands/mget](https://redis.io/commands/mget) + public func mget(_ keys: [String]) -> EventLoopFuture<[RESPValue]> { + assert(keys.count > 0, "At least 1 key should be provided.") + + return send(command: "MGET", with: keys) + .mapFromRESP() + } + /// Set key to hold the string value. /// If key already holds a value, it is overwritten, regardless of its type. /// Any previous time to live associated with the key is discarded on successful SET operation. @@ -96,4 +106,86 @@ extension RedisCommandExecutor { return send(command: "SET", with: [key, value]) .map { _ in return () } } + + /// Sets each key to the respective new value, overwriting existing values. + /// + /// - Note: Use `msetnx` if you don't want to overwrite values. + /// + /// See [https://redis.io/commands/mset](https://redis.io/commands/mset) + public func mset(_ operations: [String: RESPValueConvertible]) -> EventLoopFuture { + assert(operations.count > 0, "At least 1 key-value pair should be provided.") + + let args = _convertMSET(operations) + return send(command: "MSET", with: args) + .map { _ in return () } + } + + /// If every key does not exist, sets each key to the respective new value. + /// + /// See [https://redis.io/commands/msetnx](https://redis.io/commands/msetnx) + public func msetnx(_ operations: [String: RESPValueConvertible]) -> EventLoopFuture { + assert(operations.count > 0, "At least 1 key-value pair should be provided.") + + let args = _convertMSET(operations) + return send(command: "MSETNX", with: args) + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } + } + + @inline(__always) + private func _convertMSET(_ source: [String: RESPValueConvertible]) -> [RESPValueConvertible] { + return source.reduce(into: [RESPValueConvertible](), { (result, element) in + result.append(element.key) + result.append(element.value) + }) + } +} + +extension RedisCommandExecutor { + /// Increments the stored value by 1 and returns the new value. + /// + /// See [https://redis.io/commands/incr](https://redis.io/commands/incr) + /// - Returns: The new value after the operation. + public func increment(_ key: String) -> EventLoopFuture { + return send(command: "INCR", with: [key]) + .mapFromRESP() + } + + /// Increments the stored value by the amount desired and returns the new value. + /// + /// See [https://redis.io/commands/incrby](https://redis.io/commands/incrby) + /// - Returns: The new value after the operation. + public func increment(_ key: String, by count: Int) -> EventLoopFuture { + return send(command: "INCRBY", with: [key, count]) + .mapFromRESP() + } + + /// Increments the stored value by the amount desired and returns the new value. + /// + /// See [https://redis.io/commands/incrbyfloat](https://redis.io/commands/incrbyfloat) + /// - Returns: The new value after the operation. + public func increment(_ key: String, by count: T) -> EventLoopFuture + where T: RESPValueConvertible + { + return send(command: "INCRBYFLOAT", with: [key, count]) + .mapFromRESP() + } + + /// Decrements the stored value by 1 and returns the new value. + /// + /// See [https://redis.io/commands/decr](https://redis.io/commands/decr) + /// - Returns: The new value after the operation. + public func decrement(_ key: String) -> EventLoopFuture { + return send(command: "DECR", with: [key]) + .mapFromRESP() + } + + /// Decrements the stored valye by the amount desired and returns the new value. + /// + /// See [https://redis.io/commands/decrby](https://redis.io/commands/decrby) + /// - Returns: The new value after the operation. + public func decrement(_ key: String, by count: Int) -> EventLoopFuture { + return send(command: "DECRBY", with: [key, count]) + .mapFromRESP() + } } diff --git a/Tests/NIORedisTests/Commands/BasicCommandsTests.swift b/Tests/NIORedisTests/Commands/BasicCommandsTests.swift index 9d1360e..9a764d8 100644 --- a/Tests/NIORedisTests/Commands/BasicCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/BasicCommandsTests.swift @@ -100,6 +100,95 @@ final class BasicCommandsTests: XCTestCase { XCTAssertEqual(first, "100") } + func test_increment() throws { + var result = try connection?.increment(#function).wait() + XCTAssertEqual(result, 1) + result = try connection?.increment(#function).wait() + XCTAssertEqual(result, 2) + } + + func test_incrementBy() throws { + var result = try connection?.increment(#function, by: 10).wait() + XCTAssertEqual(result, 10) + result = try connection?.increment(#function, by: -3).wait() + XCTAssertEqual(result, 7) + result = try connection?.increment(#function, by: 0).wait() + XCTAssertEqual(result, 7) + } + + func test_incrementByFloat() throws { + var float = try connection?.increment(#function, by: Float(3.0)).wait() + XCTAssertEqual(float, 3.0) + float = try connection?.increment(#function, by: Float(-10.135901)).wait() + XCTAssertEqual(float, -7.135901) + + var double = try connection?.increment(#function, by: Double(10.2839)).wait() + XCTAssertEqual(double, 3.147999) + double = try connection?.increment(#function, by: Double(15.2938)).wait() + XCTAssertEqual(double, 18.441799) + } + + func test_decrement() throws { + var result = try connection?.decrement(#function).wait() + XCTAssertEqual(result, -1) + result = try connection?.decrement(#function).wait() + XCTAssertEqual(result, -2) + } + + func test_decrementBy() throws { + var result = try connection?.decrement(#function, by: -10).wait() + XCTAssertEqual(result, 10) + result = try connection?.decrement(#function, by: 3).wait() + XCTAssertEqual(result, 7) + result = try connection?.decrement(#function, by: 0).wait() + XCTAssertEqual(result, 7) + } + + func test_mget() throws { + let keys = ["one", "two"] + try keys.forEach { _ = try connection?.set($0, to: $0).wait() } + + let values = try connection?.mget(keys + ["empty"]).wait() + XCTAssertEqual(values?.count, 3) + XCTAssertEqual(values?[0].string, "one") + XCTAssertEqual(values?[1].string, "two") + XCTAssertEqual(values?[2].isNull, true) + + XCTAssertEqual(try connection?.mget(["empty", #function]).wait().count, 2) + } + + func test_mset() throws { + let data = [ + "first": 1, + "second": 2 + ] + XCTAssertNoThrow(try connection?.mset(data).wait()) + let values = try connection?.mget(["first", "second"]).wait().compactMap { $0.string } + XCTAssertEqual(values?.count, 2) + XCTAssertEqual(values?[0], "1") + XCTAssertEqual(values?[1], "2") + + XCTAssertNoThrow(try connection?.mset(["first": 10]).wait()) + let val = try connection?.get("first").wait() + XCTAssertEqual(val, "10") + } + + func test_msetnx() throws { + let data = [ + "first": 1, + "second": 2 + ] + var success = try connection?.msetnx(data).wait() + XCTAssertEqual(success, true) + + success = try connection?.msetnx(["first": 10, "second": 20]).wait() + XCTAssertEqual(success, false) + + let values = try connection?.mget(["first", "second"]).wait().compactMap { $0.string } + XCTAssertEqual(values?[0], "1") + XCTAssertEqual(values?[1], "2") + } + static var allTests = [ ("test_select", test_select), ("test_set", test_set), @@ -108,5 +197,14 @@ final class BasicCommandsTests: XCTestCase { ("test_delete", test_delete), ("test_ping", test_ping), ("test_echo", test_echo), + ("test_swapdb", test_swapdb), + ("test_increment", test_increment), + ("test_incrementBy", test_incrementBy), + ("test_incrementByFloat", test_incrementByFloat), + ("test_decrement", test_decrement), + ("test_decrementBy", test_decrementBy), + ("test_mget", test_mget), + ("test_mset", test_mset), + ("test_msetnx", test_msetnx), ] }