Refine Redis Command API

Motivation:

It was noticed that many of the commands are cumbersome to use with boilerplate type casting for each use that can be simplified within the library
by doing type conversion before returning the value to an end user.

Modifications:

Many APIs that return a `RESPValue` now have overloads to provide a `RESPValueConvertible` type that the value will be turned into before being returned.

For a few APIs that returned `RESPValue`, they did so as an Optional. Those APIs have been changed to always provide a `RESPValue` and return `.null` in cases where `nil` was returned.

In addition, the `@inlinable` attribute has been removed from any non-generic command API.

Result:

Developers should have less code boilerplate for turning values from `RESPValue` to their desired type with many commands.
parent 86f2eb69
......@@ -20,7 +20,6 @@ extension RedisClient {
/// 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> {
let args = [RESPValue(bulk: message)]
return send(command: "ECHO", with: args)
......@@ -32,7 +31,6 @@ extension RedisClient {
/// See [https://redis.io/commands/ping](https://redis.io/commands/ping)
/// - 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 args: [RESPValue] = message != nil
? [.init(bulk: message!)] // safe because we did a nil pre-check
......@@ -47,7 +45,6 @@ extension RedisClient {
/// [https://redis.io/commands/select](https://redis.io/commands/select)
/// - 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> {
let args = [RESPValue(bulk: index)]
return send(command: "SELECT", with: args)
......@@ -61,7 +58,6 @@ extension RedisClient {
/// - first: The index of the first database.
/// - second: The index of the second database.
/// - Returns: `true` if the swap was successful.
@inlinable
public func swapDatabase(_ first: Int, with second: Int) -> EventLoopFuture<Bool> {
let args: [RESPValue] = [
.init(bulk: first),
......@@ -77,7 +73,6 @@ extension RedisClient {
/// [https://redis.io/commands/auth](https://redis.io/commands/auth)
/// - Parameter password: The password to authenticate with.
/// - Returns: A `NIO.EventLoopFuture` that resolves if the password was accepted, otherwise it fails.
@inlinable
public func authorize(with password: String) -> EventLoopFuture<Void> {
let args = [RESPValue(bulk: password)]
return send(command: "AUTH", with: args)
......@@ -89,7 +84,6 @@ extension RedisClient {
/// [https://redis.io/commands/del](https://redis.io/commands/del)
/// - 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: [RedisKey]) -> EventLoopFuture<Int> {
guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
......@@ -103,7 +97,6 @@ extension RedisClient {
/// [https://redis.io/commands/del](https://redis.io/commands/del)
/// - 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: RedisKey...) -> EventLoopFuture<Int> {
return self.delete(keys)
}
......@@ -116,7 +109,6 @@ extension RedisClient {
/// - key: The key to set the expiration on.
/// - timeout: The time from now the key will expire at.
/// - Returns: `true` if the expiration was set.
@inlinable
public func expire(_ key: RedisKey, after timeout: TimeAmount) -> EventLoopFuture<Bool> {
let args: [RESPValue] = [
.init(bulk: key),
......@@ -136,16 +128,15 @@ extension RedisClient {
/// [https://redis.io/commands/scan](https://redis.io/commands/scan)
/// - Parameters:
/// - position: The cursor position to start 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.
/// - count: The number of elements to advance by. Redis default is 10.
/// - Returns: A cursor position for additional invocations with a limited collection of keys found in the database.
@inlinable
public func scan(
startingFrom position: Int = 0,
count: Int? = nil,
matching match: String? = nil
matching match: String? = nil,
count: Int? = nil
) -> EventLoopFuture<(Int, [String])> {
return _scan(command: "SCAN", nil, position, count, match)
return _scan(command: "SCAN", nil, position, match, count)
}
@usableFromInline
......@@ -154,8 +145,8 @@ extension RedisClient {
resultType: T.Type = T.self,
_ key: RedisKey?,
_ pos: Int,
_ count: Int?,
_ match: String?
_ match: String?,
_ count: Int?
) -> EventLoopFuture<(Int, T)>
where
T: RESPValueConvertible
......
......@@ -18,14 +18,18 @@ import NIO
extension RedisClient {
@usableFromInline
internal static func _mapHashResponse(_ values: [String]) -> [String: String] {
internal static func _mapHashResponse(_ values: [RESPValue]) throws -> [String: RESPValue] {
guard values.count > 0 else { return [:] }
var result: [String: String] = [:]
var result: [String: RESPValue] = [:]
var index = 0
repeat {
let field = values[index]
guard let field = String(fromRESP: values[index]) else {
throw RedisClientError.assertionFailure(
message: "Received non-string value where string hash field key was expected. Raw Value: \(values[index])"
)
}
let value = values[index + 1]
result[field] = value
index += 2
......@@ -45,7 +49,6 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<Int> {
guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
......@@ -63,7 +66,6 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<Int> {
return self.hdel(fields, from: key)
}
......@@ -75,7 +77,6 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<Bool> {
let args: [RESPValue] = [
.init(bulk: key),
......@@ -91,7 +92,6 @@ extension RedisClient {
/// 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: RedisKey) -> EventLoopFuture<Int> {
let args = [RESPValue(bulk: key)]
return send(command: "HLEN", with: args)
......@@ -105,7 +105,6 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<Int> {
let args: [RESPValue] = [
.init(bulk: key),
......@@ -120,7 +119,6 @@ extension RedisClient {
/// 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: RedisKey) -> EventLoopFuture<[String]> {
let args = [RESPValue(bulk: key)]
return send(command: "HKEYS", with: args)
......@@ -132,12 +130,24 @@ extension RedisClient {
/// 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: RedisKey) -> EventLoopFuture<[RESPValue]> {
let args = [RESPValue(bulk: key)]
return send(command: "HVALS", with: args)
.map()
}
/// Gets all values stored in a hash.
///
/// See [https://redis.io/commands/hvals](https://redis.io/commands/hvals)
/// - Parameters:
/// - key: The key of the hash.
/// - type: The type to convert the values to.
/// - Returns: A list of all values stored in a hash.
@inlinable
public func hvals<Value: RESPValueConvertible>(in key: RedisKey, as type: Value.Type) -> EventLoopFuture<[Value?]> {
return self.hvals(in: key)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// Incrementally iterates over all fields in a hash.
///
......@@ -145,19 +155,43 @@ extension RedisClient {
/// - 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.
/// - count: The number of elements to advance by. Redis default is 10.
/// - valueType: The type to cast all values to.
/// - Returns: A cursor position for additional invocations with a limited collection of found fields and their values.
@inlinable
public func hscan(
public func hscan<Value: RESPValueConvertible>(
_ key: RedisKey,
startingFrom position: Int = 0,
matching match: String? = nil,
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)
valueType: Value.Type
) -> EventLoopFuture<(Int, [String: Value?])> {
return self.hscan(key, startingFrom: position, matching: match, count: count)
.map { (cursor, fields) in
let mappedFields = fields.mapValues(Value.init(fromRESP:))
return (cursor, mappedFields)
}
}
/// 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.
/// - match: A glob-style pattern to filter values to be selected from the result set.
/// - count: The number of elements to advance by. Redis default is 10.
/// - Returns: A cursor position for additional invocations with a limited collection of found fields and their values.
public func hscan(
_ key: RedisKey,
startingFrom position: Int = 0,
matching match: String? = nil,
count: Int? = nil
) -> EventLoopFuture<(Int, [String: RESPValue])> {
return _scan(command: "HSCAN", resultType: [RESPValue].self, key, position, match, count)
.flatMapThrowing {
let values = try Self._mapHashResponse($0.1)
return ($0.0, values)
}
}
......@@ -250,15 +284,31 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<String?> {
/// - Returns: The value of the hash field. If the key or field does not exist, it will be `.null`.
public func hget(_ field: String, from key: RedisKey) -> EventLoopFuture<RESPValue> {
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field)
]
return send(command: "HGET", with: args)
.map { return String(fromRESP: $0) }
}
/// Gets a hash field's value as the desired type.
///
/// 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.
/// - type: The type to convert the value to.
/// - Returns: The value of the hash field, or `nil` if the `RESPValue` conversion fails or either the key or field does not exist.
@inlinable
public func hget<Value: RESPValueConvertible>(
_ field: String,
from key: RedisKey,
as type: Value.Type
) -> EventLoopFuture<Value?> {
return self.hget(field, from: key)
.map(Value.init(fromRESP:))
}
/// Gets the values of a hash for the fields specified.
......@@ -267,9 +317,8 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<[String?]> {
/// - Returns: A list of values in the same order as the `fields` argument. Non-existent fields return `.null` values.
public func hmget(_ fields: [String], from key: RedisKey) -> EventLoopFuture<[RESPValue]> {
guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture([]) }
var args: [RESPValue] = [.init(bulk: key)]
......@@ -277,7 +326,24 @@ extension RedisClient {
return send(command: "HMGET", with: args)
.map(to: [RESPValue].self)
.map { return $0.map(String.init) }
}
/// Gets the values of a hash for the fields specified as a specific type.
///
/// 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.
/// - type: The type to convert the values to.
/// - Returns: A list of values in the same order as the `fields` argument. Non-existent fields and elements that fail the `RESPValue` conversion return `nil` values.
@inlinable
public func hmget<Value: RESPValueConvertible>(
_ fields: [String],
from key: RedisKey,
as type: Value.Type
) -> EventLoopFuture<[Value?]> {
return self.hmget(fields, from: key)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// Gets the values of a hash for the fields specified.
......@@ -286,23 +352,54 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<[String?]> {
/// - Returns: A list of values in the same order as the `fields` argument. Non-existent fields return `.null` values.
public func hmget(_ fields: String..., from key: RedisKey) -> EventLoopFuture<[RESPValue]> {
return self.hmget(fields, from: key)
}
/// 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.
/// - type: The type to convert the values to.
/// - Returns: A list of values in the same order as the `fields` argument. Non-existent fields and elements that fail the `RESPValue` conversion return `nil` values.
@inlinable
public func hmget<Value: RESPValueConvertible>(
_ fields: String...,
from key: RedisKey,
as type: Value.Type
) -> EventLoopFuture<[Value?]> {
return self.hmget(fields, from: key, as: type)
}
/// 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: RedisKey) -> EventLoopFuture<[String: String]> {
public func hgetall(from key: RedisKey) -> EventLoopFuture<[String: RESPValue]> {
let args = [RESPValue(bulk: key)]
return send(command: "HGETALL", with: args)
.map(to: [String].self)
.map(Self._mapHashResponse)
.map(to: [RESPValue].self)
.flatMapThrowing(Self._mapHashResponse)
}
/// Returns all the fields and values stored in a hash.
///
/// See [https://redis.io/commands/hgetall](https://redis.io/commands/hgetall)
/// - Parameters:
/// - key: The key of the hash to pull from.
/// - type: The type to convert the values to.
/// - Returns: A key-value pair list of fields and their values. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func hgetall<Value: RESPValueConvertible>(
from key: RedisKey,
as type: Value.Type
) -> EventLoopFuture<[String: Value?]> {
return self.hgetall(from: key)
.map { return $0.mapValues(Value.init(fromRESP:)) }
}
}
......@@ -318,7 +415,11 @@ extension RedisClient {
/// - 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: RedisKey) -> EventLoopFuture<Int> {
public func hincrby<Value: FixedWidthInteger & RESPValueConvertible>(
_ amount: Value,
field: String,
in key: RedisKey
) -> EventLoopFuture<Value> {
return _hincr(command: "HINCRBY", amount, field, key)
}
......@@ -331,11 +432,11 @@ extension RedisClient {
/// - key: The key of the hash the field is stored in.
/// - Returns: The new value of the hash field.
@inlinable
public func hincrbyfloat<Value>(_ amount: Value, field: String, in key: RedisKey) -> EventLoopFuture<Value>
where
Value: BinaryFloatingPoint,
Value: RESPValueConvertible
{
public func hincrbyfloat<Value: BinaryFloatingPoint & RESPValueConvertible>(
_ amount: Value,
field: String,
in key: RedisKey
) -> EventLoopFuture<Value> {
return _hincr(command: "HINCRBYFLOAT", amount, field, key)
}
......
......@@ -25,12 +25,27 @@ extension RedisClient {
/// 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: RedisKey) -> EventLoopFuture<[RESPValue]> {
let args = [RESPValue(bulk: key)]
return send(command: "SMEMBERS", with: args)
.map()
}
/// 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.
///
/// See [https://redis.io/commands/smembers](https://redis.io/commands/smembers)
/// - Parameters:
/// - key: The key of the set.
/// - type: The type to convert the values to.
/// - Returns: A list of elements found within the set. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func smembers<Value: RESPValueConvertible>(of key: RedisKey, as type: Value.Type) -> EventLoopFuture<[Value?]> {
return self.smembers(of: key)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// Checks if the element is included in a set.
///
......@@ -55,7 +70,6 @@ extension RedisClient {
/// 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: RedisKey) -> EventLoopFuture<Int> {
let args = [RESPValue(bulk: key)]
return send(command: "SCARD", with: args)
......@@ -129,7 +143,6 @@ extension RedisClient {
/// - 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: RedisKey, max count: Int = 1) -> EventLoopFuture<[RESPValue]> {
assert(count >= 0, "A negative max count is nonsense.")
......@@ -143,6 +156,24 @@ extension RedisClient {
.map()
}
/// Randomly selects and removes one or more elements in a set.
///
/// See [https://redis.io/commands/spop](https://redis.io/commands/spop)
/// - Parameters:
/// - key: The key of the set.
/// - type: The type to convert the values to.
/// - count: The max number of elements to pop from the set.
/// - Returns: The element that was popped from the set. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func spop<Value: RESPValueConvertible>(
from key: RedisKey,
as type: Value.Type,
max count: Int = 1
) -> EventLoopFuture<[Value?]> {
return self.spop(from: key, max: count)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// Randomly selects one or more elements in a set.
///
/// connection.srandmember("my_key") // pulls just one random element
......@@ -154,7 +185,6 @@ extension RedisClient {
/// - 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: RedisKey, max count: Int = 1) -> EventLoopFuture<[RESPValue]> {
guard count != 0 else { return self.eventLoop.makeSucceededFuture([]) }
......@@ -165,6 +195,24 @@ extension RedisClient {
return send(command: "SRANDMEMBER", with: args)
.map()
}
/// Randomly selects one or more elements in a set.
///
/// See [https://redis.io/commands/srandmember](https://redis.io/commands/srandmember)
/// - Parameters:
/// - key: The key of the set.
/// - type; The type to convert the values to.
/// - count: The max number of elements to select from the set.
/// - Returns: The elements randomly selected from the set. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func srandmember<Value: RESPValueConvertible>(
from key: RedisKey,
as type: Value.Type,
max count: Int = 1
) -> EventLoopFuture<[Value?]> {
return self.srandmember(from: key, max: count)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// Moves an element from one set to another.
///
......@@ -201,14 +249,38 @@ extension RedisClient {
/// - 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: RedisKey,
startingFrom position: Int = 0,
count: Int? = nil,
matching match: String? = nil
matching match: String? = nil,
count: Int? = nil
) -> EventLoopFuture<(Int, [RESPValue])> {
return _scan(command: "SSCAN", key, position, count, match)
return _scan(command: "SSCAN", key, position, match, count)
}
/// Incrementally iterates over all values in a set.
///
/// 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.
/// - match: A glob-style pattern to filter values to be selected from the result set.
/// - count: The number of elements to advance by. Redis default is 10.
/// - valueType: The type to convert the value to.
/// - Returns: A cursor position for additional invocations with a limited collection of elements found in the set. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func sscan<Value: RESPValueConvertible>(
_ key: RedisKey,
startingFrom position: Int = 0,
matching match: String? = nil,
count: Int? = nil,
valueType: Value.Type
) -> EventLoopFuture<(Int, [Value?])> {
return self.sscan(key, startingFrom: position, matching: match, count: count)
.map { (cursor, rawValues) in
let values = rawValues.map(Value.init(fromRESP:))
return (cursor, values)
}
}
}
......@@ -220,7 +292,6 @@ extension RedisClient {
/// 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: [RedisKey]) -> EventLoopFuture<[RESPValue]> {
guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture([]) }
......@@ -229,15 +300,39 @@ extension RedisClient {
.map()
}
/// Calculates the difference between two or more sets.
///
/// See [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff)
/// - Parameters:
/// - keys: The source sets to calculate the difference of.
/// - valueType: The type to convert the values to.
/// - Returns: A list of elements resulting from the difference. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func sdiff<Value: RESPValueConvertible>(of keys: [RedisKey], valueType: Value.Type) -> EventLoopFuture<[Value?]> {
return self.sdiff(of: keys)
.map { return $0.map(Value.init(fromRESP:)) }
}
/// 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: RedisKey...) -> EventLoopFuture<[RESPValue]> {
return self.sdiff(of: keys)
}
/// Calculates the difference between two or more sets.
///
/// See [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff)
/// - Parameters:
/// - keys: The source sets to calculate the difference of.
/// - valueType: The type to convert the values to.
/// - Returns: A list of elements resulting from the difference. Elements that fail the `RESPValue` conversion will be `nil`.
@inlinable
public func sdiff<Value: RESPValueConvertible>(of keys: RedisKey..., valueType: Value.Type