From 5a35a918aeebb85fb7c069bb2e330fefcb52ca93 Mon Sep 17 00:00:00 2001 From: Nathan Harris Date: Wed, 20 Mar 2019 12:43:08 -0700 Subject: [PATCH] Add extension to `EventLoopFuture` for `RESPValueConvertible` Values for easier mapping --- Sources/NIORedis/Commands/BasicCommands.swift | 37 +++------ Sources/NIORedis/Commands/SetCommands.swift | 79 ++++++------------- .../Extensions/NIO/EventLoopFuture.swift | 29 +++++++ Sources/NIORedis/RedisError.swift | 9 +-- 4 files changed, 71 insertions(+), 83 deletions(-) create mode 100644 Sources/NIORedis/Extensions/NIO/EventLoopFuture.swift diff --git a/Sources/NIORedis/Commands/BasicCommands.swift b/Sources/NIORedis/Commands/BasicCommands.swift index 14b3001..45c6f85 100644 --- a/Sources/NIORedis/Commands/BasicCommands.swift +++ b/Sources/NIORedis/Commands/BasicCommands.swift @@ -25,12 +25,7 @@ extension RedisCommandExecutor { /// - Returns: A future number of keys that were removed. public func delete(_ keys: String...) -> EventLoopFuture { return send(command: "DEL", with: keys) - .flatMapThrowing { res in - guard let count = res.int else { - throw RedisError(identifier: "delete", reason: "Unexpected response: \(res)") - } - return count - } + .mapFromRESP() } /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. @@ -42,12 +37,8 @@ extension RedisCommandExecutor { /// - Returns: A future bool indicating if the expiration was set or not. public func expire(_ key: String, after deadline: Int) -> EventLoopFuture { return send(command: "EXPIRE", with: [key, deadline.description]) - .flatMapThrowing { res in - guard let value = res.int else { - throw RedisError(identifier: "expire", reason: "Unexpected response: \(res)") - } - return value == 1 - } + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } } /// Get the value of a key. @@ -71,38 +62,36 @@ extension RedisCommandExecutor { } /// Echos the provided message through the Redis instance. + /// + /// See [https://redis.io/commands/echo](https://redis.io/commands/echo) /// - Parameter message: The message to echo. /// - Returns: The message sent with the command. public func echo(_ message: String) -> EventLoopFuture { return send(command: "ECHO", with: [message]) - .flatMapThrowing { - guard let response = $0.string else { throw RedisError.respConversion(to: String.self) } - return response - } + .mapFromRESP() } /// Pings the server, which will respond with a message. + /// + /// See [https://redis.io/commands/ping](https://redis.io/commands/ping) /// - Parameter with: The optional message that the server should respond with. /// - Returns: The provided message or Redis' default response of `"PONG"`. public func ping(with message: String? = nil) -> EventLoopFuture { let arg = message != nil ? [message] : [] return send(command: "PING", with: arg) - .flatMapThrowing { - guard let response = $0.string else { throw RedisError.respConversion(to: String.self) } - return response - } + .mapFromRESP() } /// Swaps the data of two Redis database by their index ID. + /// + /// See [https://redis.io/commands/swapdb](https://redis.io/commands/swapdb) /// - Parameters: /// - firstIndex: The index of the first database. /// - secondIndex: The index of the second database. /// - Returns: `true` if the swap was successful. public func swapdb(firstIndex: Int, secondIndex: Int) -> EventLoopFuture { return send(command: "SWAPDB", with: [firstIndex, secondIndex]) - .flatMapThrowing { - guard let response = $0.string else { throw RedisError.respConversion(to: String.self) } - return response == "OK" - } + .mapFromRESP(to: String.self) + .map { return $0 == "OK" } } } diff --git a/Sources/NIORedis/Commands/SetCommands.swift b/Sources/NIORedis/Commands/SetCommands.swift index 019eeac..955af14 100644 --- a/Sources/NIORedis/Commands/SetCommands.swift +++ b/Sources/NIORedis/Commands/SetCommands.swift @@ -19,10 +19,8 @@ extension RedisCommandExecutor { /// - Parameter item: The element to look in the set for, stored as a `bulkString`. public func sismember(_ key: String, item: RESPValueConvertible) -> EventLoopFuture { return send(command: "SISMEMBER", with: [key, item]) - .flatMapThrowing { - guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) } - return result == 1 - } + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } } /// Returns the total count of elements in the set stored at key. @@ -30,10 +28,7 @@ extension RedisCommandExecutor { /// [https://redis.io/commands/scard](https://redis.io/commands/scard) public func scard(_ key: String) -> EventLoopFuture { return send(command: "SCARD", with: [key]) - .flatMapThrowing { - guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) } - return count - } + .mapFromRESP() } /// Adds the provided items to the set stored at key, returning the count of items added. @@ -44,10 +39,7 @@ extension RedisCommandExecutor { assert(items.count > 0, "There must be at least 1 item to add.") return send(command: "SADD", with: [key] + items) - .flatMapThrowing { - guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) } - return result - } + .mapFromRESP() } /// Removes the provided items from the set stored at key, returning the count of items removed. @@ -58,10 +50,7 @@ extension RedisCommandExecutor { assert(items.count > 0, "There must be at least 1 item listed to remove.") return send(command: "SREM", with: [key] + items) - .flatMapThrowing { - guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) } - return result - } + .mapFromRESP() } /// Randomly selects an item from the set stored at key, and removes it. @@ -90,10 +79,7 @@ extension RedisCommandExecutor { /// [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff) public func sdiff(_ keys: String...) -> EventLoopFuture<[RESPValue]> { return send(command: "SDIFF", with: keys) - .flatMapThrowing { - guard let elements = $0.array else { throw RedisError.respConversion(to: Array.self) } - return elements - } + .mapFromRESP() } /// Functionally equivalent to `sdiff`, but instead stores the resulting set at the `destination` key @@ -103,10 +89,7 @@ extension RedisCommandExecutor { /// - Important: If the `destination` key already exists, it is overwritten. public func sdiffstore(destination dest: String, _ keys: String...) -> EventLoopFuture { return send(command: "SDIFFSTORE", with: [dest] + keys) - .flatMapThrowing { - guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) } - return count - } + .mapFromRESP() } /// Returns the members of the set resulting from the intersection of all the given sets. @@ -114,10 +97,7 @@ extension RedisCommandExecutor { /// [https://redis.io/commands/sinter](https://redis.io/commands/sinter) public func sinter(_ keys: String...) -> EventLoopFuture<[RESPValue]> { return send(command: "SINTER", with: keys) - .flatMapThrowing { - guard let elements = $0.array else { throw RedisError.respConversion(to: Array.self) } - return elements - } + .mapFromRESP() } /// Functionally equivalent to `sinter`, but instead stores the resulting set at the `destination` key @@ -127,10 +107,7 @@ extension RedisCommandExecutor { /// - Important: If the `destination` key already exists, it is overwritten. public func sinterstore(destination dest: String, _ keys: String...) -> EventLoopFuture { return send(command: "SINTERSTORE", with: [dest] + keys) - .flatMapThrowing { - guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) } - return count - } + .mapFromRESP() } /// Moves the `item` from the source key to the destination key. @@ -139,10 +116,8 @@ extension RedisCommandExecutor { /// - Important: This will resolve to `true` as long as it was successfully removed from the `source` key. public func smove(item: RESPValueConvertible, fromKey source: String, toKey dest: String) -> EventLoopFuture { return send(command: "SMOVE", with: [source, dest, item]) - .flatMapThrowing { - guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) } - return result == 1 - } + .mapFromRESP() + .map { return $0 == 1 } } /// Returns the members of the set resulting from the union of all the given keys. @@ -150,10 +125,7 @@ extension RedisCommandExecutor { /// [https://redis.io/commands/sunion](https://redis.io/commands/sunion) public func sunion(_ keys: String...) -> EventLoopFuture<[RESPValue]> { return send(command: "SUNION", with: keys) - .flatMapThrowing { - guard let elements = $0.array else { throw RedisError.respConversion(to: Array.self) } - return elements - } + .mapFromRESP() } /// Functionally equivalent to `sunion`, but instead stores the resulting set at the `destination` key @@ -163,10 +135,7 @@ extension RedisCommandExecutor { /// - Important: If the `destination` key already exists, it is overwritten. public func sunionstore(destination dest: String, _ keys: String...) -> EventLoopFuture { return send(command: "SUNIONSTORE", with: [dest] + keys) - .flatMapThrowing { - guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) } - return count - } + .mapFromRESP() } /// Incrementally iterates over a set, returning a cursor position for additional calls with a limited collection @@ -193,16 +162,18 @@ extension RedisCommandExecutor { args.append(c) } - return send(command: "SSCAN", with: args) - .flatMapThrowing { - guard let response = $0.array else { throw RedisError.respConversion(to: Array.self) } - guard - let position = response[0].string, - let newPosition = Int(position) - else { throw RedisError.respConversion(to: Int.self) } - guard let elements = response[1].array else { throw RedisError.respConversion(to: Array.self) } + let response = send(command: "SSCAN", with: args).mapFromRESP(to: [RESPValue].self) + let position = response.flatMapThrowing { result -> Int in + guard + let value = result[0].string, + let position = Int(value) + else { throw RedisError(identifier: #function, reason: "Unexpected value in response: \(result[0])") } + return position + } + let elements = response + .map { return $0[1] } + .mapFromRESP(to: [RESPValue].self) - return (newPosition, elements) - } + return position.and(elements) } } diff --git a/Sources/NIORedis/Extensions/NIO/EventLoopFuture.swift b/Sources/NIORedis/Extensions/NIO/EventLoopFuture.swift new file mode 100644 index 0000000..7e24fc7 --- /dev/null +++ b/Sources/NIORedis/Extensions/NIO/EventLoopFuture.swift @@ -0,0 +1,29 @@ +import NIO + +extension EventLoopFuture where Value == RESPValue { + /// Attempts to convert the `RESPValue` to the desired `RESPValueConvertible` type. + /// If the `RESPValueConvertible.init(_:)` returns `nil`, then the `EventLoopFuture` will fail. + /// - Parameter to: The desired type to convert to. + /// - Returns: An `EventLoopFuture` that resolves a value of the desired type. + @inlinable + public func mapFromRESP( + to type: T.Type = T.self, + file: StaticString = #function, + function: StaticString = #function, + line: UInt = #line + ) -> EventLoopFuture where T: RESPValueConvertible + { + return self.flatMapThrowing { + guard let value = T($0) else { + throw RedisError( + identifier: #function, + reason: "Failed to convert RESP to \(String(describing: type))", + file: file, + function: function, + line: line + ) + } + return value + } + } +} diff --git a/Sources/NIORedis/RedisError.swift b/Sources/NIORedis/RedisError.swift index d315208..1f7b800 100644 --- a/Sources/NIORedis/RedisError.swift +++ b/Sources/NIORedis/RedisError.swift @@ -8,14 +8,13 @@ public struct RedisError: CustomDebugStringConvertible, CustomStringConvertible, public init( identifier: String, reason: String, - file: String = #file, - function: String = #function, - line: UInt = #line, - column: UInt = #column + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line ) { let name = String(describing: type(of: self)) description = "⚠️ [\(name).\(identifier): \(reason)]" - debugDescription = "⚠️ Redis Error: \(reason)\n- id: \(name).\(identifier)\n\n\(Thread.callStackSymbols)" + debugDescription = "⚠️ Redis Error: \(reason)\n- id: \(name).\(identifier)\n\n\(file): L\(line) - \(function)\n\n\(Thread.callStackSymbols)" } }