Commit f4e646c1 authored by Nathan Harris's avatar Nathan Harris

Audit convenience commands

Motivation:

Performance, clarity, and uniformity with code documentation, Swift API, and data types.
parent 601431ac
......@@ -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<String> {
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<String> {
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<Void> {
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<Void> {
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<Void> {
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<Bool> {
return send(command: "SWAPDB", with: [firstIndex, secondIndex])
@inlinable
public func swapDatabase(_ first: Int, with second: Int) -> EventLoopFuture<Bool> {
/// 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<Int> {
/// - 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<Int> {
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<Bool> {
return send(command: "EXPIRE", with: [key, deadline.description])
.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<String?> {
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<Void> {
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<Void> {
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<Bool> {
assert(operations.count > 0, "At least 1 key-value pair should be provided.")
let args = _convertMSET(operations)
return send(command: "MSETNX", with: args)
/// - 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<Bool> {
let amount = deadline.nanoseconds / 1_000_000_000
return send(command: "EXPIRE", with: [key, amount])
.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<Int> {
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<Int> {
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<T: BinaryFloatingPoint>(_ key: String, by count: T) -> EventLoopFuture<T>
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<Int> {
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<Int> {
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<T: RESPValueConvertible>(
@usableFromInline
func _scan<T>(
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
......
import NIO
extension RedisCommandExecutor {
/// Sets the hash field stored at the provided key with the value specified.
///
/// See [https://redis.io/commands/hset](https://redis.io/commands/hset)
/// - Returns: `true` if the hash was created, `false` if it was updated.
@inlinable
public func hset(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
return send(command: "HSET", with: [key, field, value])
.mapFromRESP(to: Int.self)
.map { return $0 == 1 }
}
/// Sets the specified fields to the values provided, overwriting existing values.
///
/// See [https://redis.io/commands/hmset](https://redis.io/commands/hmset)
@inlinable
public func hmset(_ key: String, to fields: [String: String]) -> EventLoopFuture<Void> {
assert(fields.count > 0, "At least 1 key-value pair should be specified")
let args: [RESPValueConvertible] = fields.reduce(into: [], { (result, element) in
result.append(element.key)
result.append(element.value)
})
return send(command: "HMSET", with: [key] + args)
.map { _ in () }
}
// MARK: Static Helpers
/// Sets the specified hash field to the value provided only if the field does not exist.
///
/// See [https://redis.io/commands/hsetnx](https://redis.io/commands/hsetnx)
/// - Returns: The success of setting the field's value.
@inlinable
public func hsetnx(_ key: String, field: String, to value: String) -> EventLoopFuture<Bool> {
return send(command: "HSETNX", with: [key, field, value])
.mapFromRESP(to: Int.self)
.map { return $0 == 1 }
}
extension RedisCommandExecutor {
@usableFromInline
static func _mapHashResponse(_ values: [String]) -> [String: String] {
guard values.count > 0 else { return [:] }
/// Gets the value stored in the hash field at the key provided.
///
/// See [https://redis.io/commands/hget](https://redis.io/commands/hget)
@inlinable
public func hget(_ key: String, field: String) -> EventLoopFuture<String?> {
return send(command: "HGET", with: [key, field])
.map { return String($0) }
}
var result: [String: String] = [:]
/// 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")
var index = 0
repeat {
let field = values[index]
let value = values[index + 1]
result[field] = value
index += 2
} while (index < values.count)
return send(command: "HMGET", with: [key] + fields)
.mapFromRESP(to: [RESPValue].self)
.map { return $0.map(String.init) }
return result
}
}
/// 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)
}
// MARK: General
/// Removes the specified fields from the hash stored at the key provided.
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(_ key: String, fields: [String]) -> EventLoopFuture<Int> {
assert(fields.count > 0, "At least 1 field should be specified")
public func hdel(_ fields: [String], from key: String) -> EventLoopFuture<Int> {
guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
return send(command: "HDEL", with: [key] + fields)
.mapFromRESP()
}
/// Checks if the provided key and field exist.
/// 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(_ key: String, field: String) -> EventLoopFuture<Bool> {
public func hexists(_ field: String, in key: String) -> EventLoopFuture<Bool> {
return send(command: "HEXISTS", with: [key, field])
.mapFromRESP(to: Int.self)
.map { return $0 == 1 }
}
/// Returns the number of fields contained in the hash stored at the key provided.
/// Gets the number of fields contained in a hash.
///
/// 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.
/// - 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<Int> {
return send(command: "HLEN", with: [key])
.mapFromRESP()
}
/// Returns hash field's value length as a string, stored at the provided key.
/// 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 key: String, field: String) -> EventLoopFuture<Int> {
public func hstrlen(of field: String, in key: String) -> EventLoopFuture<Int> {
return send(command: "HSTRLEN", with: [key, field])
.mapFromRESP()
}
/// Returns all field names in the hash stored at the key provided.
/// Gets all field names in a hash.
///
/// See [https://redis.io/commands/hkeys](https://redis.io/commands/hkeys)
/// - Returns: An array of field names, or an empty array.
/// - Parameter key: The key of the hash.
/// - Returns: A list of field names stored within the hash.
@inlinable
public func hkeys(storedAt key: String) -> EventLoopFuture<[String]> {
public func hkeys(in key: String) -> EventLoopFuture<[String]> {
return send(command: "HKEYS", with: [key])
.mapFromRESP()
}
/// Returns all of the field values stored in hash at the key provided.
/// 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(storedAt key: String) -> EventLoopFuture<[String]> {
public func hvals(in key: String) -> EventLoopFuture<[RESPValue]> {
return send(command: "HVALS", with: [key])
.mapFromRESP()
}
/// Increments the field value stored at the key provided, and returns the new value.
///
/// See [https://redis.io/commands/hincrby](https://redis.io/commands/hincrby)
@inlinable
public func hincrby(_ key: String, field: String, by amount: Int) -> EventLoopFuture<Int> {
return send(command: "HINCRBY", with: [key, field, amount])
.mapFromRESP()
}
/// Increments the field value stored at the key provided, and returns the new value.
///
/// See [https://redis.io/commands/hincrbyfloat](https://redis.io/commands/hincrbyfloat)
@inlinable
public func hincrbyfloat<T: BinaryFloatingPoint>(_ key: String, field: String, by amount: T) -> EventLoopFuture<T>
where T: RESPValueConvertible
{
return send(command: "HINCRBYFLOAT", with: [key, field, amount])
.mapFromRESP()
}
/// Incrementally iterates over all fields in the hash stored at the key provided.
/// 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.
/// - atPosition: 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 stored at the keys.
/// - 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,
atPosition pos: Int = 0,
startingFrom position: Int = 0,
count: Int? = nil,
matching match: String? = nil) -> EventLoopFuture<(Int, [String: String])>
{
return _scan(command: "HSCAN", resultType: [String].self, key, pos, count, match)
matching match: String? = nil
) -> EventLoopFuture<(Int, [String: String])> {
return _scan(command: "HSCAN", resultType: [String].self, key, position, count, match)
.map {
let values = Self.mapHashResponseToDictionary($0.1)
let values = Self._mapHashResponse($0.1)
return ($0.0, values)
}
}
}
// MARK: Set
extension RedisCommandExecutor {
@inline(__always)
@usableFromInline
static func mapHashResponseToDictionary(_ values: [String]) -> [String: String] {
guard values.count > 0 else { return [:] }
/// 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<Bool> {
return send(command: "HSET", with: [key, field, value])
.mapFromRESP(to: Int.self)
.map { return $0 == 1 }
}
var result: [String: String] = [:]
/// 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<Bool> {
return send(command: "HSETNX", with: [key, field, value])
.mapFromRESP(to: Int.self)
.map { return $0 == 1 }
}
var index = 0
repeat {
let field = values[index]
let value = values[index + 1]
result[field] = value
index += 2
} while (index < values.count)
/// 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<Void> {
assert(fields.count > 0, "At least 1 key-value pair should be specified")
return result