diff --git a/Sources/NIORedis/Commands/BasicCommands.swift b/Sources/NIORedis/Commands/BasicCommands.swift index 8570dc1..b900d13 100644 --- a/Sources/NIORedis/Commands/BasicCommands.swift +++ b/Sources/NIORedis/Commands/BasicCommands.swift @@ -7,6 +7,7 @@ extension RedisCommandExecutor { /// See [https://redis.io/commands/echo](https://redis.io/commands/echo) /// - Parameter message: The message to echo. /// - Returns: The message sent with the command. + @inlinable public func echo(_ message: String) -> EventLoopFuture { return send(command: "ECHO", with: [message]) .mapFromRESP() @@ -15,8 +16,9 @@ extension RedisCommandExecutor { /// 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. + /// - Parameter message: The optional message that the server should respond with. /// - Returns: The provided message or Redis' default response of `"PONG"`. + @inlinable public func ping(with message: String? = nil) -> EventLoopFuture { let arg = message != nil ? [message] : [] return send(command: "PING", with: arg) @@ -26,196 +28,102 @@ extension RedisCommandExecutor { /// Request for authentication in a password-protected Redis server. /// /// [https://redis.io/commands/auth](https://redis.io/commands/auth) + /// - Parameter password: The password being used to access the Redis server. + /// - Returns: An `EventLoopFuture` that resolves when the connection has been authorized, or fails with a `RedisError`. + @inlinable public func authorize(with password: String) -> EventLoopFuture { return send(command: "AUTH", with: [password]) .map { _ in return () } } /// Select the Redis logical database having the specified zero-based numeric index. - /// New connections always use the database `0`. + /// - Note: New connections always use the database `0`. /// /// [https://redis.io/commands/select](https://redis.io/commands/select) - public func select(database id: Int) -> EventLoopFuture { - return send(command: "SELECT", with: [id.description]) + /// - Parameter index: The 0-based index of the database that will receive later commands. + /// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func select(database index: Int) -> EventLoopFuture { + return send(command: "SELECT", with: [index]) .map { _ in return () } } - /// Swaps the data of two Redis database by their index ID. + /// Swaps the data of two Redis databases by their index IDs. /// /// 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. + /// - first: The index of the first database. + /// - second: 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]) + @inlinable + public func swapDatabase(_ first: Int, with second: Int) -> EventLoopFuture { + /// connection.swapDatabase(index: 0, withIndex: 10) + return send(command: "SWAPDB", with: [first, second]) .mapFromRESP(to: String.self) .map { return $0 == "OK" } } -} -extension RedisCommandExecutor { /// Removes the specified keys. A key is ignored if it does not exist. /// /// [https://redis.io/commands/del](https://redis.io/commands/del) - /// - Returns: A future number of keys that were removed. - public func delete(_ keys: String...) -> EventLoopFuture { + /// - Parameter keys: A list of keys to delete from the database. + /// - Returns: The number of keys deleted from the database. + @inlinable + public func delete(_ keys: [String]) -> EventLoopFuture { + guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture(0) } + return send(command: "DEL", with: keys) .mapFromRESP() } - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. - /// A key with an associated timeout is often said to be volatile in Redis terminology. + /// Sets a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// - Note: A key with an associated timeout is often said to be "volatile" in Redis terminology. /// /// [https://redis.io/commands/expire](https://redis.io/commands/expire) /// - Parameters: - /// - after: The lifetime (in seconds) the key will expirate at. - /// - 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]) + /// - key: The key to set the expiration on. + /// - deadline: The time from now the key will expire at. + /// - Returns: `true` if the expiration was set. + @inlinable + public func expire(_ key: String, after deadline: TimeAmount) -> EventLoopFuture { + let amount = deadline.nanoseconds / 1_000_000_000 + return send(command: "EXPIRE", with: [key, amount]) .mapFromRESP(to: Int.self) .map { return $0 == 1 } } - - /// Get the value of a key. - /// If the key does not exist the value will be `nil`. - /// An error is resolved if the value stored at key is not a string, because GET only handles string values. - /// - /// [https://redis.io/commands/get](https://redis.io/commands/get) - public func get(_ key: String) -> EventLoopFuture { - return send(command: "GET", with: [key]) - .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. - /// - /// [https://redis.io/commands/set](https://redis.io/commands/set) - public func set(_ key: String, to value: String) -> EventLoopFuture { - 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() - } -} +// MARK: Scan extension RedisCommandExecutor { /// Incrementally iterates over all keys in the currently selected database. /// /// [https://redis.io/commands/scan](https://redis.io/commands/scan) /// - Parameters: - /// - startingFrom: The cursor position to start from. + /// - position: The cursor position to start 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 keys stored in the database. + /// - match: 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 keys found in the database. @inlinable public func scan( - startingFrom pos: Int = 0, + startingFrom position: Int = 0, count: Int? = nil, - matching match: String? = nil) -> EventLoopFuture<(Int, [String])> - { - return _scan(command: "SCAN", resultType: [String].self, nil, pos, count, match) + matching match: String? = nil + ) -> EventLoopFuture<(Int, [String])> { + return _scan(command: "SCAN", nil, position, count, match) } - @inline(__always) - @usableFromInline func _scan( + @usableFromInline + func _scan( command: String, - resultType: T.Type, + resultType: T.Type = T.self, _ key: String?, _ pos: Int, _ count: Int?, - _ match: String?) -> EventLoopFuture<(Int, T)> + _ match: String? + ) -> EventLoopFuture<(Int, T)> + where + T: RESPValueConvertible { var args: [RESPValueConvertible] = [pos] @@ -237,7 +145,12 @@ extension RedisCommandExecutor { guard let value = result[0].string, let position = Int(value) - else { throw RedisError(identifier: #function, reason: "Unexpected value in response: \(result[0])") } + else { + throw RedisError( + identifier: #function, + reason: "Unexpected value in response: \(result[0])" + ) + } return position } let elements = response diff --git a/Sources/NIORedis/Commands/HashCommands.swift b/Sources/NIORedis/Commands/HashCommands.swift index f627438..d47d074 100644 --- a/Sources/NIORedis/Commands/HashCommands.swift +++ b/Sources/NIORedis/Commands/HashCommands.swift @@ -1,185 +1,10 @@ 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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(_ key: String, field: String, by amount: T) -> EventLoopFuture - 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) - } - } -} +// MARK: Static Helpers extension RedisCommandExecutor { - @inline(__always) @usableFromInline - static func mapHashResponseToDictionary(_ values: [String]) -> [String: String] { + static func _mapHashResponse(_ values: [String]) -> [String: String] { guard values.count > 0 else { return [:] } var result: [String: String] = [:] @@ -195,3 +20,254 @@ extension RedisCommandExecutor { return result } } + +// MARK: General + +extension RedisCommandExecutor { + /// Removes the specified fields from a hash. + /// + /// See [https://redis.io/commands/hdel](https://redis.io/commands/hdel) + /// - Parameters: + /// - fields: The list of field names that should be removed from the hash. + /// - key: The key of the hash to delete from. + /// - Returns: The number of fields that were deleted. + @inlinable + public func hdel(_ fields: [String], from key: String) -> EventLoopFuture { + guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture(0) } + + return send(command: "HDEL", with: [key] + fields) + .mapFromRESP() + } + + /// Checks if a hash contains the field specified. + /// + /// See [https://redis.io/commands/hexists](https://redis.io/commands/hexists) + /// - Parameters: + /// - field: The field name to look for. + /// - key: The key of the hash to look within. + /// - Returns: `true` if the hash contains the field, `false` if either the key or field do not exist. + @inlinable + public func hexists(_ field: String, in key: String) -> EventLoopFuture { + return send(command: "HEXISTS", with: [key, field]) + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } + } + + /// Gets the number of fields contained in a hash. + /// + /// See [https://redis.io/commands/hlen](https://redis.io/commands/hlen) + /// - Parameter key: The key of the hash to get field count of. + /// - Returns: The number of fields in the hash, or `0` if the key doesn't exist. + @inlinable + public func hlen(of key: String) -> EventLoopFuture { + return send(command: "HLEN", with: [key]) + .mapFromRESP() + } + + /// Gets the string length of a hash field's value. + /// + /// See [https://redis.io/commands/hstrlen](https://redis.io/commands/hstrlen) + /// - Parameters: + /// - field: The field name whose value is being accessed. + /// - key: The key of the hash. + /// - Returns: The string length of the hash field's value, or `0` if the field or hash do not exist. + @inlinable + public func hstrlen(of field: String, in key: String) -> EventLoopFuture { + return send(command: "HSTRLEN", with: [key, field]) + .mapFromRESP() + } + + /// Gets all field names in a hash. + /// + /// See [https://redis.io/commands/hkeys](https://redis.io/commands/hkeys) + /// - Parameter key: The key of the hash. + /// - Returns: A list of field names stored within the hash. + @inlinable + public func hkeys(in key: String) -> EventLoopFuture<[String]> { + return send(command: "HKEYS", with: [key]) + .mapFromRESP() + } + + /// Gets all values stored in a hash. + /// + /// See [https://redis.io/commands/hvals](https://redis.io/commands/hvals) + /// - Parameter key: The key of the hash. + /// - Returns: A list of all values stored in a hash. + @inlinable + public func hvals(in key: String) -> EventLoopFuture<[RESPValue]> { + return send(command: "HVALS", with: [key]) + .mapFromRESP() + } + + /// Incrementally iterates over all fields in a hash. + /// + /// [https://redis.io/commands/scan](https://redis.io/commands/scan) + /// - Parameters: + /// - key: The key of the hash. + /// - position: The position to start the scan from. + /// - count: The number of elements to advance by. Redis default is 10. + /// - match: 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 found fields and their values. + @inlinable + public func hscan( + _ key: String, + startingFrom position: Int = 0, + count: Int? = nil, + matching match: String? = nil + ) -> EventLoopFuture<(Int, [String: String])> { + return _scan(command: "HSCAN", resultType: [String].self, key, position, count, match) + .map { + let values = Self._mapHashResponse($0.1) + return ($0.0, values) + } + } +} + +// MARK: Set + +extension RedisCommandExecutor { + /// Sets a hash field to the value specified. + /// - Note: If you do not want to overwrite existing values, use `hsetnx(_:field:to:)`. + /// + /// See [https://redis.io/commands/hset](https://redis.io/commands/hset) + /// - Parameters: + /// - field: The name of the field in the hash being set. + /// - value: The value the hash field should be set to. + /// - key: The key that holds the hash. + /// - Returns: `true` if the hash was created, `false` if it was updated. + @inlinable + public func hset( + _ field: String, + to value: RESPValueConvertible, + in key: String + ) -> EventLoopFuture { + return send(command: "HSET", with: [key, field, value]) + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } + } + + /// Sets a hash field to the value specified only if the field does not currently exist. + /// - Note: If you do not care about overwriting existing values, use `hset(_:field:to:)`. + /// + /// See [https://redis.io/commands/hsetnx](https://redis.io/commands/hsetnx) + /// - Parameters: + /// - field: The name of the field in the hash being set. + /// - value: The value the hash field should be set to. + /// - key: The key that holds the hash. + /// - Returns: `true` if the hash was created. + @inlinable + public func hsetnx( + _ field: String, + to value: RESPValueConvertible, + in key: String + ) -> EventLoopFuture { + return send(command: "HSETNX", with: [key, field, value]) + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } + } + + /// Sets the fields in a hash to the respective values provided. + /// + /// See [https://redis.io/commands/hmset](https://redis.io/commands/hmset) + /// - Parameters: + /// - fields: The key-value pair of field names and their respective values to set. + /// - key: The key that holds the hash. + /// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. + @inlinable + public func hmset( + _ fields: [String: RESPValueConvertible], + in key: String + ) -> EventLoopFuture { + 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 () } + } +} + +// MARK: Get + +extension RedisCommandExecutor { + /// Gets a hash field's value. + /// + /// See [https://redis.io/commands/hget](https://redis.io/commands/hget) + /// - Parameters: + /// - field: The name of the field whose value is being accessed. + /// - key: The key of the hash being accessed. + /// - Returns: The value of the hash field, or `nil` if either the key or field does not exist. + @inlinable + public func hget(_ field: String, from key: String) -> EventLoopFuture { + return send(command: "HGET", with: [key, field]) + .map { return String($0) } + } + + /// Gets the values of a hash for the fields specified. + /// + /// See [https://redis.io/commands/hmget](https://redis.io/commands/hmget) + /// - Parameters: + /// - fields: A list of field names to get values for. + /// - key: The key of the hash being accessed. + /// - Returns: A list of values in the same order as the `fields` argument. Non-existent fields return `nil` values. + @inlinable + public func hmget(_ fields: [String], from key: String) -> EventLoopFuture<[String?]> { + guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + + return send(command: "HMGET", with: [key] + fields) + .mapFromRESP(to: [RESPValue].self) + .map { return $0.map(String.init) } + } + + /// Returns all the fields and values stored in a hash. + /// + /// See [https://redis.io/commands/hgetall](https://redis.io/commands/hgetall) + /// - Parameter key: The key of the hash to pull from. + /// - 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._mapHashResponse) + } +} + +// MARK: Increment + +extension RedisCommandExecutor { + /// Increments a hash field's value and returns the new value. + /// + /// See [https://redis.io/commands/hincrby](https://redis.io/commands/hincrby) + /// - Parameters: + /// - amount: The amount to increment the value stored in the field by. + /// - field: The name of the field whose value should be incremented. + /// - key: The key of the hash the field is stored in. + /// - Returns: The new value of the hash field. + @inlinable + public func hincrby(_ amount: Int, field: String, in key: String) -> EventLoopFuture { + /// connection.hincrby(20, field: "foo", in: "key") + return send(command: "HINCRBY", with: [key, field, amount]) + .mapFromRESP() + } + + /// Increments a hash field's value and returns the new value. + /// + /// See [https://redis.io/commands/hincrbyfloat](https://redis.io/commands/hincrbyfloat) + /// - Parameters: + /// - amount: The amount to increment the value stored in the field by. + /// - field: The name of the field whose value should be incremented. + /// - key: The key of the hash the field is stored in. + /// - Returns: The new value of the hash field. + @inlinable + public func hincrbyfloat(_ amount: T, field: String, in key: String) -> EventLoopFuture + where + T: BinaryFloatingPoint, + T: RESPValueConvertible + { + return send(command: "HINCRBYFLOAT", with: [key, field, amount]) + .mapFromRESP() + } +} diff --git a/Sources/NIORedis/Commands/ListCommands.swift b/Sources/NIORedis/Commands/ListCommands.swift index cf22ada..1315e83 100644 --- a/Sources/NIORedis/Commands/ListCommands.swift +++ b/Sources/NIORedis/Commands/ListCommands.swift @@ -1,67 +1,103 @@ import NIO +// MARK: General + extension RedisCommandExecutor { - /// Returns the length of the list stored at the key provided. + /// Gets the length of a list. /// /// See [https://redis.io/commands/llen](https://redis.io/commands/llen) + /// - Parameter key: The key of the list. + /// - Returns: The number of elements in the list. @inlinable public func llen(of key: String) -> EventLoopFuture { return send(command: "LLEN", with: [key]) .mapFromRESP() } - /// Returns the element at the specified index stored at the key provided. + /// Gets the element from a list stored at the provided index position. /// - /// See [https://redis.io/commands/llen](https://redis.io/commands/llen) + /// See [https://redis.io/commands/lindex](https://redis.io/commands/lindex) + /// - Parameters: + /// - index: The 0-based index of the element to get. + /// - key: The key of the list. + /// - Returns: The element stored at index, or `.null` if out of bounds. @inlinable - public func lindex(_ key: String, index: Int) -> EventLoopFuture { + public func lindex(_ index: Int, from key: String) -> EventLoopFuture { return send(command: "LINDEX", with: [key, index]) - .flatMapThrowing { response in - guard response.isNull else { return response } - throw RedisError(identifier: #function, reason: "Index out of bounds.") - } } - /// Sets the value at the specified index stored at the key provided. + /// Sets the value of an element in a list at the provided index position. /// /// See [https://redis.io/commands/lset](https://redis.io/commands/lset) + /// - Parameters: + /// - index: The 0-based index of the element to set. + /// - value: The new value the element should be. + /// - key: The key of the list to update. + /// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. @inlinable - public func lset(_ key: String, index: Int, to value: RESPValueConvertible) -> EventLoopFuture { + public func lset( + index: Int, + to value: RESPValueConvertible, + in key: String + ) -> EventLoopFuture { return send(command: "LSET", with: [key, index, value]) .map { _ in () } } - /// Removes elements from the list matching the value provided, up to the count specified. + /// Removes elements from a list matching the value provided. /// /// See [https://redis.io/commands/lrem](https://redis.io/commands/lrem) - /// - Returns: The number of elements removed. + /// - Parameters: + /// - value: The value to delete from the list. + /// - key: The key of the list to remove from. + /// - count: The max number of elements to remove matching the value. See Redis' documentation for more info. + /// - Returns: The number of elements removed from the list. @inlinable - public func lrem(_ value: RESPValueConvertible, from key: String, count: Int) -> EventLoopFuture { + public func lrem( + _ value: RESPValueConvertible, + from key: String, + count: Int = 0 + ) -> EventLoopFuture { return send(command: "LREM", with: [key, count, value]) .mapFromRESP() } - /// Trims the list stored at the key provided to contain elements within the bounds of indexes specified. + /// Trims a list to only contain elements within the specified inclusive bounds of 0-based indices. /// /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// - Parameters: + /// - key: The key of the list to trim. + /// - start: The index of the first element to keep. + /// - stop: The index of the last element to keep. + /// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`. @inlinable - public func ltrim(_ key: String, startIndex start: Int, endIndex end: Int) -> EventLoopFuture { - return send(command: "LTRIM", with: [key, start, end]) + public func ltrim(_ key: String, before start: Int, after stop: Int) -> EventLoopFuture { + return send(command: "LTRIM", with: [key, start, stop]) .map { _ in () } } - /// Returns the elements within the range bounds provided. + /// Gets all elements from a list within the the specified inclusive bounds of 0-based indices. /// - /// See [https://redis.io/commands/ltrim](https://redis.io/commands/ltrim) + /// See [https://redis.io/commands/lrange](https://redis.io/commands/lrange) + /// - Parameters: + /// - range: The range of inclusive indices of elements to get. + /// - key: The key of the list. + /// - Returns: A list of elements found within the range specified. @inlinable - public func lrange(of key: String, startIndex start: Int, endIndex end: Int) -> EventLoopFuture<[RESPValue]> { - return send(command: "LRANGE", with: [key, start, end]) + public func lrange( + within range: (startIndex: Int, endIndex: Int), + from key: String + ) -> EventLoopFuture<[RESPValue]> { + return send(command: "LRANGE", with: [key, range.startIndex, range.endIndex]) .mapFromRESP() } - /// Pops the last element from the source list and pushes it to the destination list. + /// Pops the last element from a source list and pushes it to a destination list. /// /// See [https://redis.io/commands/rpoplpush](https://redis.io/commands/rpoplpush) + /// - Parameters: + /// - source: The key of the list to pop from. + /// - dest: The key of the list to push to. /// - Returns: The element that was moved. @inlinable public func rpoplpush(from source: String, to dest: String) -> EventLoopFuture { @@ -72,36 +108,44 @@ extension RedisCommandExecutor { // MARK: Insert extension RedisCommandExecutor { - /// Inserts the value before the first element matching the pivot value provided. + /// Inserts the element before the first element matching the "pivot" value specified. /// /// See [https://redis.io/commands/linsert](https://redis.io/commands/linsert) + /// - Parameters: + /// - element: The value to insert into the list. + /// - key: The key of the list. + /// - pivot: The value of the element to insert before. /// - Returns: The size of the list after the insert, or -1 if an element matching the pivot value was not found. @inlinable - public func linsert( - _ value: T, - into key: String, - before pivot: T) -> EventLoopFuture + public func linsert(_ element: T, into key: String, before pivot: T) -> EventLoopFuture + where T: RESPValueConvertible { - return _linsert(pivotKeyword: "BEFORE", value, key, pivot) + return _linsert(pivotKeyword: "BEFORE", element, key, pivot) } - /// Inserts the value after the first element matching the pivot value provided. + /// Inserts the element after the first element matching the "pivot" value provided. /// /// See [https://redis.io/commands/linsert](https://redis.io/commands/linsert) + /// - Parameters: + /// - element: The value to insert into the list. + /// - key: The key of the list. + /// - pivot: The value of the element to insert after. /// - Returns: The size of the list after the insert, or -1 if an element matching the pivot value was not found. @inlinable - public func linsert( - _ value: T, - into key: String, - after pivot: T) -> EventLoopFuture + public func linsert(_ element: T, into key: String, after pivot: T) -> EventLoopFuture + where T: RESPValueConvertible { - return _linsert(pivotKeyword: "AFTER", value, key, pivot) + return _linsert(pivotKeyword: "AFTER", element, key, pivot) } - @inline(__always) @usableFromInline - func _linsert(pivotKeyword: StaticString, _ value: RESPValueConvertible, _ key: String, _ pivot: RESPValueConvertible) -> EventLoopFuture { - return send(command: "LINSERT", with: [key, pivotKeyword.description, pivot, value]) + func _linsert( + pivotKeyword: String, + _ element: RESPValueConvertible, + _ key: String, + _ pivot: RESPValueConvertible + ) -> EventLoopFuture { + return send(command: "LINSERT", with: [key, pivotKeyword, pivot, element]) .mapFromRESP() } } @@ -109,34 +153,43 @@ extension RedisCommandExecutor { // MARK: Head Operations extension RedisCommandExecutor { - /// Removes the first element in the list and returns it. + /// Removes the first element of a list. /// /// See [https://redis.io/commands/lpop](https://redis.io/commands/lpop) + /// - Parameter key: The key of the list to pop from. + /// - Returns: The element that was popped from the list, or `.null`. @inlinable - public func lpop(from key: String) -> EventLoopFuture { + public func lpop(from key: String) -> EventLoopFuture { return send(command: "LPOP", with: [key]) - .mapFromRESP() } - /// Inserts all values provided into the list stored at the key specified. - /// - Note: This inserts the values at the head of the list, for the tail see `rpush(_:to:)`. + /// Pushes all of the provided elements into a list. + /// - Note: This inserts the elements at the head of the list; for the tail see `rpush(_:into:)`. /// /// See [https://redis.io/commands/lpush](https://redis.io/commands/lpush) + /// - Parameters: + /// - elements: The values to push into the list. + /// - key: The key of the list. /// - Returns: The length of the list after adding the new elements. @inlinable - public func lpush(_ values: [RESPValueConvertible], to key: String) -> EventLoopFuture { - return send(command: "LPUSH", with: [key] + values) + public func lpush(_ elements: [RESPValueConvertible], into key: String) -> EventLoopFuture { + assert(elements.count > 0, "At least 1 element should be provided.") + + return send(command: "LPUSH", with: [key] + elements) .mapFromRESP() } - /// Inserts the value at the head of the list only if the key exists and holds a list. - /// - Note: This inserts the values at the head of the list, for the tail see `rpushx(_:to:)`. + /// Pushes an element into a list, but only if the key exists and holds a list. + /// - Note: This inserts the element at the head of the list, for the tail see `rpushx(_:into:)`. /// /// See [https://redis.io/commands/lpushx](https://redis.io/commands/lpushx) + /// - Parameters: + /// - element: The value to try and push into the list. + /// - key: The key of the list. /// - Returns: The length of the list after adding the new elements. @inlinable - public func lpushx(_ value: RESPValueConvertible, to key: String) -> EventLoopFuture { - return send(command: "LPUSHX", with: [key, value]) + public func lpushx(_ element: RESPValueConvertible, into key: String) -> EventLoopFuture { + return send(command: "LPUSHX", with: [key, element]) .mapFromRESP() } } @@ -144,34 +197,42 @@ extension RedisCommandExecutor { // MARK: Tail Operations extension RedisCommandExecutor { - /// Removes the last element in the list and returns it. + /// Removes the last element a list. /// /// See [https://redis.io/commands/rpop](https://redis.io/commands/rpop) + /// - Parameter key: The key of the list to pop from. + /// - Returns: The element that was popped from the list, else `.null`. @inlinable - public func rpop(from key: String) -> EventLoopFuture { + public func rpop(from key: String) -> EventLoopFuture { return send(command: "RPOP", with: [key]) - .mapFromRESP() } - /// Inserts all values provided into the list stored at the key specified. - /// - Note: This inserts the values at the tail of the list, for the head see `lpush(_:to:)`. + /// Pushes all of the provided elements into a list. + /// - Note: This inserts the elements at the tail of the list; for the head see `lpush(_:into:)`. /// /// See [https://redis.io/commands/rpush](https://redis.io/commands/rpush) - /// - Returns: The size of the list after adding the new elements. + /// - elements: The values to push into the list. + /// - key: The key of the list. + /// - Returns: The length of the list after adding the new elements. @inlinable - public func rpush(_ values: [RESPValueConvertible], to key: String) -> EventLoopFuture { - return send(command: "RPUSH", with: [key] + values) + public func rpush(_ elements: [RESPValueConvertible], into key: String) -> EventLoopFuture { + assert(elements.count > 0, "At least 1 element should be provided.") + + return send(command: "RPUSH", with: [key] + elements) .mapFromRESP() } - /// Inserts the value at the head of the list only if the key exists and holds a list. - /// - Note: This inserts the values at the tail of the list, for the head see `lpushx(_:to:)`. + /// Pushes an element into a list, but only if the key exists and holds a list. + /// - Note: This inserts the element at the tail of the list; for the head see `lpushx(_:into:)`. /// /// See [https://redis.io/commands/rpushx](https://redis.io/commands/rpushx) + /// - Parameters: + /// - element: The value to try and push into the list. + /// - key: The key of the list. /// - Returns: The length of the list after adding the new elements. @inlinable - public func rpushx(_ value: RESPValueConvertible, to key: String) -> EventLoopFuture { - return send(command: "RPUSHX", with: [key, value]) + public func rpushx(_ element: RESPValueConvertible, into key: String) -> EventLoopFuture { + return send(command: "RPUSHX", with: [key, element]) .mapFromRESP() } } diff --git a/Sources/NIORedis/Commands/SetCommands.swift b/Sources/NIORedis/Commands/SetCommands.swift index e5239ee..3f9c6a0 100644 --- a/Sources/NIORedis/Commands/SetCommands.swift +++ b/Sources/NIORedis/Commands/SetCommands.swift @@ -1,156 +1,250 @@ import Foundation import NIO +// MARK: General + extension RedisCommandExecutor { - /// Returns the all of the elements of the set stored at key. - /// - /// Ordering of results are stable between multiple calls of this method to the same set. + /// Gets all of the elements contained in a set. + /// - Note: Ordering of results are stable between multiple calls of this method to the same set. /// /// Results are **UNSTABLE** in regards to the ordering of insertions through the `sadd` command and this method. /// - /// [https://redis.io/commands/smembers](https://redis.io/commands/smembers) - public func smembers(_ key: String) -> EventLoopFuture { + /// See [https://redis.io/commands/smembers](https://redis.io/commands/smembers) + /// - Parameter key: The key of the set. + /// - Returns: A list of elements found within the set. + @inlinable + public func smembers(of key: String) -> EventLoopFuture<[RESPValue]> { return send(command: "SMEMBERS", with: [key]) + .mapFromRESP() } - /// Checks if the provided item is included in the set stored at key. + /// Checks if the element is included in a set. /// - /// https://redis.io/commands/sismember - /// - 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]) + /// See [https://redis.io/commands/sismember](https://redis.io/commands/sismember) + /// - Parameters: + /// - element: The element to look for in the set. + /// - key: The key of the set to look in. + /// - Returns: `true` if the element is in the set. + @inlinable + public func sismember(_ element: RESPValueConvertible, of key: String) -> EventLoopFuture { + return send(command: "SISMEMBER", with: [key, element]) .mapFromRESP(to: Int.self) .map { return $0 == 1 } } - /// Returns the total count of elements in the set stored at key. + /// Gets the total count of elements within a set. /// - /// [https://redis.io/commands/scard](https://redis.io/commands/scard) - public func scard(_ key: String) -> EventLoopFuture { + /// See [https://redis.io/commands/scard](https://redis.io/commands/scard) + /// - Parameter key: The key of the set. + /// - Returns: The total count of elements in the set. + @inlinable + public func scard(of key: String) -> EventLoopFuture { return send(command: "SCARD", with: [key]) .mapFromRESP() } - /// Adds the provided items to the set stored at key, returning the count of items added. + /// Adds elements to a set. /// - /// [https://redis.io/commands/sadd](https://redis.io/commands/sadd) - /// - Parameter items: The elements to add to the set, stored as `bulkString`s. - public func sadd(_ key: String, items: [RESPValueConvertible]) -> EventLoopFuture { - assert(items.count > 0, "There must be at least 1 item to add.") + /// See [https://redis.io/commands/sadd](https://redis.io/commands/sadd) + /// - Parameters: + /// - elements: The values to add to the set. + /// - key: The key of the set to insert into. + /// - Returns: The number of elements that were added to the set. + @inlinable + public func sadd(_ elements: [RESPValueConvertible], to key: String) -> EventLoopFuture { + guard elements.count > 0 else { return self.eventLoop.makeSucceededFuture(0) } - return send(command: "SADD", with: [key] + items) + return send(command: "SADD", with: [key] + elements) .mapFromRESP() } - /// Removes the provided items from the set stored at key, returning the count of items removed. + /// Removes elements from a set. /// - /// [https://redis.io/commands/srem](https://redis.io/commands/srem) - /// - Parameter items: The elemnts to remove from the set, stored as `bulkString`s. - public func srem(_ key: String, items: [RESPValueConvertible]) -> EventLoopFuture { - assert(items.count > 0, "There must be at least 1 item listed to remove.") + /// See [https://redis.io/commands/srem](https://redis.io/commands/srem) + /// - Parameters: + /// - elements: The values to remove from the set. + /// - key: The key of the set to remove from. + /// - Returns: The number of elements that were removed from the set. + @inlinable + public func srem(_ elements: [RESPValueConvertible], from key: String) -> EventLoopFuture { + guard elements.count > 0 else { return self.eventLoop.makeSucceededFuture(0) } - return send(command: "SREM", with: [key] + items) + return send(command: "SREM", with: [key] + elements) .mapFromRESP() } - /// Randomly selects an item from the set stored at key, and removes it. + /// Randomly selects and removes one or more elements in a set. /// - /// [https://redis.io/commands/spop](https://redis.io/commands/spop) - public func spop(_ key: String) -> EventLoopFuture { - return send(command: "SPOP", with: [key]) + /// See [https://redis.io/commands/spop](https://redis.io/commands/spop) + /// - Parameters: + /// - key: The key of the set. + /// - count: The max number of elements to pop from the set. + /// - Returns: The element that was popped from the set. + @inlinable + public func spop(from key: String, max count: Int = 1) -> EventLoopFuture<[RESPValue]> { + assert(count >= 0, "A negative max count is nonsense.") + + guard count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + + return send(command: "SPOP", with: [key, count]) + .mapFromRESP() } - /// Randomly selects elements from the set stored at string, up to the `count` provided. - /// Use the `RESPValue.array` property to access the underlying values. + /// Randomly selects one or more elements in a set. /// /// connection.srandmember("my_key") // pulls just one random element /// connection.srandmember("my_key", max: -3) // pulls up to 3 elements, allowing duplicates /// connection.srandmember("my_key", max: 3) // pulls up to 3 elements, guaranteed unique /// - /// [https://redis.io/commands/srandmember](https://redis.io/commands/srandmember) - public func srandmember(_ key: String, max count: Int = 1) -> EventLoopFuture { - assert(count != 0, "A count of zero is a noop for selecting a random element.") + /// See [https://redis.io/commands/srandmember](https://redis.io/commands/srandmember) + /// - Parameters: + /// - key: The key of the set. + /// - count: The max number of elements to select from the set. + /// - Returns: The elements randomly selected from the set. + @inlinable + public func srandmember(from key: String, max count: Int = 1) -> EventLoopFuture<[RESPValue]> { + guard count != 0 else { return self.eventLoop.makeSucceededFuture([]) } - return send(command: "SRANDMEMBER", with: [key, count.description]) - } - - /// Returns the members of the set resulting from the difference between the first set and all the successive sets. - /// - /// [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff) - public func sdiff(_ keys: String...) -> EventLoopFuture<[RESPValue]> { - return send(command: "SDIFF", with: keys) + return send(command: "SRANDMEMBER", with: [key, count]) .mapFromRESP() } - /// Functionally equivalent to `sdiff`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. + /// Moves an element from one set to another. /// - /// [https://redis.io/commands/sdiffstore](https://redis.io/commands/sdiffstore) - /// - 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) - .mapFromRESP() - } + /// See [https://redis.io/commands/smove](https://redis.io/commands/smove) + /// - Parameters: + /// - element: The value to move from the source. + /// - sourceKey: The key of the source set. + /// - destKey: The key of the destination set. + /// - Returns: `true` if the element was successfully removed from the source set. + @inlinable + public func smove( + _ element: RESPValueConvertible, + from sourceKey: String, + to destKey: String + ) -> EventLoopFuture { + guard sourceKey != destKey else { return self.eventLoop.makeSucceededFuture(true) } - /// Returns the members of the set resulting from the intersection of all the given sets. - /// - /// [https://redis.io/commands/sinter](https://redis.io/commands/sinter) - public func sinter(_ keys: String...) -> EventLoopFuture<[RESPValue]> { - return send(command: "SINTER", with: keys) - .mapFromRESP() - } - - /// Functionally equivalent to `sinter`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. - /// - /// [https://redis.io/commands/sinterstore](https://redis.io/commands/sinterstore) - /// - 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) - .mapFromRESP() - } - - /// Moves the `item` from the source key to the destination key. - /// - /// [https://redis.io/commands/smove](https://redis.io/commands/smove) - /// - 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]) + return send(command: "SMOVE", with: [sourceKey, destKey, element]) .mapFromRESP() .map { return $0 == 1 } } - /// Returns the members of the set resulting from the union of all the given keys. + /// Incrementally iterates over all values in a set. /// - /// [https://redis.io/commands/sunion](https://redis.io/commands/sunion) - public func sunion(_ keys: String...) -> EventLoopFuture<[RESPValue]> { + /// See [https://redis.io/commands/sscan](https://redis.io/commands/sscan) + /// - Parameters: + /// - key: The key of the set. + /// - position: The position to start the scan from. + /// - count: The number of elements to advance by. Redis default is 10. + /// - match: 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 elements found in the set. + @inlinable + public func sscan( + _ key: String, + startingFrom position: Int = 0, + count: Int? = nil, + matching match: String? = nil + ) -> EventLoopFuture<(Int, [RESPValue])> { + return _scan(command: "SSCAN", key, position, count, match) + } +} + +// MARK: Diff + +extension RedisCommandExecutor { + /// Calculates the difference between two or more sets. + /// + /// See [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff) + /// - Parameter keys: The source sets to calculate the difference of. + /// - Returns: A list of elements resulting from the difference. + @inlinable + public func sdiff(of keys: [String]) -> EventLoopFuture<[RESPValue]> { + guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + + return send(command: "SDIFF", with: keys) + .mapFromRESP() + } + + /// Calculates the difference between two or more sets and stores the result. + /// - Important: If the destination key already exists, it is overwritten. + /// + /// See [https://redis.io/commands/sdiffstore](https://redis.io/commands/sdiffstore) + /// - Parameters: + /// - destination: The key of the new set from the result. + /// - sources: The list of source sets to calculate the difference of. + /// - Returns: The number of elements in the difference result. + @inlinable + public func sdiffstore(as destination: String, sources keys: [String]) -> EventLoopFuture { + assert(keys.count > 0, "At least 1 key should be provided.") + + return send(command: "SDIFFSTORE", with: [destination] + keys) + .mapFromRESP() + } +} + +// MARK: Intersect + +extension RedisCommandExecutor { + /// Calculates the intersection of two or more sets. + /// + /// See [https://redis.io/commands/sinter](https://redis.io/commands/sinter) + /// - Parameter keys: The source sets to calculate the intersection of. + /// - Returns: A list of elements resulting from the intersection. + @inlinable + public func sinter(of keys: [String]) -> EventLoopFuture<[RESPValue]> { + guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + + return send(command: "SINTER", with: keys) + .mapFromRESP() + } + + /// Calculates the intersetion of two or more sets and stores the result. + /// - Important: If the destination key already exists, it is overwritten. + /// + /// See [https://redis.io/commands/sinterstore](https://redis.io/commands/sinterstore) + /// - Parameters: + /// - destination: The key of the new set from the result. + /// - sources: A list of source sets to calculate the intersection of. + /// - Returns: The number of elements in the intersection result. + @inlinable + public func sinterstore(as destination: String, sources keys: [String]) -> EventLoopFuture { + assert(keys.count > 0, "At least 1 key should be provided.") + + return send(command: "SINTERSTORE", with: [destination] + keys) + .mapFromRESP() + } +} + +// MARK: Union + +extension RedisCommandExecutor { + /// Calculates the union of two or more sets. + /// + /// See [https://redis.io/commands/sunion](https://redis.io/commands/sunion) + /// - Parameter keys: The source sets to calculate the union of. + /// - Returns: A list of elements resulting from the union. + @inlinable + public func sunion(of keys: [String]) -> EventLoopFuture<[RESPValue]> { + guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + return send(command: "SUNION", with: keys) .mapFromRESP() } - /// Functionally equivalent to `sunion`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. + /// Calculates the union of two or more sets and stores the result. + /// - Important: If the destination key already exists, it is overwritten. /// - /// [https://redis.io/commands/sunionstore](https://redis.io/commands/sunionstore) - /// - 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) + /// See [https://redis.io/commands/sunionstore](https://redis.io/commands/sunionstore) + /// - Parameters: + /// - destination: The key of the new set from the result. + /// - sources: A list of source sets to calculate the union of. + /// - Returns: The number of elements in the union result. + @inlinable + public func sunionstore(as destination: String, sources keys: [String]) -> EventLoopFuture { + assert(keys.count > 0, "At least 1 key should be provided.") + + return send(command: "SUNIONSTORE", with: [destination] + keys) .mapFromRESP() } - - /// Incrementally iterates over all values in a set. - /// - /// [https://redis.io/commands/sscan](https://redis.io/commands/sscan) - /// - Parameters: - /// - 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. - public func sscan( - _ key: String, - atPosition pos: Int = 0, - count: Int? = nil, - matching match: String? = nil) -> EventLoopFuture<(Int, [RESPValue])> - { - return _scan(command: "SSCAN", resultType: [RESPValue].self, key, pos, count, match) - } } diff --git a/Sources/NIORedis/Commands/SortedSetCommands.swift b/Sources/NIORedis/Commands/SortedSetCommands.swift index c0f279b..807b3f7 100644 --- a/Sources/NIORedis/Commands/SortedSetCommands.swift +++ b/Sources/NIORedis/Commands/SortedSetCommands.swift @@ -4,7 +4,10 @@ import NIO extension RedisCommandExecutor { @usableFromInline - static func _mapSortedSetResponse(_ response: [RESPValue], scoreIsFirst: Bool) throws -> [(RESPValue, Double)] { + static func _mapSortedSetResponse( + _ response: [RESPValue], + scoreIsFirst: Bool + ) throws -> [(RESPValue, Double)] { guard response.count > 0 else { return [] } var result: [(RESPValue, Double)] = [] @@ -17,8 +20,8 @@ extension RedisCommandExecutor { throw RedisError(identifier: #function, reason: "Unexpected response \"\(scoreItem)\"") } - let memberIndex = scoreIsFirst ? index + 1 : index - result.append((response[memberIndex], score)) + let elementIndex = scoreIsFirst ? index + 1 : index + result.append((response[elementIndex], score)) index += 2 } while (index < response.count) @@ -34,18 +37,21 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd) /// - Parameters: - /// - items: A list of elements and their score to add to the sorted set. - /// - to: The key of the sorted set. + /// - elements: A list of elements and their score to add to the sorted set. + /// - key: The key of the sorted set. /// - options: A set of options defined by Redis for this command to execute under. /// - Returns: The number of elements added to the sorted set. @inlinable public func zadd( - _ items: [(element: RESPValueConvertible, score: Double)], + _ elements: [(element: RESPValueConvertible, score: Double)], to key: String, - options: Set = []) -> EventLoopFuture - { + options: Set = [] + ) -> EventLoopFuture { guard !options.contains("INCR") else { - return eventLoop.makeFailedFuture(RedisError(identifier: #function, reason: "INCR option is unsupported. Use zincrby(_:member:by:) instead.")) + return self.eventLoop.makeFailedFuture(RedisError( + identifier: #function, + reason: "INCR option is unsupported. Use zincrby(_:element:in:) instead." + )) } assert(options.count <= 2, "Invalid number of options provided.") @@ -57,13 +63,8 @@ extension RedisCommandExecutor { var args: [RESPValueConvertible] = [key] + options.map { $0 } - for (element, score) in items { - switch score { - case .infinity: args.append("+inf") - case -.infinity: args.append("-inf") - default: args.append(score) - } - + for (element, score) in elements { + args.append(score) args.append(element) } @@ -75,24 +76,24 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zadd](https://redis.io/commands/zadd) /// - Parameters: - /// - item: The element and its score to add to the sorted set. - /// - to: The key of the sorted set. + /// - element: The element and its score to add to the sorted set. + /// - key: The key of the sorted set. /// - options: A set of options defined by Redis for this command to execute under. /// - Returns: `true` if the element was added or score was updated in the sorted set. @inlinable public func zadd( - _ item: (element: RESPValueConvertible, score: Double), + _ element: (element: RESPValueConvertible, score: Double), to key: String, - options: Set = []) -> EventLoopFuture - { - return zadd([item], to: key, options: options) + options: Set = [] + ) -> EventLoopFuture { + return zadd([element], to: key, options: options) .map { return $0 == 1 } } - /// Returns the number of elements in a sorted set. + /// Gets the number of elements in a sorted set. /// /// See [https://redis.io/commands/zcard](https://redis.io/commands/zcard) - /// - Parameter of: The key of the sorted set. + /// - Parameter key: The key of the sorted set. /// - Returns: The number of elements in the sorted set. @inlinable public func zcard(of key: String) -> EventLoopFuture { @@ -100,35 +101,35 @@ extension RedisCommandExecutor { .mapFromRESP() } - /// Returns the score of the specified member in a stored set. + /// Gets the score of the specified element in a stored set. /// /// See [https://redis.io/commands/zscore](https://redis.io/commands/zscore) /// - Parameters: - /// - of: The element in the sorted set to get the score for. - /// - storedAt: The key of the sorted set. - /// - Returns: The score of the element provided. + /// - element: The element in the sorted set to get the score for. + /// - key: The key of the sorted set. + /// - Returns: The score of the element provided, or `nil` if the element is not found in the set or the set does not exist. @inlinable - public func zscore(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { - return send(command: "ZSCORE", with: [key, member]) + public func zscore(of element: RESPValueConvertible, in key: String) -> EventLoopFuture { + return send(command: "ZSCORE", with: [key, element]) .map { return Double($0) } } - /// Incrementally iterates over all fields in a sorted set. + /// Incrementally iterates over all elements in a sorted set. /// /// See [https://redis.io/commands/zscan](https://redis.io/commands/zscan) /// - Parameters: /// - key: The key identifying the sorted set. - /// - startingFrom: The position to start the scan from. + /// - position: 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 and their scores. + /// - match: 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 elements found in the sorted set with their scores. @inlinable public func zscan( _ key: String, startingFrom position: Int = 0, count: Int? = nil, - matching match: String? = nil) -> EventLoopFuture<(Int, [(RESPValue, Double)])> - { + matching match: String? = nil + ) -> EventLoopFuture<(Int, [(RESPValue, Double)])> { return _scan(command: "ZSCAN", resultType: [RESPValue].self, key, position, count, match) .flatMapThrowing { let values = try Self._mapSortedSetResponse($0.1, scoreIsFirst: false) @@ -142,31 +143,31 @@ extension RedisCommandExecutor { extension RedisCommandExecutor { /// Returns the rank (index) of the specified element in a sorted set. /// - Note: This treats the ordered set as ordered from low to high. - /// For the inverse, see `zrevrank(of:storedAt:)`. + /// For the inverse, see `zrevrank(of:in:)`. /// /// See [https://redis.io/commands/zrank](https://redis.io/commands/zrank) /// - Parameters: - /// - of: The element in the sorted set to search for. - /// - storedAt: The key of the sorted set to search. + /// - element: The element in the sorted set to search for. + /// - key: The key of the sorted set to search. /// - Returns: The index of the element, or `nil` if the key was not found. @inlinable - public func zrank(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { - return send(command: "ZRANK", with: [key, member]) + public func zrank(of element: RESPValueConvertible, in key: String) -> EventLoopFuture { + return send(command: "ZRANK", with: [key, element]) .mapFromRESP() } /// Returns the rank (index) of the specified element in a sorted set. /// - Note: This treats the ordered set as ordered from high to low. - /// For the inverse, see `zrank(of:storedAt:)`. + /// For the inverse, see `zrank(of:in:)`. /// /// See [https://redis.io/commands/zrevrank](https://redis.io/commands/zrevrank) /// - Parameters: - /// - of: The element in the sorted set to search for. - /// - storedAt: The key of the sorted set to search. + /// - element: The element in the sorted set to search for. + /// - key: The key of the sorted set to search. /// - Returns: The index of the element, or `nil` if the key was not found. @inlinable - public func zrevrank(of member: RESPValueConvertible, storedAt key: String) -> EventLoopFuture { - return send(command: "ZREVRANK", with: [key, member]) + public func zrevrank(of element: RESPValueConvertible, in key: String) -> EventLoopFuture { + return send(command: "ZREVRANK", with: [key, element]) .mapFromRESP() } } @@ -178,11 +179,14 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zcount](https://redis.io/commands/zcount) /// - Parameters: - /// - of: The key of the sorted set to count. - /// - within: The min and max range of scores to filter for. + /// - key: The key of the sorted set to count. + /// - range: The min and max range of scores to filter for. /// - Returns: The number of elements in the sorted set that fit within the score range. @inlinable - public func zcount(of key: String, within range: (min: String, max: String)) -> EventLoopFuture { + public func zcount( + of key: String, + within range: (min: String, max: String) + ) -> EventLoopFuture { return send(command: "ZCOUNT", with: [key, range.min, range.max]) .mapFromRESP() } @@ -192,11 +196,14 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zlexcount](https://redis.io/commands/zlexcount) /// - Parameters: - /// - of: The key of the sorted set to count. - /// - within: The min and max range of values to filter for. + /// - key: The key of the sorted set to count. + /// - range: The min and max range of values to filter for. /// - Returns: The number of elements in the sorted set that fit within the value range. @inlinable - public func zlexcount(of key: String, within range: (min: String, max: String)) -> EventLoopFuture { + public func zlexcount( + of key: String, + within range: (min: String, max: String) + ) -> EventLoopFuture { return send(command: "ZLEXCOUNT", with: [key, range.min, range.max]) .mapFromRESP() } @@ -205,23 +212,22 @@ extension RedisCommandExecutor { // MARK: Pop extension RedisCommandExecutor { - /// Removes members from a sorted set with the lowest scores. + /// Removes elements from a sorted set with the lowest scores. /// /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin) /// - Parameters: + /// - key: The key identifying the sorted set in Redis. /// - count: The max number of elements to pop from the set. - /// - from: The key identifying the sorted set in Redis. - /// - Returns: A list of members popped from the sorted set with their associated score. + /// - Returns: A list of elements popped from the sorted set with their associated score. @inlinable - public func zpopmin(_ count: Int, from key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + public func zpopmin(from key: String, max count: Int) -> EventLoopFuture<[(RESPValue, Double)]> { return _zpop(command: "ZPOPMIN", count, key) } - /// Removes a member from a sorted set with the lowest score. + /// Removes the element from a sorted set with the lowest score. /// /// See [https://redis.io/commands/zpopmin](https://redis.io/commands/zpopmin) - /// - Parameters: - /// - from: The key identifying the sorted set in Redis. + /// - Parameter key: The key identifying the sorted set in Redis. /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty. @inlinable public func zpopmin(from key: String) -> EventLoopFuture<(RESPValue, Double)?> { @@ -229,23 +235,22 @@ extension RedisCommandExecutor { .map { return $0.count > 0 ? $0[0] : nil } } - /// Removes members from a sorted set with the highest scores. + /// Removes elements from a sorted set with the highest scores. /// /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax) /// - Parameters: + /// - key: The key identifying the sorted set in Redis. /// - count: The max number of elements to pop from the set. - /// - from: The key identifying the sorted set in Redis. - /// - Returns: A list of members popped from the sorted set with their associated score. + /// - Returns: A list of elements popped from the sorted set with their associated score. @inlinable - public func zpopmax(_ count: Int, from key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + public func zpopmax(from key: String, max count: Int) -> EventLoopFuture<[(RESPValue, Double)]> { return _zpop(command: "ZPOPMAX", count, key) } - /// Removes a member from a sorted set with the highest score. + /// Removes the element from a sorted set with the highest score. /// /// See [https://redis.io/commands/zpopmax](https://redis.io/commands/zpopmax) - /// - Parameters: - /// - from: The key identifying the sorted set in Redis. + /// - Parameter key: The key identifying the sorted set in Redis. /// - Returns: The element and its associated score that was popped from the sorted set, or `nil` if set was empty. @inlinable public func zpopmax(from key: String) -> EventLoopFuture<(RESPValue, Double)?> { @@ -254,10 +259,18 @@ extension RedisCommandExecutor { } @usableFromInline - func _zpop(command: String, _ count: Int?, _ key: String) -> EventLoopFuture<[(RESPValue, Double)]> { + func _zpop( + command: String, + _ count: Int?, + _ key: String + ) -> EventLoopFuture<[(RESPValue, Double)]> { var args: [RESPValueConvertible] = [key] - if let c = count { args.append(c) } + if let c = count { + guard c != 0 else { return self.eventLoop.makeSucceededFuture([]) } + + args.append(c) + } return send(command: command, with: args) .mapFromRESP(to: [RESPValue].self) @@ -268,31 +281,21 @@ extension RedisCommandExecutor { // MARK: Increment extension RedisCommandExecutor { - /// Increments the score of the specified member in a sorted set. + /// Increments the score of the specified element in a sorted set. /// /// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby) /// - Parameters: + /// - amount: The amount to increment this element's score by. + /// - element: The element to increment. /// - key: The key of the sorted set. - /// - member: The element to increment. - /// - by: The amount to increment this element's score by. - /// - Returns: The new score of the member. + /// - Returns: The new score of the element. @inlinable - public func zincrby(_ key: String, member: RESPValueConvertible, by amount: Int) -> EventLoopFuture { - return send(command: "ZINCRBY", with: [key, amount, member]) - .mapFromRESP() - } - - /// Increments the score of the specified member in a sorted set. - /// - /// See [https://redis.io/commands/zincrby](https://redis.io/commands/zincrby) - /// - Parameters: - /// - key: The key of the sorted set. - /// - member: The element to increment. - /// - by: The amount to increment this element's score by. - /// - Returns: The new score of the member. - @inlinable - public func zincrby(_ key: String, member: RESPValueConvertible, by amount: Double) -> EventLoopFuture { - return send(command: "ZINCRBY", with: [key, amount, member]) + public func zincrby( + _ amount: Double, + element: RESPValueConvertible, + in key: String + ) -> EventLoopFuture { + return send(command: "ZINCRBY", with: [key, amount, element]) .mapFromRESP() } } @@ -300,43 +303,43 @@ extension RedisCommandExecutor { // MARK: Intersect and Union extension RedisCommandExecutor { - /// Computes a new sorted set as a union between all provided source sorted sets and stores the result at the key desired. + /// Calculates the union of two or more sorted sets and stores the result. /// - Note: This operation overwrites any value stored at the destination key. /// /// See [https://redis.io/commands/zunionstore](https://redis.io/commands/zunionstore) /// - Parameters: + /// - destination: The key of the new sorted set from the result. /// - sources: The list of sorted set keys to treat as the source of the union. - /// - to: The key to store the union sorted set at. /// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters. /// - aggregateMethod: The method of aggregating the values of the union. Supported values are "SUM", "MIN", and "MAX". - /// - Returns: The number of members in the new sorted set. + /// - Returns: The number of elements in the new sorted set. @inlinable public func zunionstore( - _ sources: [String], - to destination: String, + as destination: String, + sources: [String], weights: [Int]? = nil, - aggregateMethod aggregate: String? = nil) -> EventLoopFuture - { + aggregateMethod aggregate: String? = nil + ) -> EventLoopFuture { return _zopstore(command: "ZUNIONSTORE", sources, destination, weights, aggregate) } - /// Computes a new sorted set as an intersection between all provided source sorted sets and stores the result at the key desired. + /// Calculates the intersection of two or more sorted sets and stores the result. /// - Note: This operation overwrites any value stored at the destination key. /// /// See [https://redis.io/commands/zinterstore](https://redis.io/commands/zinterstore) /// - Parameters: + /// - destination: The key of the new sorted set from the result. /// - sources: The list of sorted set keys to treat as the source of the intersection. - /// - to: The key to store the intersected sorted set at. /// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters. /// - aggregateMethod: The method of aggregating the values of the intersection. Supported values are "SUM", "MIN", and "MAX". - /// - Returns: The number of members in the new sorted set. + /// - Returns: The number of elements in the new sorted set. @inlinable public func zinterstore( - _ sources: [String], - to destination: String, + as destination: String, + sources: [String], weights: [Int]? = nil, - aggregateMethod aggregate: String? = nil) -> EventLoopFuture - { + aggregateMethod aggregate: String? = nil + ) -> EventLoopFuture { return _zopstore(command: "ZINTERSTORE", sources, destination, weights, aggregate) } @@ -375,46 +378,54 @@ extension RedisCommandExecutor { // MARK: Range extension RedisCommandExecutor { - /// Returns the specified range of elements in a sorted set. + /// Gets the specified range of elements in a sorted set. /// - Note: This treats the ordered set as ordered from low to high. - /// For the inverse, see `zrevrange(of:startIndex:endIndex:withScores:)`. + /// + /// For the inverse, see `zrevrange(within:from:withScores:)`. /// /// See [https://redis.io/commands/zrange](https://redis.io/commands/zrange) /// - Parameters: - /// - withinIndices: The start and stop 0-based indices of the range of elements to include. - /// - from: The key of the sorted set to search. - /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - range: The start and stop 0-based indices of the range of elements to include. + /// - key: The key of the sorted set to search. + /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. @inlinable public func zrange( - withinIndices range: (start: Int, stop: Int), + within range: (start: Int, stop: Int), from key: String, - withScores: Bool = false) -> EventLoopFuture<[RESPValue]> - { + withScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { return _zrange(command: "ZRANGE", key, range.start, range.stop, withScores) } - /// Returns the specified range of elements in a sorted set. + /// Gets the specified range of elements in a sorted set. /// - Note: This treats the ordered set as ordered from high to low. - /// For the inverse, see `zrange(of:startIndex:endIndex:withScores:)`. + /// + /// For the inverse, see `zrange(within:from:withScores:)`. /// /// See [https://redis.io/commands/zrevrange](https://redis.io/commands/zrevrange) /// - Parameters: - /// - withinIndices: The start and stop 0-based indices of the range of elements to include. - /// - from: The key of the sorted set to search. - /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] + /// - range: The start and stop 0-based indices of the range of elements to include. + /// - key: The key of the sorted set to search. + /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. @inlinable public func zrevrange( - withinIndices range: (start: Int, stop: Int), + within range: (start: Int, stop: Int), from key: String, - withScores: Bool = false) -> EventLoopFuture<[RESPValue]> - { + withScores: Bool = false + ) -> EventLoopFuture<[RESPValue]> { return _zrange(command: "ZREVRANGE", key, range.start, range.stop, withScores) } @usableFromInline - func _zrange(command: String, _ key: String, _ start: Int, _ stop: Int, _ withScores: Bool) -> EventLoopFuture<[RESPValue]> { + func _zrange( + command: String, + _ key: String, + _ start: Int, + _ stop: Int, + _ withScores: Bool + ) -> EventLoopFuture<[RESPValue]> { var args: [RESPValueConvertible] = [key, start, stop] if withScores { args.append("WITHSCORES") } @@ -427,50 +438,58 @@ extension RedisCommandExecutor { // MARK: Range by Score extension RedisCommandExecutor { - /// Returns elements from a sorted set whose score fits within the range specified. + /// Gets elements from a sorted set whose score fits within the range specified. /// - Note: This treats the ordered set as ordered from low to high. - /// For the inverse, see `zrevrangebyscore(of:within:withScores:limitBy:)`. + /// + /// For the inverse, see `zrevrangebyscore(within:from:withScores:limitBy:)`. /// /// See [https://redis.io/commands/zrangebyscore](https://redis.io/commands/zrangebyscore) /// - Parameters: - /// - within: The range of min and max scores to filter elements by. - /// - from: The key of the sorted set to search. - /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] - /// - limitBy: The optional offset and count of items to query. + /// - range: The range of min and max scores to filter elements by. + /// - key: The key of the sorted set to search. + /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] + /// - limit: The optional offset and count of elements to query. /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. @inlinable public func zrangebyscore( within range: (min: String, max: String), from key: String, withScores: Bool = false, - limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> - { + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { return _zrangebyscore(command: "ZRANGEBYSCORE", key, range, withScores, limit) } - /// Returns elements from a sorted set whose score fits within the range specified. + /// Gets elements from a sorted set whose score fits within the range specified. /// - Note: This treats the ordered set as ordered from high to low. - /// For the inverse, see `zrangebyscore(of:within:withScores:limitBy:)`. + /// + /// For the inverse, see `zrangebyscore(within:from:withScores:limitBy:)`. /// /// See [https://redis.io/commands/zrevrangebyscore](https://redis.io/commands/zrevrangebyscore) /// - Parameters: - /// - within: The range of min and max scores to filter elements by. - /// - from: The key of the sorted set to search. - /// - withScores: Should the list contain the items AND their scores? [Item_1, Score_1, Item_2, ...] - /// - limitBy: The optional offset and count of items to query. + /// - range: The range of min and max scores to filter elements by. + /// - key: The key of the sorted set to search. + /// - withScores: Should the list contain the elements AND their scores? [Item_1, Score_1, Item_2, ...] + /// - limit: The optional offset and count of elements to query. /// - Returns: A list of elements from the sorted set that were within the range provided, and optionally their scores. @inlinable public func zrevrangebyscore( within range: (min: String, max: String), from key: String, withScores: Bool = false, - limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> - { + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { return _zrangebyscore(command: "ZREVRANGEBYSCORE", key, (range.max, range.min), withScores, limit) } @usableFromInline - func _zrangebyscore(command: String, _ key: String, _ range: (min: String, max: String), _ withScores: Bool, _ limit: (offset: Int, count: Int)?) -> EventLoopFuture<[RESPValue]> { + func _zrangebyscore( + command: String, + _ key: String, + _ range: (min: String, max: String), + _ withScores: Bool, + _ limit: (offset: Int, count: Int)? + ) -> EventLoopFuture<[RESPValue]> { var args: [RESPValueConvertible] = [key, range.min, range.max] if withScores { args.append("WITHSCORES") } @@ -488,48 +507,55 @@ extension RedisCommandExecutor { // MARK: Range by Lexiographical extension RedisCommandExecutor { - /// Returns elements from a sorted set whose lexiographical values are between the range specified. + /// Gets elements from a sorted set whose lexiographical values are between the range specified. /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. /// - Note: This treats the ordered set as ordered from low to high. - /// For the inverse, see `zrevrangebylex(of:within:limitBy:)`. + /// + /// For the inverse, see `zrevrangebylex(within:from:limitBy:)`. /// /// See [https://redis.io/commands/zrangebylex](https://redis.io/commands/zrangebylex) /// - Parameters: - /// - within: The value range to filter elements by. - /// - from: The key of the sorted set to search. - /// - limitBy: The optional offset and count of items to query. + /// - range: The value range to filter elements by. + /// - key: The key of the sorted set to search. + /// - limit: The optional offset and count of elements to query. /// - Returns: A list of elements from the sorted set that were within the range provided. @inlinable public func zrangebylex( within range: (min: String, max: String), from key: String, - limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> - { + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { return _zrangebylex(command: "ZRANGEBYLEX", key, range, limit) } - /// Returns elements from a sorted set whose lexiographical values are between the range specified. + /// Gets elements from a sorted set whose lexiographical values are between the range specified. /// - Important: This assumes all elements in the sorted set have the same score. If not, the returned elements are unspecified. /// - Note: This treats the ordered set as ordered from high to low. - /// For the inverse, see `zrangebylex(of:within:limitBy:)`. + /// + /// For the inverse, see `zrangebylex(within:from:limitBy:)`. /// /// See [https://redis.io/commands/zrevrangebylex](https://redis.io/commands/zrevrangebylex) /// - Parameters: - /// - within: The value range to filter elements by. - /// - from: The key of the sorted set to search. - /// - limitBy: The optional offset and count of items to query. + /// - range: The value range to filter elements by. + /// - key: The key of the sorted set to search. + /// - limit: The optional offset and count of elements to query. /// - Returns: A list of elements from the sorted set that were within the range provided. @inlinable public func zrevrangebylex( within range: (min: String, max: String), from key: String, - limitBy limit: (offset: Int, count: Int)? = nil) -> EventLoopFuture<[RESPValue]> - { + limitBy limit: (offset: Int, count: Int)? = nil + ) -> EventLoopFuture<[RESPValue]> { return _zrangebylex(command: "ZREVRANGEBYLEX", key, (range.max, range.min), limit) } @usableFromInline - func _zrangebylex(command: String, _ key: String, _ range: (min: String, max: String), _ limit: (offset: Int, count: Int)?) -> EventLoopFuture<[RESPValue]> { + func _zrangebylex( + command: String, + _ key: String, + _ range: (min: String, max: String), + _ limit: (offset: Int, count: Int)? + ) -> EventLoopFuture<[RESPValue]> { var args: [RESPValueConvertible] = [key, range.min, range.max] if let l = limit { @@ -545,18 +571,18 @@ extension RedisCommandExecutor { // MARK: Remove extension RedisCommandExecutor { - /// Removes the specified items from a sorted set. + /// Removes the specified elements from a sorted set. /// /// See [https://redis.io/commands/zrem](https://redis.io/commands/zrem) /// - Parameters: - /// - items: The values to remove from the sorted set. - /// - from: The key of the sorted set. - /// - Returns: The number of items removed from the set. + /// - elements: The values to remove from the sorted set. + /// - key: The key of the sorted set. + /// - Returns: The number of elements removed from the set. @inlinable - public func zrem(_ items: [RESPValueConvertible], from key: String) -> EventLoopFuture { - assert(items.count > 0, "At least 1 item should be provided.") + public func zrem(_ elements: [RESPValueConvertible], from key: String) -> EventLoopFuture { + guard elements.count > 0 else { return self.eventLoop.makeSucceededFuture(0) } - return send(command: "ZREM", with: [key] + items) + return send(command: "ZREM", with: [key] + elements) .mapFromRESP() } @@ -565,11 +591,14 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zremrangebylex](https://redis.io/commands/zremrangebylex) /// - Parameters: - /// - within: The value range to filter for elements to remove. - /// - from: The key of the sorted set to search. + /// - range: The value range to filter for elements to remove. + /// - key: The key of the sorted set to search. /// - Returns: The number of elements removed from the sorted set. @inlinable - public func zremrangebylex(within range: (min: String, max: String), from key: String) -> EventLoopFuture { + public func zremrangebylex( + within range: (min: String, max: String), + from key: String + ) -> EventLoopFuture { return send(command: "ZREMRANGEBYLEX", with: [key, range.min, range.max]) .mapFromRESP() } @@ -578,13 +607,15 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zremrangebyrank](https://redis.io/commands/zremrangebyrank) /// - Parameters: - /// - startingFrom: The starting index of the range. - /// - endingAt: The ending index of the range. - /// - from: The key of the sorted set to search. + /// - range: The index range of elements to remove. + /// - key: The key of the sorted set to search. /// - Returns: The number of elements removed from the sorted set. @inlinable - public func zremrangebyrank(startingFrom start: Int, endingAt stop: Int, from key: String) -> EventLoopFuture { - return send(command: "ZREMRANGEBYRANK", with: [key, start, stop]) + public func zremrangebyrank( + within range: (start: Int, stop: Int), + from key: String + ) -> EventLoopFuture { + return send(command: "ZREMRANGEBYRANK", with: [key, range.start, range.stop]) .mapFromRESP() } @@ -592,11 +623,14 @@ extension RedisCommandExecutor { /// /// See [https://redis.io/commands/zremrangebyscore](https://redis.io/commands/zremrangebyscore) /// - Parameters: - /// - within: The score range to filter for elements to remove. - /// - from: The key of the sorted set to search. + /// - range: The score range to filter for elements to remove. + /// - key: The key of the sorted set to search. /// - Returns: The number of elements removed from the sorted set. @inlinable - public func zremrangebyscore(within range: (min: String, max: String), from key: String) -> EventLoopFuture { + public func zremrangebyscore( + within range: (min: String, max: String), + from key: String + ) -> EventLoopFuture { return send(command: "ZREMRANGEBYSCORE", with: [key, range.min, range.max]) .mapFromRESP() } diff --git a/Sources/NIORedis/Commands/StringCommands.swift b/Sources/NIORedis/Commands/StringCommands.swift new file mode 100644 index 0000000..915be00 --- /dev/null +++ b/Sources/NIORedis/Commands/StringCommands.swift @@ -0,0 +1,162 @@ +import NIO + +// MARK: Get + +extension RedisCommandExecutor { + /// Get the value of a key. + /// - Note: This operation only works with string values. + /// The `EventLoopFuture` will fail with a `RedisError` if the value is not a string, such as a Set. + /// + /// [https://redis.io/commands/get](https://redis.io/commands/get) + /// - Parameter key: The key to fetch the value from. + /// - Returns: The string value stored at the key provided, otherwise `nil` if the key does not exist. + @inlinable + public func get(_ key: String) -> EventLoopFuture { + return send(command: "GET", with: [key]) + .map { return $0.string } + } + + /// Gets the values of all specified keys, using `.null` to represent non-existant values. + /// + /// See [https://redis.io/commands/mget](https://redis.io/commands/mget) + /// - Parameter keys: The list of keys to fetch the values from. + /// - Returns: The values stored at the keys provided, matching the same order. + @inlinable + public func mget(_ keys: [String]) -> EventLoopFuture<[RESPValue]> { + guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture([]) } + + return send(command: "MGET", with: keys) + .mapFromRESP() + } +} + +// MARK: Set + +extension RedisCommandExecutor { + /// Sets the value stored in the key provided, overwriting the previous value. + /// + /// Any previous expiration set on the key is discarded if the SET operation was successful. + /// + /// - Important: Regardless of the type of value stored at the key, it will be overwritten to a string value. + /// + /// [https://redis.io/commands/set](https://redis.io/commands/set) + /// - Parameters: + /// - key: The key to use to uniquely identify this value. + /// - value: The value to set the key to. + /// - Returns: An `EventLoopFuture` that resolves if the operation was successful. + @inlinable + public func set(_ key: String, to value: RESPValueConvertible) -> EventLoopFuture { + return send(command: "SET", with: [key, value]) + .map { _ in () } + } + + /// Sets each key to their 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) + /// - Parameter operations: The key-value list of SET operations to execute. + /// - Returns: An `EventLoopFuture` that resolves if the operation was successful. + @inlinable + public func mset(_ operations: [String: RESPValueConvertible]) -> EventLoopFuture { + return _mset(command: "MSET", operations) + .map { _ in () } + } + + /// Sets each key to their respective new value, only if all keys do not currently exist. + /// - Note: Use `mset(_:)` if you don't care about overwriting values. + /// + /// See [https://redis.io/commands/msetnx](https://redis.io/commands/msetnx) + /// - Parameter operations: The key-value list of SET operations to execute. + /// - Returns: `true` if the operation successfully completed. + @inlinable + public func msetnx(_ operations: [String: RESPValueConvertible]) -> EventLoopFuture { + return _mset(command: "MSETNX", operations) + .mapFromRESP(to: Int.self) + .map { return $0 == 1 } + } + + @usableFromInline + func _mset( + command: String, + _ operations: [String: RESPValueConvertible] + ) -> EventLoopFuture { + assert(operations.count > 0, "At least 1 key-value pair should be provided.") + + let args: [RESPValueConvertible] = operations.reduce(into: [], { (result, element) in + result.append(element.key) + result.append(element.value) + }) + + return send(command: command, with: args) + } +} + +// MARK: Increment + +extension RedisCommandExecutor { + /// Increments the stored value by 1. + /// + /// See [https://redis.io/commands/incr](https://redis.io/commands/incr) + /// - Parameter key: The key whose value should be incremented. + /// - Returns: The new value after the operation. + @inlinable + public func increment(_ key: String) -> EventLoopFuture { + return send(command: "INCR", with: [key]) + .mapFromRESP() + } + + /// Increments the stored value by the amount desired . + /// + /// See [https://redis.io/commands/incrby](https://redis.io/commands/incrby) + /// - Parameters: + /// - key: The key whose value should be incremented. + /// - count: The amount that this value should be incremented, supporting both positive and negative values. + /// - Returns: The new value after the operation. + @inlinable + 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. + /// + /// See [https://redis.io/commands/incrbyfloat](https://redis.io/commands/incrbyfloat) + /// - Parameters: + /// - key: The key whose value should be incremented. + /// - count: The amount that this value should be incremented, supporting both positive and negative values. + /// - Returns: The new value after the operation. + @inlinable + public func increment(_ key: String, by count: T) -> EventLoopFuture + where T: RESPValueConvertible + { + return send(command: "INCRBYFLOAT", with: [key, count]) + .mapFromRESP() + } +} + +// MARK: Decrement + +extension RedisCommandExecutor { + /// Decrements the stored value by 1. + /// + /// See [https://redis.io/commands/decr](https://redis.io/commands/decr) + /// - Parameter key: The key whose value should be decremented. + /// - Returns: The new value after the operation. + @inlinable + public func decrement(_ key: String) -> EventLoopFuture { + return send(command: "DECR", with: [key]) + .mapFromRESP() + } + + /// Decrements the stored valye by the amount desired. + /// + /// See [https://redis.io/commands/decrby](https://redis.io/commands/decrby) + /// - Parameters: + /// - key: The key whose value should be decremented. + /// - count: The amount that this value should be decremented, supporting both positive and negative values. + /// - Returns: The new value after the operation. + public func decrement(_ key: String, by count: Int) -> EventLoopFuture { + return send(command: "DECRBY", with: [key, count]) + .mapFromRESP() + } +}