59 -- Use `RESPValueConvertible` as Generic Constraint

Motivation:

Johannes continues to provide great insight, and correctly pointed out that `RESPValueConvertible` was being used as an "existential" in all cases.

This can cause unexpected type-erasure and introduce unnecessary cost overhead with dynamic dispatch when in most cases we know the exact value we want for `RESPValue` to execute commands.

Modifications:

- Add new extensions to `Array where Element == RESPValue` for appending and adding elements into them
- Change `RedisClient.send(command:with:)` to require `[RESPValue]` instead of `[RESPValueConvertible]` as the `with` argument type
- Change all instances of `RESPValueConvertible` being an "existential" type for method arguments to instead be a generic constraint

Result:

The library should be safeguarded from a class of bugs, with the use of `send` being a bit more straight forward, with some new convenience methods for `[RESPValue]` types.
parent e964ba04
Pipeline #69350304 passed with stages
in 10 minutes and 55 seconds
......@@ -22,7 +22,8 @@ extension RedisClient {
/// - Returns: The message sent with the command.
@inlinable
public func echo(_ message: String) -> EventLoopFuture<String> {
return send(command: "ECHO", with: [message])
let args = [RESPValue(bulk: message)]
return send(command: "ECHO", with: args)
.convertFromRESPValue()
}
......@@ -33,8 +34,10 @@ extension RedisClient {
/// - 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)
let args: [RESPValue] = message != nil
? [.init(bulk: message!)] // safe because we did a nil pre-check
: []
return send(command: "PING", with: args)
.convertFromRESPValue()
}
......@@ -46,7 +49,8 @@ extension RedisClient {
/// - 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])
let args = [RESPValue(bulk: index)]
return send(command: "SELECT", with: args)
.map { _ in return () }
}
......@@ -59,8 +63,11 @@ extension RedisClient {
/// - Returns: `true` if the swap was successful.
@inlinable
public func swapDatabase(_ first: Int, with second: Int) -> EventLoopFuture<Bool> {
/// connection.swapDatabase(index: 0, withIndex: 10)
return send(command: "SWAPDB", with: [first, second])
let args: [RESPValue] = [
.init(bulk: first),
.init(bulk: second)
]
return send(command: "SWAPDB", with: args)
.convertFromRESPValue(to: String.self)
.map { return $0 == "OK" }
}
......@@ -74,7 +81,8 @@ extension RedisClient {
public func delete(_ keys: [String]) -> EventLoopFuture<Int> {
guard keys.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
return send(command: "DEL", with: keys)
let args = keys.map(RESPValue.init)
return send(command: "DEL", with: args)
.convertFromRESPValue()
}
......@@ -89,7 +97,11 @@ extension RedisClient {
@inlinable
public func expire(_ key: String, after timeout: TimeAmount) -> EventLoopFuture<Bool> {
let amount = timeout.nanoseconds / 1_000_000_000
return send(command: "EXPIRE", with: [key, amount])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: amount.description)
]
return send(command: "EXPIRE", with: args)
.convertFromRESPValue(to: Int.self)
.map { return $0 == 1 }
}
......@@ -116,7 +128,7 @@ extension RedisClient {
}
@usableFromInline
func _scan<T>(
internal func _scan<T>(
command: String,
resultType: T.Type = T.self,
_ key: String?,
......@@ -127,19 +139,19 @@ extension RedisClient {
where
T: RESPValueConvertible
{
var args: [RESPValueConvertible] = [pos]
var args: [RESPValue] = [.init(bulk: pos)]
if let k = key {
args.insert(k, at: 0)
args.insert(.init(bulk: k), at: 0)
}
if let m = match {
args.append("match")
args.append(m)
args.append(.init(bulk: "match"))
args.append(.init(bulk: m))
}
if let c = count {
args.append("count")
args.append(c)
args.append(.init(bulk: "count"))
args.append(.init(bulk: c))
}
let response = send(command: command, with: args).convertFromRESPValue(to: [RESPValue].self)
......
......@@ -18,7 +18,7 @@ import NIO
extension RedisClient {
@usableFromInline
static func _mapHashResponse(_ values: [String]) -> [String: String] {
internal static func _mapHashResponse(_ values: [String]) -> [String: String] {
guard values.count > 0 else { return [:] }
var result: [String: String] = [:]
......@@ -48,8 +48,11 @@ extension RedisClient {
@inlinable
public func hdel(_ fields: [String], from key: String) -> EventLoopFuture<Int> {
guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture(0) }
var args: [RESPValue] = [.init(bulk: key)]
args.append(convertingContentsOf: fields)
return send(command: "HDEL", with: [key] + fields)
return send(command: "HDEL", with: args)
.convertFromRESPValue()
}
......@@ -62,7 +65,11 @@ extension RedisClient {
/// - 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<Bool> {
return send(command: "HEXISTS", with: [key, field])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field)
]
return send(command: "HEXISTS", with: args)
.convertFromRESPValue(to: Int.self)
.map { return $0 == 1 }
}
......@@ -74,7 +81,8 @@ extension RedisClient {
/// - 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])
let args = [RESPValue(bulk: key)]
return send(command: "HLEN", with: args)
.convertFromRESPValue()
}
......@@ -87,7 +95,11 @@ extension RedisClient {
/// - 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<Int> {
return send(command: "HSTRLEN", with: [key, field])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field)
]
return send(command: "HSTRLEN", with: args)
.convertFromRESPValue()
}
......@@ -98,7 +110,8 @@ extension RedisClient {
/// - 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])
let args = [RESPValue(bulk: key)]
return send(command: "HKEYS", with: args)
.convertFromRESPValue()
}
......@@ -109,7 +122,8 @@ extension RedisClient {
/// - 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])
let args = [RESPValue(bulk: key)]
return send(command: "HVALS", with: args)
.convertFromRESPValue()
}
......@@ -150,12 +164,17 @@ extension RedisClient {
/// - key: The key that holds the hash.
/// - Returns: `true` if the hash was created, `false` if it was updated.
@inlinable
public func hset(
public func hset<Value: RESPValueConvertible>(
_ field: String,
to value: RESPValueConvertible,
to value: Value,
in key: String
) -> EventLoopFuture<Bool> {
return send(command: "HSET", with: [key, field, value])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field),
value.convertedToRESPValue()
]
return send(command: "HSET", with: args)
.convertFromRESPValue(to: Int.self)
.map { return $0 == 1 }
}
......@@ -170,12 +189,17 @@ extension RedisClient {
/// - key: The key that holds the hash.
/// - Returns: `true` if the hash was created.
@inlinable
public func hsetnx(
public func hsetnx<Value: RESPValueConvertible>(
_ field: String,
to value: RESPValueConvertible,
to value: Value,
in key: String
) -> EventLoopFuture<Bool> {
return send(command: "HSETNX", with: [key, field, value])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field),
value.convertedToRESPValue()
]
return send(command: "HSETNX", with: args)
.convertFromRESPValue(to: Int.self)
.map { return $0 == 1 }
}
......@@ -188,18 +212,19 @@ extension RedisClient {
/// - 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],
public func hmset<Value: RESPValueConvertible>(
_ fields: [String: Value],
in key: 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)
})
var args: [RESPValue] = [.init(bulk: key)]
args.add(contentsOf: fields, overestimatedCountBeingAdded: fields.count * 2) { (array, element) in
array.append(.init(bulk: element.key))
array.append(element.value.convertedToRESPValue())
}
return send(command: "HMSET", with: [key] + args)
return send(command: "HMSET", with: args)
.map { _ in () }
}
}
......@@ -216,7 +241,11 @@ extension RedisClient {
/// - 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<String?> {
return send(command: "HGET", with: [key, field])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field)
]
return send(command: "HGET", with: args)
.map { return String(fromRESP: $0) }
}
......@@ -230,8 +259,11 @@ extension RedisClient {
@inlinable
public func hmget(_ fields: [String], from key: String) -> EventLoopFuture<[String?]> {
guard fields.count > 0 else { return self.eventLoop.makeSucceededFuture([]) }
var args: [RESPValue] = [.init(bulk: key)]
args.append(convertingContentsOf: fields)
return send(command: "HMGET", with: [key] + fields)
return send(command: "HMGET", with: args)
.convertFromRESPValue(to: [RESPValue].self)
.map { return $0.map(String.init) }
}
......@@ -243,7 +275,8 @@ extension RedisClient {
/// - 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])
let args = [RESPValue(bulk: key)]
return send(command: "HGETALL", with: args)
.convertFromRESPValue(to: [String].self)
.map(Self._mapHashResponse)
}
......@@ -262,9 +295,7 @@ extension RedisClient {
/// - Returns: The new value of the hash field.
@inlinable
public func hincrby(_ amount: Int, field: String, in key: String) -> EventLoopFuture<Int> {
/// connection.hincrby(20, field: "foo", in: "key")
return send(command: "HINCRBY", with: [key, field, amount])
.convertFromRESPValue()
return _hincr(command: "HINCRBY", amount, field, key)
}
/// Increments a hash field's value and returns the new value.
......@@ -276,12 +307,27 @@ 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<T>(_ amount: T, field: String, in key: String) -> EventLoopFuture<T>
public func hincrbyfloat<Value>(_ amount: Value, field: String, in key: String) -> EventLoopFuture<Value>
where
T: BinaryFloatingPoint,
T: RESPValueConvertible
Value: BinaryFloatingPoint,
Value: RESPValueConvertible
{
return send(command: "HINCRBYFLOAT", with: [key, field, amount])
return _hincr(command: "HINCRBYFLOAT", amount, field, key)
}
@usableFromInline
internal func _hincr<Value: RESPValueConvertible>(
command: String,
_ amount: Value,
_ field: String,
_ key: String
) -> EventLoopFuture<Value> {
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: field),
amount.convertedToRESPValue()
]
return send(command: command, with: args)
.convertFromRESPValue()
}
}
......@@ -24,7 +24,8 @@ extension RedisClient {
/// - Returns: The number of elements in the list.
@inlinable
public func llen(of key: String) -> EventLoopFuture<Int> {
return send(command: "LLEN", with: [key])
let args = [RESPValue(bulk: key)]
return send(command: "LLEN", with: args)
.convertFromRESPValue()
}
......@@ -37,7 +38,11 @@ extension RedisClient {
/// - Returns: The element stored at index, or `.null` if out of bounds.
@inlinable
public func lindex(_ index: Int, from key: String) -> EventLoopFuture<RESPValue> {
return send(command: "LINDEX", with: [key, index])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: index)
]
return send(command: "LINDEX", with: args)
}
/// Sets the value of an element in a list at the provided index position.
......@@ -49,12 +54,17 @@ extension RedisClient {
/// - 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(
public func lset<Value: RESPValueConvertible>(
index: Int,
to value: RESPValueConvertible,
to value: Value,
in key: String
) -> EventLoopFuture<Void> {
return send(command: "LSET", with: [key, index, value])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: index),
value.convertedToRESPValue()
]
return send(command: "LSET", with: args)
.map { _ in () }
}
......@@ -67,12 +77,17 @@ extension RedisClient {
/// - 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,
public func lrem<Value: RESPValueConvertible>(
_ value: Value,
from key: String,
count: Int = 0
) -> EventLoopFuture<Int> {
return send(command: "LREM", with: [key, count, value])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: count),
value.convertedToRESPValue()
]
return send(command: "LREM", with: args)
.convertFromRESPValue()
}
......@@ -86,7 +101,12 @@ extension RedisClient {
/// - Returns: An `EventLoopFuture` that resolves when the operation has succeeded, or fails with a `RedisError`.
@inlinable
public func ltrim(_ key: String, before start: Int, after stop: Int) -> EventLoopFuture<Void> {
return send(command: "LTRIM", with: [key, start, stop])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: start),
.init(bulk: stop)
]
return send(command: "LTRIM", with: args)
.map { _ in () }
}
......@@ -102,7 +122,12 @@ extension RedisClient {
within range: (startIndex: Int, endIndex: Int),
from key: String
) -> EventLoopFuture<[RESPValue]> {
return send(command: "LRANGE", with: [key, range.startIndex, range.endIndex])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: range.startIndex),
.init(bulk: range.endIndex)
]
return send(command: "LRANGE", with: args)
.convertFromRESPValue()
}
......@@ -115,7 +140,11 @@ extension RedisClient {
/// - Returns: The element that was moved.
@inlinable
public func rpoplpush(from source: String, to dest: String) -> EventLoopFuture<RESPValue> {
return send(command: "RPOPLPUSH", with: [source, dest])
let args: [RESPValue] = [
.init(bulk: source),
.init(bulk: dest)
]
return send(command: "RPOPLPUSH", with: args)
}
/// Pops the last element from a source list and pushes it to a destination list, blocking until
......@@ -141,7 +170,12 @@ extension RedisClient {
to dest: String,
timeout: Int = 0
) -> EventLoopFuture<RESPValue?> {
return send(command: "BRPOPLPUSH", with: [source, dest, timeout])
let args: [RESPValue] = [
.init(bulk: source),
.init(bulk: dest),
.init(bulk: timeout)
]
return send(command: "BRPOPLPUSH", with: args)
.map { $0.isNull ? nil: $0 }
}
}
......@@ -158,9 +192,11 @@ extension RedisClient {
/// - 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<T>(_ element: T, into key: String, before pivot: T) -> EventLoopFuture<Int>
where T: RESPValueConvertible
{
public func linsert<Value: RESPValueConvertible>(
_ element: Value,
into key: String,
before pivot: Value
) -> EventLoopFuture<Int> {
return _linsert(pivotKeyword: "BEFORE", element, key, pivot)
}
......@@ -173,20 +209,28 @@ extension RedisClient {
/// - 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<T>(_ element: T, into key: String, after pivot: T) -> EventLoopFuture<Int>
where T: RESPValueConvertible
{
public func linsert<Value: RESPValueConvertible>(
_ element: Value,
into key: String,
after pivot: Value
) -> EventLoopFuture<Int> {
return _linsert(pivotKeyword: "AFTER", element, key, pivot)
}
@usableFromInline
func _linsert(
func _linsert<Value: RESPValueConvertible>(
pivotKeyword: String,
_ element: RESPValueConvertible,
_ element: Value,
_ key: String,
_ pivot: RESPValueConvertible
_ pivot: Value
) -> EventLoopFuture<Int> {
return send(command: "LINSERT", with: [key, pivotKeyword, pivot, element])
let args: [RESPValue] = [
.init(bulk: key),
.init(bulk: pivotKeyword),
pivot.convertedToRESPValue(),
element.convertedToRESPValue()
]
return send(command: "LINSERT", with: args)
.convertFromRESPValue()
}
}
......@@ -201,7 +245,8 @@ extension RedisClient {
/// - Returns: The element that was popped from the list, or `.null`.
@inlinable
public func lpop(from key: String) -> EventLoopFuture<RESPValue> {
return send(command: "LPOP", with: [key])
let args = [RESPValue(bulk: key)]
return send(command: "LPOP", with: args)
}
/// Pushes all of the provided elements into a list.
......@@ -213,10 +258,13 @@ extension RedisClient {
/// - key: The key of the list.
/// - Returns: The length of the list after adding the new elements.
@inlinable
public func lpush(_ elements: [RESPValueConvertible], into key: String) -> EventLoopFuture<Int> {
public func lpush<Value: RESPValueConvertible>(_ elements: [Value], into key: String) -> EventLoopFuture<Int> {
assert(elements.count > 0, "At least 1 element should be provided.")
return send(command: "LPUSH", with: [key] + elements)
var args: [RESPValue] = [.init(bulk: key)]
args.append(convertingContentsOf: elements)
return send(command: "LPUSH", with: args)
.convertFromRESPValue()
}
......@@ -229,8 +277,12 @@ extension RedisClient {
/// - key: The key of the list.
/// - Returns: The length of the list after adding the new elements.
@inlinable
public func lpushx(_ element: RESPValueConvertible, into key: String) -> EventLoopFuture<Int> {
return send(command: "LPUSHX", with: [key, element])
public func lpushx<Value: RESPValueConvertible>(_ element: Value, into key: String) -> EventLoopFuture<Int> {
let args: [RESPValue] = [
.init(bulk: key),
element.convertedToRESPValue()
]
return send(command: "LPUSHX", with: args)
.convertFromRESPValue()
}
}
......@@ -245,7 +297,8 @@ extension RedisClient {
/// - Returns: The element that was popped from the list, else `.null`.
@inlinable
public func rpop(from key: String) -> EventLoopFuture<RESPValue> {
return send(command: "RPOP", with: [key])
let args = [RESPValue(bulk: key)]
return send(command: "RPOP", with: args)
}
/// Pushes all of the provided elements into a list.
......@@ -256,10 +309,13 @@ extension RedisClient {
/// - key: The key of the list.
/// - Returns: The length of the list after adding the new elements.
@inlinable
public func rpush(_ elements: [RESPValueConvertible], into key: String) -> EventLoopFuture<Int> {
public func rpush<Value: RESPValueConvertible>(_ elements: [Value], into key: String) -> EventLoopFuture<Int> {
assert(elements.count > 0, "At least 1 element should be provided.")
return send(command: "RPUSH", with: [key] + elements)
var args: [RESPValue] = [.init(bulk: key)]
args.append(convertingContentsOf: elements)
return send(command: "RPUSH", with: args)
.convertFromRESPValue()
}
......@@ -272,8 +328,12 @@ extension RedisClient {
/// - key: The key of the list.
/// - Returns: The length of the list after adding the new elements.
@inlinable
public func rpushx(_ element: RESPValueConvertible, into key: String) -> EventLoopFuture<Int> {
return send(command: "RPUSHX", with: [key, element])
public func rpushx<Value: RESPValueConvertible>(_ element: Value, into key: String) -> EventLoopFuture<Int> {
let args: [RESPValue] = [
.init(bulk: key),
element.convertedToRESPValue()
]
return send(command: "RPUSHX", with: args)
.convertFromRESPValue()
}
}
......@@ -375,7 +435,9 @@ extension RedisClient {
_ keys: [String],
_ timeout: Int
) -> EventLoopFuture<(String, RESPValue)?> {
let args = keys as [RESPValueConvertible] + [timeout]
var args = keys.map(RESPValue.init)
args.append(.init(bulk: timeout))
return send(command: command, with: args)
.flatMapThrowing {