From f4e646c1aaf75c41bc274bfada011bfe4fbd6a88 Mon Sep 17 00:00:00 2001 From: Nathan Harris Date: Sat, 23 Mar 2019 23:28:41 -0700 Subject: [PATCH 1/2] Audit convenience commands Motivation: Performance, clarity, and uniformity with code documentation, Swift API, and data types. --- Sources/NIORedis/Commands/BasicCommands.swift | 197 +++------- Sources/NIORedis/Commands/HashCommands.swift | 318 +++++++++------ Sources/NIORedis/Commands/ListCommands.swift | 179 ++++++--- Sources/NIORedis/Commands/SetCommands.swift | 270 ++++++++----- .../NIORedis/Commands/SortedSetCommands.swift | 372 ++++++++++-------- .../NIORedis/Commands/StringCommands.swift | 162 ++++++++ 6 files changed, 919 insertions(+), 579 deletions(-) create mode 100644 Sources/NIORedis/Commands/StringCommands.swift 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]) - .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) + /// - 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 } } - - @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,197 +1,273 @@ 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 () } - } +// 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 { - 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 { - 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 { - assert(fields.count > 0, "At least 1 field should be specified") + 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 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 { + public func hexists(_ field: String, in key: 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. + /// 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 { 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 { + public func hstrlen(of field: String, in key: String) -> EventLoopFuture { 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 { - 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. + /// 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 { + 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 { + 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 { + assert(fields.count > 0, "At least 1 key-value pair should be specified") - return result + 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.") - - return send(command: "SADD", with: [key] + items) + /// 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] + 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.") - - return send(command: "SREM", with: [key] + items) + /// 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] + 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.") - - return send(command: "SRANDMEMBER", with: [key, count.description]) + /// 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]) + .mapFromRESP() } - /// Returns the members of the set resulting from the difference between the first set and all the successive sets. + /// Moves an element from one set to another. /// - /// [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff) - public func sdiff(_ keys: String...) -> EventLoopFuture<[RESPValue]> { - return send(command: "SDIFF", with: keys) + /// 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) } + + return send(command: "SMOVE", with: [sourceKey, destKey, element]) .mapFromRESP() + .map { return $0 == 1 } } - /// Functionally equivalent to `sdiff`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. + /// Incrementally iterates over all values in a set. /// - /// [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/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) } +} - /// Returns the members of the set resulting from the intersection of all the given sets. +// MARK: Diff + +extension RedisCommandExecutor { + /// Calculates the difference between two or more sets. /// - /// [https://redis.io/commands/sinter](https://redis.io/commands/sinter) - public func sinter(_ keys: String...) -> EventLoopFuture<[RESPValue]> { - return send(command: "SINTER", with: keys) + /// 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() } - /// Functionally equivalent to `sinter`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. + /// Calculates the difference between two or more sets and stores the result. + /// - Important: If the destination key already exists, it is overwritten. /// - /// [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) + /// 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() } +} - /// Moves the `item` from the source key to the destination key. +// MARK: Intersect + +extension RedisCommandExecutor { + /// Calculates the intersection of two or more sets. /// - /// [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]) + /// 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() - .map { return $0 == 1 } } - /// Returns the members of the set resulting from the union of all the given keys. + /// Calculates the intersetion of two or more sets and stores the result. + /// - Important: If the destination key already exists, it is overwritten. /// - /// [https://redis.io/commands/sunion](https://redis.io/commands/sunion) - public func sunion(_ keys: String...) -> EventLoopFuture<[RESPValue]> { - return send(command: "SUNION", with: keys) + /// 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() } +} - /// Functionally equivalent to `sunion`, but instead stores the resulting set at the `destination` key - /// and returns the count of elements in the result set. - /// - /// [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) +// 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() } - /// Incrementally iterates over all values in a 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/sscan](https://redis.io/commands/sscan) + /// See [https://redis.io/commands/sunionstore](https://redis.io/commands/sunionstore) /// - 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) + /// - 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() } } 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() + } +} From 16dc4ac1bfc5df92207684cff699022901ced1c6 Mon Sep 17 00:00:00 2001 From: Nathan Harris Date: Sun, 24 Mar 2019 23:46:47 -0700 Subject: [PATCH 2/2] Updated unit tests to use new syntax and to remove a bunch of optional crud --- .../Commands/BasicCommandsTests.swift | 177 +++------------ .../Commands/HashCommandsTests.swift | 78 +++---- .../Commands/ListCommandsTests.swift | 96 +++++---- .../Commands/SetCommandsTests.swift | 201 ++++++++---------- .../Commands/SortedSetCommandsTests.swift | 66 +++--- .../Commands/StringCommandsTests.swift | 137 ++++++++++++ Tests/NIORedisTests/XCTestManifests.swift | 3 +- 7 files changed, 393 insertions(+), 365 deletions(-) create mode 100644 Tests/NIORedisTests/Commands/StringCommandsTests.swift diff --git a/Tests/NIORedisTests/Commands/BasicCommandsTests.swift b/Tests/NIORedisTests/Commands/BasicCommandsTests.swift index e8fb694..295bd14 100644 --- a/Tests/NIORedisTests/Commands/BasicCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/BasicCommandsTests.swift @@ -5,7 +5,7 @@ final class BasicCommandsTests: XCTestCase { private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1)) deinit { try? redis.terminate() } - private var connection: RedisConnection? + private var connection: RedisConnection! override func setUp() { do { @@ -16,200 +16,101 @@ final class BasicCommandsTests: XCTestCase { } override func tearDown() { - _ = try? connection?.send(command: "FLUSHALL").wait() - connection?.close() + _ = try? connection.send(command: "FLUSHALL").wait() + connection.close() connection = nil } func test_select() { - XCTAssertNoThrow(try connection?.select(database: 3).wait()) + XCTAssertNoThrow(try connection.select(database: 3).wait()) } func test_delete() throws { let keys = [ #function + "1", #function + "2", #function + "3" ] - try connection?.set(keys[0], to: "value").wait() - try connection?.set(keys[1], to: "value").wait() - try connection?.set(keys[2], to: "value").wait() + try connection.set(keys[0], to: "value").wait() + try connection.set(keys[1], to: "value").wait() + try connection.set(keys[2], to: "value").wait() - let first = try connection?.delete(keys[0]).wait() + let first = try connection.delete([keys[0]]).wait() XCTAssertEqual(first, 1) - let second = try connection?.delete(keys[0]).wait() + let second = try connection.delete([keys[0]]).wait() XCTAssertEqual(second, 0) - let third = try connection?.delete(keys[1], keys[2]).wait() + let third = try connection.delete([keys[1], keys[2]]).wait() XCTAssertEqual(third, 2) } - func test_set() { - XCTAssertNoThrow(try connection?.set(#function, to: "value").wait()) - } - - func test_get() throws { - try connection?.set(#function, to: "value").wait() - let result = try connection?.get(#function).wait() - XCTAssertEqual(result, "value") - } - func test_expire() throws { - try connection?.set(#function, to: "value").wait() - let before = try connection?.get(#function).wait() + try connection.set(#function, to: "value").wait() + let before = try connection.get(#function).wait() XCTAssertNotNil(before) - let result = try connection?.expire(#function, after: 0).wait() + let result = try connection.expire(#function, after: .nanoseconds(1)).wait() XCTAssertEqual(result, true) - let after = try connection?.get(#function).wait() + let after = try connection.get(#function).wait() XCTAssertNil(after) } func test_ping() throws { - let first = try connection?.ping().wait() + let first = try connection.ping().wait() XCTAssertEqual(first, "PONG") - let second = try connection?.ping(with: "My message").wait() + let second = try connection.ping(with: "My message").wait() XCTAssertEqual(second, "My message") } func test_echo() throws { - let response = try connection?.echo("FIZZ_BUZZ").wait() + let response = try connection.echo("FIZZ_BUZZ").wait() XCTAssertEqual(response, "FIZZ_BUZZ") } - func test_swapdb() throws { - try connection?.set("first", to: "3").wait() - var first = try connection?.get("first").wait() + func test_swapDatabase() throws { + try connection.set("first", to: "3").wait() + var first = try connection.get("first").wait() XCTAssertEqual(first, "3") - try connection?.select(database: 1).wait() - var second = try connection?.get("first").wait() + try connection.select(database: 1).wait() + var second = try connection.get("first").wait() XCTAssertEqual(second, nil) - try connection?.set("second", to: "100").wait() - second = try connection?.get("second").wait() + try connection.set("second", to: "100").wait() + second = try connection.get("second").wait() XCTAssertEqual(second, "100") - let success = try connection?.swapdb(firstIndex: 0, secondIndex: 1).wait() + let success = try connection.swapDatabase(0, with: 1).wait() XCTAssertEqual(success, true) - second = try connection?.get("first").wait() + second = try connection.get("first").wait() XCTAssertEqual(second, "3") - try connection?.select(database: 0).wait() - first = try connection?.get("second").wait() + try connection.select(database: 0).wait() + first = try connection.get("second").wait() XCTAssertEqual(first, "100") } - func test_increment() throws { - var result = try connection?.increment(#function).wait() - XCTAssertEqual(result, 1) - result = try connection?.increment(#function).wait() - XCTAssertEqual(result, 2) - } - - func test_incrementBy() throws { - var result = try connection?.increment(#function, by: 10).wait() - XCTAssertEqual(result, 10) - result = try connection?.increment(#function, by: -3).wait() - XCTAssertEqual(result, 7) - result = try connection?.increment(#function, by: 0).wait() - XCTAssertEqual(result, 7) - } - - func test_incrementByFloat() throws { - var float = try connection?.increment(#function, by: Float(3.0)).wait() - XCTAssertEqual(float, 3.0) - float = try connection?.increment(#function, by: Float(-10.135901)).wait() - XCTAssertEqual(float, -7.135901) - - var double = try connection?.increment(#function, by: Double(10.2839)).wait() - XCTAssertEqual(double, 3.147999) - double = try connection?.increment(#function, by: Double(15.2938)).wait() - XCTAssertEqual(double, 18.441799) - } - - func test_decrement() throws { - var result = try connection?.decrement(#function).wait() - XCTAssertEqual(result, -1) - result = try connection?.decrement(#function).wait() - XCTAssertEqual(result, -2) - } - - func test_decrementBy() throws { - var result = try connection?.decrement(#function, by: -10).wait() - XCTAssertEqual(result, 10) - result = try connection?.decrement(#function, by: 3).wait() - XCTAssertEqual(result, 7) - result = try connection?.decrement(#function, by: 0).wait() - XCTAssertEqual(result, 7) - } - - func test_mget() throws { - let keys = ["one", "two"] - try keys.forEach { _ = try connection?.set($0, to: $0).wait() } - - let values = try connection?.mget(keys + ["empty"]).wait() - XCTAssertEqual(values?.count, 3) - XCTAssertEqual(values?[0].string, "one") - XCTAssertEqual(values?[1].string, "two") - XCTAssertEqual(values?[2].isNull, true) - - XCTAssertEqual(try connection?.mget(["empty", #function]).wait().count, 2) - } - - func test_mset() throws { - let data = [ - "first": 1, - "second": 2 - ] - XCTAssertNoThrow(try connection?.mset(data).wait()) - let values = try connection?.mget(["first", "second"]).wait().compactMap { $0.string } - XCTAssertEqual(values?.count, 2) - XCTAssertEqual(values?[0], "1") - XCTAssertEqual(values?[1], "2") - - XCTAssertNoThrow(try connection?.mset(["first": 10]).wait()) - let val = try connection?.get("first").wait() - XCTAssertEqual(val, "10") - } - - func test_msetnx() throws { - let data = [ - "first": 1, - "second": 2 - ] - var success = try connection?.msetnx(data).wait() - XCTAssertEqual(success, true) - - success = try connection?.msetnx(["first": 10, "second": 20]).wait() - XCTAssertEqual(success, false) - - let values = try connection?.mget(["first", "second"]).wait().compactMap { $0.string } - XCTAssertEqual(values?[0], "1") - XCTAssertEqual(values?[1], "2") - } - func test_scan() throws { var dataset: [String] = .init(repeating: "", count: 10) for index in 1...15 { let key = "key\(index)\(index % 2 == 0 ? "_even" : "_odd")" dataset.append(key) - _ = try connection?.set(key, to: "\(index)").wait() + _ = try connection.set(key, to: "\(index)").wait() } - var (cursor, keys) = try connection?.scan(count: 5).wait() ?? (0, []) + var (cursor, keys) = try connection.scan(count: 5).wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(keys.count, 5) - (_, keys) = try connection?.scan(startingFrom: cursor, count: 8).wait() ?? (0, []) + (_, keys) = try connection.scan(startingFrom: cursor, count: 8).wait() XCTAssertGreaterThanOrEqual(keys.count, 8) - (cursor, keys) = try connection?.scan(matching: "*_odd").wait() ?? (0, []) + (cursor, keys) = try connection.scan(matching: "*_odd").wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(keys.count, 1) XCTAssertLessThanOrEqual(keys.count, 7) - (cursor, keys) = try connection?.scan(matching: "*_even*").wait() ?? (0, []) + (cursor, keys) = try connection.scan(matching: "*_even*").wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(keys.count, 1) XCTAssertLessThanOrEqual(keys.count, 7) @@ -217,21 +118,11 @@ final class BasicCommandsTests: XCTestCase { static var allTests = [ ("test_select", test_select), - ("test_set", test_set), - ("test_get", test_get), ("test_expire", test_expire), ("test_delete", test_delete), ("test_ping", test_ping), ("test_echo", test_echo), - ("test_swapdb", test_swapdb), - ("test_increment", test_increment), - ("test_incrementBy", test_incrementBy), - ("test_incrementByFloat", test_incrementByFloat), - ("test_decrement", test_decrement), - ("test_decrementBy", test_decrementBy), - ("test_mget", test_mget), - ("test_mset", test_mset), - ("test_msetnx", test_msetnx), + ("test_swapDatabase", test_swapDatabase), ("test_scan", test_scan), ] } diff --git a/Tests/NIORedisTests/Commands/HashCommandsTests.swift b/Tests/NIORedisTests/Commands/HashCommandsTests.swift index 3dd2255..9239267 100644 --- a/Tests/NIORedisTests/Commands/HashCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/HashCommandsTests.swift @@ -22,37 +22,37 @@ final class HashCommandsTests: XCTestCase { } func test_hset() throws { - var result = try connection.hset(#function, field: "test", to: "\(#line)").wait() + var result = try connection.hset("test", to: "\(#line)", in: #function).wait() XCTAssertTrue(result) - result = try connection.hset(#function, field: "test", to: "\(#line)").wait() + result = try connection.hset("test", to: "\(#line)", in: #function).wait() XCTAssertFalse(result) } func test_hmset() throws { - XCTAssertNoThrow(try connection.hmset(#function, to: ["field": "30"]).wait()) - let value = try connection.hget(#function, field: "field").wait() + XCTAssertNoThrow(try connection.hmset(["field": 30], in: #function).wait()) + let value = try connection.hget("field", from: #function).wait() XCTAssertEqual(value, "30") } func test_hsetnx() throws { - var success = try connection.hsetnx(#function, field: "field", to: "foo").wait() + var success = try connection.hsetnx("field", to: "foo", in: #function).wait() XCTAssertTrue(success) - success = try connection.hsetnx(#function, field: "field", to: "30").wait() + success = try connection.hsetnx("field", to: 30, in: #function).wait() XCTAssertFalse(success) - let value = try connection.hget(#function, field: "field").wait() + let value = try connection.hget("field", from: #function).wait() XCTAssertEqual(value, "foo") } func test_hget() throws { - _ = try connection.hset(#function, field: "test", to: "30").wait() - let value = try connection.hget(#function, field: "test").wait() + _ = try connection.hset("test", to: 30, in: #function).wait() + let value = try connection.hget("test", from: #function).wait() XCTAssertEqual(value, "30") } func test_hmget() throws { - _ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait() - let values = try connection.hmget(#function, fields: ["first", "second", "fake"]).wait() + _ = try connection.hmset(["first": "foo", "second": "bar"], in: #function).wait() + let values = try connection.hmget(["first", "second", "fake"], from: #function).wait() XCTAssertEqual(values[0], "foo") XCTAssertEqual(values[1], "bar") XCTAssertNil(values[2]) @@ -60,53 +60,54 @@ final class HashCommandsTests: XCTestCase { func test_hgetall() throws { let dataset = ["first": "foo", "second": "bar"] - _ = try connection.hmset(#function, to: dataset).wait() + _ = try connection.hmset(dataset, in: #function).wait() let hashes = try connection.hgetall(from: #function).wait() XCTAssertEqual(hashes, dataset) } func test_hdel() throws { - _ = try connection.hmset(#function, to: ["first": "foo", "second": "bar"]).wait() - let count = try connection.hdel(#function, fields: ["first", "second", "fake"]).wait() + _ = try connection.hmset(["first": "foo", "second": "bar"], in: #function).wait() + let count = try connection.hdel(["first", "second", "fake"], from: #function).wait() XCTAssertEqual(count, 2) } func test_hexists() throws { - var exists = try connection.hexists(#function, field: "foo").wait() + var exists = try connection.hexists("foo", in: #function).wait() XCTAssertFalse(exists) - _ = try connection.hset(#function, field: "foo", to: "\(#line)").wait() - exists = try connection.hexists(#function, field: "foo").wait() + _ = try connection.hset("foo", to: "\(#line)", in: #function).wait() + exists = try connection.hexists("foo", in: #function).wait() XCTAssertTrue(exists) } func test_hlen() throws { var count = try connection.hlen(of: #function).wait() XCTAssertEqual(count, 0) - _ = try connection.hset(#function, field: "first", to: "\(#line)").wait() + _ = try connection.hset("first", to: "\(#line)", in: #function).wait() count = try connection.hlen(of: #function).wait() XCTAssertEqual(count, 1) - _ = try connection.hset(#function, field: "second", to: "\(#line)").wait() + _ = try connection.hset("second", to: "\(#line)", in: #function).wait() count = try connection.hlen(of: #function).wait() XCTAssertEqual(count, 2) } func test_hstrlen() throws { - _ = try connection.hset(#function, field: "first", to: "foo").wait() - var size = try connection.hstrlen(of: #function, field: "first").wait() + _ = try connection.hset("first", to: "foo", in: #function).wait() + var size = try connection.hstrlen(of: "first", in: #function).wait() XCTAssertEqual(size, 3) - _ = try connection.hset(#function, field: "second", to: "300").wait() - size = try connection.hstrlen(of: #function, field: "second").wait() + _ = try connection.hset("second", to: 300, in: #function).wait() + size = try connection.hstrlen(of: "second", in: #function).wait() XCTAssertEqual(size, 3) } func test_hkeys() throws { - let dataset = [ + let dataset: [String: String] = [ "first": "3", "second": "foo" ] - _ = try connection.hmset(#function, to: dataset).wait() - let keys = try connection.hkeys(storedAt: #function).wait() - XCTAssertEqual(Array(dataset.keys), keys) + _ = try connection.hmset(dataset, in: #function).wait() + let keys = try connection.hkeys(in: #function).wait() + XCTAssertEqual(keys.count, 2) + XCTAssertTrue(keys.allSatisfy(dataset.keys.contains)) } func test_hvals() throws { @@ -114,26 +115,27 @@ final class HashCommandsTests: XCTestCase { "first": "3", "second": "foo" ] - _ = try connection.hmset(#function, to: dataset).wait() - let values = try connection.hvals(storedAt: #function).wait() - XCTAssertEqual(Array(dataset.values), values) + _ = try connection.hmset(dataset, in: #function).wait() + let values = try connection.hvals(in: #function).wait().compactMap { String($0) } + XCTAssertEqual(values.count, 2) + XCTAssertTrue(values.allSatisfy(dataset.values.contains)) } func test_hincrby() throws { - _ = try connection.hset(#function, field: "first", to: "3").wait() - var value = try connection.hincrby(#function, field: "first", by: 10).wait() + _ = try connection.hset("first", to: 3, in: #function).wait() + var value = try connection.hincrby(10, field: "first", in: #function).wait() XCTAssertEqual(value, 13) - value = try connection.hincrby(#function, field: "first", by: -15).wait() + value = try connection.hincrby(-15, field: "first", in: #function).wait() XCTAssertEqual(value, -2) } func test_hincrbyfloat() throws { - _ = try connection.hset(#function, field: "first", to: "3.14").wait() + _ = try connection.hset("first", to: 3.14, in: #function).wait() - let double = try connection.hincrbyfloat(#function, field: "first", by: Double(3.14)).wait() + let double = try connection.hincrbyfloat(Double(3.14), field: "first", in: #function).wait() XCTAssertEqual(double, 6.28) - let float = try connection.hincrbyfloat(#function, field: "first", by: Float(-10.23523)).wait() + let float = try connection.hincrbyfloat(Float(-10.23523), field: "first", in: #function).wait() XCTAssertEqual(float, -3.95523) } @@ -143,13 +145,13 @@ final class HashCommandsTests: XCTestCase { let key = "key\(index)\(index % 2 == 0 ? "_even" : "_odd")" dataset[key] = "\(index)" } - _ = try connection.hmset(#function, to: dataset).wait() + _ = try connection.hmset(dataset, in: #function).wait() var (cursor, fields) = try connection.hscan(#function, count: 5).wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(fields.count, 5) - (_, fields) = try connection.hscan(#function, atPosition: cursor, count: 8).wait() + (_, fields) = try connection.hscan(#function, startingFrom: cursor, count: 8).wait() XCTAssertGreaterThanOrEqual(fields.count, 8) (cursor, fields) = try connection.hscan(#function, matching: "*_odd").wait() diff --git a/Tests/NIORedisTests/Commands/ListCommandsTests.swift b/Tests/NIORedisTests/Commands/ListCommandsTests.swift index 5ddb9fc..2b314bd 100644 --- a/Tests/NIORedisTests/Commands/ListCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/ListCommandsTests.swift @@ -24,28 +24,32 @@ final class ListCommandsTests: XCTestCase { func test_llen() throws { var length = try connection.llen(of: #function).wait() XCTAssertEqual(length, 0) - _ = try connection.lpush([30], to: #function).wait() + _ = try connection.lpush([30], into: #function).wait() length = try connection.llen(of: #function).wait() XCTAssertEqual(length, 1) } func test_lindex() throws { - XCTAssertThrowsError(try connection.lindex(#function, index: 0).wait()) - _ = try connection.lpush([10], to: #function).wait() - let element = try connection.lindex(#function, index: 0).wait() + var element = try connection.lindex(0, from: #function).wait() + XCTAssertTrue(element.isNull) + + _ = try connection.lpush([10], into: #function).wait() + + element = try connection.lindex(0, from: #function).wait() + XCTAssertFalse(element.isNull) XCTAssertEqual(Int(element), 10) } func test_lset() throws { - XCTAssertThrowsError(try connection.lset(#function, index: 0, to: 30).wait()) - _ = try connection.lpush([10], to: #function).wait() - XCTAssertNoThrow(try connection.lset(#function, index: 0, to: 30).wait()) - let element = try connection.lindex(#function, index: 0).wait() + XCTAssertThrowsError(try connection.lset(index: 0, to: 30, in: #function).wait()) + _ = try connection.lpush([10], into: #function).wait() + XCTAssertNoThrow(try connection.lset(index: 0, to: 30, in: #function).wait()) + let element = try connection.lindex(0, from: #function).wait() XCTAssertEqual(Int(element), 30) } func test_lrem() throws { - _ = try connection.lpush([10, 10, 20, 30, 10], to: #function).wait() + _ = try connection.lpush([10, 10, 20, 30, 10], into: #function).wait() var count = try connection.lrem(10, from: #function, count: 2).wait() XCTAssertEqual(count, 2) count = try connection.lrem(10, from: #function, count: 2).wait() @@ -53,29 +57,29 @@ final class ListCommandsTests: XCTestCase { } func test_lrange() throws { - var elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 10).wait() + var elements = try connection.lrange(within: (0, 10), from: #function).wait() XCTAssertEqual(elements.count, 0) - _ = try connection.lpush([5, 4, 3, 2, 1], to: #function).wait() + _ = try connection.lpush([5, 4, 3, 2, 1], into: #function).wait() - elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 4).wait() + elements = try connection.lrange(within: (0, 4), from: #function).wait() XCTAssertEqual(elements.count, 5) XCTAssertEqual(Int(elements[0]), 1) XCTAssertEqual(Int(elements[4]), 5) - elements = try connection.lrange(of: #function, startIndex: 2, endIndex: 0).wait() + elements = try connection.lrange(within: (2, 0), from: #function).wait() XCTAssertEqual(elements.count, 0) - elements = try connection.lrange(of: #function, startIndex: 4, endIndex: 5).wait() + elements = try connection.lrange(within: (4, 5), from: #function).wait() XCTAssertEqual(elements.count, 1) - elements = try connection.lrange(of: #function, startIndex: 0, endIndex: -4).wait() + elements = try connection.lrange(within: (0, -4), from: #function).wait() XCTAssertEqual(elements.count, 2) } func test_rpoplpush() throws { - _ = try connection.lpush([10], to: "first").wait() - _ = try connection.lpush([30], to: "second").wait() + _ = try connection.lpush([10], into: "first").wait() + _ = try connection.lpush([30], into: "second").wait() var element = try connection.rpoplpush(from: "first", to: "second").wait() XCTAssertEqual(Int(element), 10) @@ -88,79 +92,87 @@ final class ListCommandsTests: XCTestCase { } func test_linsert() throws { - _ = try connection.lpush([10], to: #function).wait() + _ = try connection.lpush([10], into: #function).wait() _ = try connection.linsert(20, into: #function, after: 10).wait() - var elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 1) + var elements = try connection.lrange(within: (0, 1), from: #function) .map { response in response.compactMap { Int($0) } } .wait() XCTAssertEqual(elements, [10, 20]) _ = try connection.linsert(30, into: #function, before: 10).wait() - elements = try connection.lrange(of: #function, startIndex: 0, endIndex: 2) + elements = try connection.lrange(within: (0, 2), from: #function) .map { response in response.compactMap { Int($0) } } .wait() XCTAssertEqual(elements, [30, 10, 20]) } func test_lpop() throws { - _ = try connection.lpush([10, 20, 30], to: #function).wait() + var element = try connection.lpop(from: #function).wait() + XCTAssertTrue(element.isNull) - let element = try connection.lpop(from: #function).wait() - XCTAssertNotNil(element) - XCTAssertEqual(Int(element ?? .null), 30) + _ = try connection.lpush([10, 20, 30], into: #function).wait() + + element = try connection.lpop(from: #function).wait() + XCTAssertFalse(element.isNull) + XCTAssertEqual(Int(element), 30) } func test_lpush() throws { - _ = try connection.rpush([10, 20, 30], to: #function).wait() + _ = try connection.rpush([10, 20, 30], into: #function).wait() - let size = try connection.lpush([100], to: #function).wait() - let element = try connection.lindex(#function, index: 0).mapFromRESP(to: Int.self).wait() + let size = try connection.lpush([100], into: #function).wait() + let element = try connection.lindex(0, from: #function).wait() XCTAssertEqual(size, 4) - XCTAssertEqual(element, 100) + XCTAssertEqual(Int(element), 100) } func test_lpushx() throws { - var size = try connection.lpushx(10, to: #function).wait() + var size = try connection.lpushx(10, into: #function).wait() XCTAssertEqual(size, 0) - _ = try connection.lpush([10], to: #function).wait() + _ = try connection.lpush([10], into: #function).wait() - size = try connection.lpushx(30, to: #function).wait() + size = try connection.lpushx(30, into: #function).wait() XCTAssertEqual(size, 2) let element = try connection.rpop(from: #function) - .map { return Int($0 ?? .null) } + .map { return Int($0) } .wait() XCTAssertEqual(element, 10) } func test_rpop() throws { - _ = try connection.lpush([10, 20, 30], to: #function).wait() + _ = try connection.lpush([10, 20, 30], into: #function).wait() let element = try connection.rpop(from: #function).wait() XCTAssertNotNil(element) - XCTAssertEqual(Int(element ?? .null), 10) + XCTAssertEqual(Int(element), 10) + + _ = try connection.delete([#function]).wait() + + let result = try connection.rpop(from: #function).wait() + XCTAssertTrue(result.isNull) } func test_rpush() throws { - _ = try connection.lpush([10, 20, 30], to: #function).wait() + _ = try connection.lpush([10, 20, 30], into: #function).wait() - let size = try connection.rpush([100], to: #function).wait() - let element = try connection.lindex(#function, index: 3).mapFromRESP(to: Int.self).wait() + let size = try connection.rpush([100], into: #function).wait() + let element = try connection.lindex(3, from: #function).wait() XCTAssertEqual(size, 4) - XCTAssertEqual(element, 100) + XCTAssertEqual(Int(element), 100) } func test_rpushx() throws { - var size = try connection.rpushx(10, to: #function).wait() + var size = try connection.rpushx(10, into: #function).wait() XCTAssertEqual(size, 0) - _ = try connection.rpush([10], to: #function).wait() + _ = try connection.rpush([10], into: #function).wait() - size = try connection.rpushx(30, to: #function).wait() + size = try connection.rpushx(30, into: #function).wait() XCTAssertEqual(size, 2) let element = try connection.lpop(from: #function) - .map { return Int($0 ?? .null) } + .map { return Int($0) } .wait() XCTAssertEqual(element, 10) } diff --git a/Tests/NIORedisTests/Commands/SetCommandsTests.swift b/Tests/NIORedisTests/Commands/SetCommandsTests.swift index fa89488..1cf4319 100644 --- a/Tests/NIORedisTests/Commands/SetCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/SetCommandsTests.swift @@ -5,7 +5,7 @@ final class SetCommandsTests: XCTestCase { private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1)) deinit { try? redis.terminate() } - private var connection: RedisConnection? + private var connection: RedisConnection! override func setUp() { do { @@ -16,91 +16,79 @@ final class SetCommandsTests: XCTestCase { } override func tearDown() { - _ = try? connection?.send(command: "FLUSHALL").wait() - connection?.close() + _ = try? connection.send(command: "FLUSHALL").wait() + connection.close() connection = nil } func test_sadd() throws { - let key = #function - - var insertCount = try connection?.sadd(key, items: [1, 2, 3]).wait() + var insertCount = try connection.sadd([1, 2, 3], to: #function).wait() XCTAssertEqual(insertCount, 3) - insertCount = try connection?.sadd(key, items: [3, 4, 5]).wait() + insertCount = try connection.sadd([3, 4, 5], to: #function).wait() XCTAssertEqual(insertCount, 2) } func test_smembers() throws { - let key = #function let first = ["Hello", ","] let second = ["World", "!"] - _ = try connection?.sadd(key, items: first).wait() - var set = try connection?.smembers(key).wait() - XCTAssertEqual(set?.array?.count, 2) + _ = try connection.sadd(first, to: #function).wait() + var set = try connection.smembers(of: #function).wait() + XCTAssertEqual(set.count, 2) - _ = try connection?.sadd(key, items: first).wait() - set = try connection?.smembers(key).wait() - XCTAssertEqual(set?.array?.count, 2) + _ = try connection.sadd(first, to: #function).wait() + set = try connection.smembers(of: #function).wait() + XCTAssertEqual(set.count, 2) - _ = try connection?.sadd(key, items: second).wait() - set = try connection?.smembers(key).wait() - XCTAssertEqual(set?.array?.count, 4) + _ = try connection.sadd(second, to: #function).wait() + set = try connection.smembers(of: #function).wait() + XCTAssertEqual(set.count, 4) } func test_sismember() throws { - let key = #function - - _ = try connection?.sadd(key, items: ["Hello"]).wait() - XCTAssertTrue(try connection?.sismember(key, item: "Hello").wait() ?? false) + _ = try connection.sadd(["Hello"], to: #function).wait() + XCTAssertTrue(try connection.sismember("Hello", of: #function).wait()) - XCTAssertFalse(try connection?.sismember(key, item: 3).wait() ?? true) - - _ = try connection?.sadd(key, items: [3]).wait() - XCTAssertTrue(try connection?.sismember(key, item: 3).wait() ?? false) + XCTAssertFalse(try connection.sismember(3, of: #function).wait()) + _ = try connection.sadd([3], to: #function).wait() + XCTAssertTrue(try connection.sismember(3, of: #function).wait()) } func test_scard() throws { - let key = #function - - XCTAssertEqual(try connection?.scard(key).wait(), 0) - _ = try connection?.sadd(key, items: [1, 2, 3]).wait() - XCTAssertEqual(try connection?.scard(key).wait(), 3) + XCTAssertEqual(try connection.scard(of: #function).wait(), 0) + _ = try connection.sadd([1, 2, 3], to: #function).wait() + XCTAssertEqual(try connection.scard(of: #function).wait(), 3) } func test_srem() throws { - let key = #function - - var removedCount = try connection?.srem(key, items: [1]).wait() + var removedCount = try connection.srem([1], from: #function).wait() XCTAssertEqual(removedCount, 0) - _ = try connection?.sadd(key, items: [1]).wait() - removedCount = try connection?.srem(key, items: [1]).wait() + _ = try connection.sadd([1], to: #function).wait() + removedCount = try connection.srem([1], from: #function).wait() XCTAssertEqual(removedCount, 1) } func test_spop() throws { - let key = #function - - var count = try connection?.scard(key).wait() - var item = try connection?.spop(key).wait() + var count = try connection.scard(of: #function).wait() + var result = try connection.spop(from: #function).wait() XCTAssertEqual(count, 0) - XCTAssertEqual(item?.isNull, true) + XCTAssertEqual(result.count, 0) - _ = try connection?.sadd(key, items: ["Hello"]).wait() - item = try connection?.spop(key).wait() - XCTAssertEqual(item?.string, "Hello") - count = try connection?.scard(key).wait() + _ = try connection.sadd(["Hello"], to: #function).wait() + + result = try connection.spop(from: #function).wait() + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result[0].string, "Hello") + count = try connection.scard(of: #function).wait() XCTAssertEqual(count, 0) } func test_srandmember() throws { - let key = #function - - _ = try connection?.sadd(key, items: [1, 2, 3]).wait() - XCTAssertEqual(try connection?.srandmember(key).wait().array?.count, 1) - XCTAssertEqual(try connection?.srandmember(key, max: 4).wait().array?.count, 3) - XCTAssertEqual(try connection?.srandmember(key, max: -4).wait().array?.count, 4) + _ = try connection.sadd([1, 2, 3], to: #function).wait() + XCTAssertEqual(try connection.srandmember(from: #function).wait().count, 1) + XCTAssertEqual(try connection.srandmember(from: #function, max: 4).wait().count, 3) + XCTAssertEqual(try connection.srandmember(from: #function, max: -4).wait().count, 4) } func test_sdiff() throws { @@ -108,20 +96,20 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() - _ = try connection?.sadd(key3, items: [2, 4]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([3, 4, 5], to: key2).wait() + _ = try connection.sadd([2, 4], to: key3).wait() - let diff1 = try connection?.sdiff(key1, key2).wait() ?? [] + let diff1 = try connection.sdiff(of: [key1, key2]).wait() XCTAssertEqual(diff1.count, 2) - let diff2 = try connection?.sdiff(key1, key3).wait() ?? [] + let diff2 = try connection.sdiff(of: [key1, key3]).wait() XCTAssertEqual(diff2.count, 2) - let diff3 = try connection?.sdiff(key1, key2, key3).wait() ?? [] + let diff3 = try connection.sdiff(of: [key1, key2, key3]).wait() XCTAssertEqual(diff3.count, 1) - let diff4 = try connection?.sdiff(key3, key1, key2).wait() ?? [] + let diff4 = try connection.sdiff(of: [key3, key1, key2]).wait() XCTAssertEqual(diff4.count, 0) } @@ -130,14 +118,14 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([3, 4, 5], to: key2).wait() - let diffCount = try connection?.sdiffstore(destination: key3, key1, key2).wait() + let diffCount = try connection.sdiffstore(as: key3, sources: [key1, key2]).wait() XCTAssertEqual(diffCount, 2) - let members = try connection?.smembers(key3).wait().array - XCTAssertEqual(members?[0].string, "1") - XCTAssertEqual(members?[1].string, "2") + let members = try connection.smembers(of: key3).wait() + XCTAssertEqual(members[0].string, "1") + XCTAssertEqual(members[1].string, "2") } func test_sinter() throws { @@ -145,20 +133,20 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() - _ = try connection?.sadd(key3, items: [2, 4]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([3, 4, 5], to: key2).wait() + _ = try connection.sadd([2, 4], to: key3).wait() - let diff1 = try connection?.sinter(key1, key2).wait() ?? [] + let diff1 = try connection.sinter(of: [key1, key2]).wait() XCTAssertEqual(diff1.count, 1) - let diff2 = try connection?.sinter(key1, key3).wait() ?? [] + let diff2 = try connection.sinter(of: [key1, key3]).wait() XCTAssertEqual(diff2.count, 1) - let diff3 = try connection?.sinter(key1, key2, key3).wait() ?? [] + let diff3 = try connection.sinter(of: [key1, key2, key3]).wait() XCTAssertEqual(diff3.count, 0) - let diff4 = try connection?.sinter(key3, key1, key2).wait() ?? [] + let diff4 = try connection.sinter(of: [key3, key1, key2]).wait() XCTAssertEqual(diff4.count, 0) } @@ -167,33 +155,30 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([3, 4, 5], to: key2).wait() - let diffCount = try connection?.sinterstore(destination: key3, key1, key2).wait() + let diffCount = try connection.sinterstore(as: key3, sources: [key1, key2]).wait() XCTAssertEqual(diffCount, 1) - XCTAssertEqual(try connection?.smembers(key3).wait().array?[0].string, "3") + XCTAssertEqual(try connection.smembers(of: key3).wait()[0].string, "3") } func test_smove() throws { - let key1 = #function - let key2 = #file - - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() + _ = try connection.sadd([1, 2, 3], to: #function).wait() + _ = try connection.sadd([3, 4, 5], to: #file).wait() - var didMove = try connection?.smove(item: 3, fromKey: key1, toKey: key2).wait() - XCTAssertTrue(didMove ?? false) - XCTAssertEqual(try connection?.scard(key1).wait(), 2) - XCTAssertEqual(try connection?.scard(key2).wait(), 3) + var didMove = try connection.smove(3, from: #function, to: #file).wait() + XCTAssertTrue(didMove) + XCTAssertEqual(try connection.scard(of: #function).wait(), 2) + XCTAssertEqual(try connection.scard(of: #file).wait(), 3) - didMove = try connection?.smove(item: 2, fromKey: key1, toKey: key2).wait() - XCTAssertTrue(didMove ?? false) - XCTAssertEqual(try connection?.scard(key1).wait(), 1) - XCTAssertEqual(try connection?.scard(key2).wait(), 4) + didMove = try connection.smove(2, from: #function, to: #file).wait() + XCTAssertTrue(didMove) + XCTAssertEqual(try connection.scard(of: #function).wait(), 1) + XCTAssertEqual(try connection.scard(of: #file).wait(), 4) - didMove = try connection?.smove(item: 6, fromKey: key2, toKey: key1).wait() - XCTAssertFalse(didMove ?? false) + didMove = try connection.smove(6, from: #file, to: #function).wait() + XCTAssertFalse(didMove) } func test_sunion() throws { @@ -201,17 +186,17 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [3, 4, 5]).wait() - _ = try connection?.sadd(key3, items: [2, 4]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([3, 4, 5], to: key2).wait() + _ = try connection.sadd([2, 4], to: key3).wait() - let union1 = try connection?.sunion(key1, key2).wait() ?? [] + let union1 = try connection.sunion(of: [key1, key2]).wait() XCTAssertEqual(union1.count, 5) - let union2 = try connection?.sunion(key2, key3).wait() ?? [] + let union2 = try connection.sunion(of: [key2, key3]).wait() XCTAssertEqual(union2.count, 4) - let diff3 = try connection?.sunion(key1, key2, key3).wait() ?? [] + let diff3 = try connection.sunion(of: [key1, key2, key3]).wait() XCTAssertEqual(diff3.count, 5) } @@ -220,16 +205,16 @@ final class SetCommandsTests: XCTestCase { let key2 = #file let key3 = key1 + key2 - _ = try connection?.sadd(key1, items: [1, 2, 3]).wait() - _ = try connection?.sadd(key2, items: [2, 3, 4]).wait() + _ = try connection.sadd([1, 2, 3], to: key1).wait() + _ = try connection.sadd([2, 3, 4], to: key2).wait() - let unionCount = try connection?.sunionstore(destination: key3, key1, key2).wait() + let unionCount = try connection.sunionstore(as: key3, sources: [key1, key2]).wait() XCTAssertEqual(unionCount, 4) - let results = try connection?.smembers(key3).wait().array - XCTAssertEqual(results?[0].string, "1") - XCTAssertEqual(results?[1].string, "2") - XCTAssertEqual(results?[2].string, "3") - XCTAssertEqual(results?[3].string, "4") + let results = try connection.smembers(of: key3).wait() + XCTAssertEqual(results[0].string, "1") + XCTAssertEqual(results[1].string, "2") + XCTAssertEqual(results[2].string, "3") + XCTAssertEqual(results[3].string, "4") } func test_sscan() throws { @@ -252,21 +237,21 @@ final class SetCommandsTests: XCTestCase { "Honolulu, United States" ] - _ = try connection?.sadd(key, items: dataset).wait() + _ = try connection.sadd(dataset, to: key).wait() - var (cursor, results) = try connection?.sscan(key, count: 5).wait() ?? (0, []) + var (cursor, results) = try connection.sscan(key, count: 5).wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(results.count, 5) - (_, results) = try connection?.sscan(key, atPosition: cursor, count: 8).wait() ?? (0, []) + (_, results) = try connection.sscan(key, startingFrom: cursor, count: 8).wait() XCTAssertGreaterThanOrEqual(results.count, 8) - (cursor, results) = try connection?.sscan(key, matching: "*Denmark").wait() ?? (0, []) + (cursor, results) = try connection.sscan(key, matching: "*Denmark").wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(results.count, 1) XCTAssertLessThanOrEqual(results.count, 5) - (cursor, results) = try connection?.sscan(key, matching: "*ing*").wait() ?? (0, []) + (cursor, results) = try connection.sscan(key, matching: "*ing*").wait() XCTAssertGreaterThanOrEqual(cursor, 0) XCTAssertGreaterThanOrEqual(results.count, 1) XCTAssertLessThanOrEqual(results.count, 3) diff --git a/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift b/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift index 9dd11ca..2e6a295 100644 --- a/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift +++ b/Tests/NIORedisTests/Commands/SortedSetCommandsTests.swift @@ -64,17 +64,17 @@ final class SortedSetCommandsTests: XCTestCase { func test_zscore() throws { _ = try connection.send(command: "FLUSHALL").wait() - var score = try connection.zscore(of: 30, storedAt: #function).wait() + var score = try connection.zscore(of: 30, in: #function).wait() XCTAssertEqual(score, nil) _ = try connection.zadd((30, 1), to: #function).wait() - score = try connection.zscore(of: 30, storedAt: #function).wait() + score = try connection.zscore(of: 30, in: #function).wait() XCTAssertEqual(score, 1) - _ = try connection.zincrby(#function, member: 30, by: 10).wait() + _ = try connection.zincrby(10, element: 30, in: #function).wait() - score = try connection.zscore(of: 30, storedAt: #function).wait() + score = try connection.zscore(of: 30, in: #function).wait() XCTAssertEqual(score, 11) } @@ -99,9 +99,9 @@ final class SortedSetCommandsTests: XCTestCase { func test_zrank() throws { let futures = [ - connection.zrank(of: 1, storedAt: key), - connection.zrank(of: 2, storedAt: key), - connection.zrank(of: 3, storedAt: key), + connection.zrank(of: 1, in: key), + connection.zrank(of: 2, in: key), + connection.zrank(of: 3, in: key), ] let scores = try EventLoopFuture.whenAllSucceed(futures, on: connection.eventLoop).wait() XCTAssertEqual(scores, [0, 1, 2]) @@ -109,9 +109,9 @@ final class SortedSetCommandsTests: XCTestCase { func test_zrevrank() throws { let futures = [ - connection.zrevrank(of: 1, storedAt: key), - connection.zrevrank(of: 2, storedAt: key), - connection.zrevrank(of: 3, storedAt: key), + connection.zrevrank(of: 1, in: key), + connection.zrevrank(of: 2, in: key), + connection.zrevrank(of: 3, in: key), ] let scores = try EventLoopFuture.whenAllSucceed(futures, on: connection.eventLoop).wait() XCTAssertEqual(scores, [9, 8, 7]) @@ -135,9 +135,9 @@ final class SortedSetCommandsTests: XCTestCase { let min = try connection.zpopmin(from: key).wait() XCTAssertEqual(min?.1, 1) - _ = try connection.zpopmin(7, from: key).wait() + _ = try connection.zpopmin(from: key, max: 7).wait() - let results = try connection.zpopmin(3, from: key).wait() + let results = try connection.zpopmin(from: key, max: 3).wait() XCTAssertEqual(results.count, 2) XCTAssertEqual(results[0].1, 9) XCTAssertEqual(results[1].1, 10) @@ -147,22 +147,22 @@ final class SortedSetCommandsTests: XCTestCase { let min = try connection.zpopmax(from: key).wait() XCTAssertEqual(min?.1, 10) - _ = try connection.zpopmax(7, from: key).wait() + _ = try connection.zpopmax(from: key, max: 7).wait() - let results = try connection.zpopmax(3, from: key).wait() + let results = try connection.zpopmax(from: key, max: 3).wait() XCTAssertEqual(results.count, 2) XCTAssertEqual(results[0].1, 2) XCTAssertEqual(results[1].1, 1) } func test_zincrby() throws { - var score = try connection.zincrby(key, member: 1, by: 3_00_1398.328923).wait() + var score = try connection.zincrby(3_00_1398.328923, element: 1, in: key).wait() XCTAssertEqual(score, 3_001_399.328923) - score = try connection.zincrby(key, member: 1, by: -201_309.1397318).wait() + score = try connection.zincrby(-201_309.1397318, element: 1, in: key).wait() XCTAssertEqual(score, 2_800_090.1891912) - score = try connection.zincrby(key, member: 1, by: 20).wait() + score = try connection.zincrby(20, element: 1, in: key).wait() XCTAssertEqual(score, 2_800_110.1891912) } @@ -171,15 +171,15 @@ final class SortedSetCommandsTests: XCTestCase { _ = try connection.zadd([(3, 3), (4, 4)], to: #file).wait() let unionCount = try connection.zunionstore( - [key, #function, #file], - to: #function+#file, + as: #function+#file, + sources: [key, #function, #file], weights: [3, 2, 1], aggregateMethod: "MAX" ).wait() XCTAssertEqual(unionCount, 10) - let rank = try connection.zrank(of: 10, storedAt: #function+#file).wait() + let rank = try connection.zrank(of: 10, in: #function+#file).wait() XCTAssertEqual(rank, 9) - let score = try connection.zscore(of: 10, storedAt: #function+#file).wait() + let score = try connection.zscore(of: 10, in: #function+#file).wait() XCTAssertEqual(score, 30) } @@ -187,22 +187,22 @@ final class SortedSetCommandsTests: XCTestCase { _ = try connection.zadd([(3, 3), (10, 10), (11, 11)], to: #function).wait() let unionCount = try connection.zinterstore( - [key, #function], - to: #file, + as: #file, + sources: [key, #function], weights: [3, 2], aggregateMethod: "MIN" - ).wait() + ).wait() XCTAssertEqual(unionCount, 2) - let rank = try connection.zrank(of: 10, storedAt: #file).wait() + let rank = try connection.zrank(of: 10, in: #file).wait() XCTAssertEqual(rank, 1) - let score = try connection.zscore(of: 10, storedAt: #file).wait() + let score = try connection.zscore(of: 10, in: #file).wait() XCTAssertEqual(score, 20.0) } func test_zrange() throws { - var elements = try connection.zrange(withinIndices: (1, 3), from: key).wait() + var elements = try connection.zrange(within: (1, 3), from: key).wait() XCTAssertEqual(elements.count, 3) - elements = try connection.zrange(withinIndices: (1, 3), from: key, withScores: true).wait() + elements = try connection.zrange(within: (1, 3), from: key, withScores: true).wait() XCTAssertEqual(elements.count, 6) let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -214,9 +214,9 @@ final class SortedSetCommandsTests: XCTestCase { } func test_zrevrange() throws { - var elements = try connection.zrevrange(withinIndices: (1, 3), from: key).wait() + var elements = try connection.zrevrange(within: (1, 3), from: key).wait() XCTAssertEqual(elements.count, 3) - elements = try connection.zrevrange(withinIndices: (1, 3), from: key, withScores: true).wait() + elements = try connection.zrevrange(within: (1, 3), from: key, withScores: true).wait() XCTAssertEqual(elements.count, 6) let values = try NIORedisConnection._mapSortedSetResponse(elements, scoreIsFirst: false) @@ -313,11 +313,11 @@ final class SortedSetCommandsTests: XCTestCase { } func test_zremrangebyrank() throws { - var count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 3, from: key).wait() + var count = try connection.zremrangebyrank(within: (0, 3), from: key).wait() XCTAssertEqual(count, 4) - count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 10, from: key).wait() + count = try connection.zremrangebyrank(within: (0, 10), from: key).wait() XCTAssertEqual(count, 6) - count = try connection.zremrangebyrank(startingFrom: 0, endingAt: 3, from: key).wait() + count = try connection.zremrangebyrank(within: (0, 3), from: key).wait() XCTAssertEqual(count, 0) } diff --git a/Tests/NIORedisTests/Commands/StringCommandsTests.swift b/Tests/NIORedisTests/Commands/StringCommandsTests.swift new file mode 100644 index 0000000..b880151 --- /dev/null +++ b/Tests/NIORedisTests/Commands/StringCommandsTests.swift @@ -0,0 +1,137 @@ +@testable import NIORedis +import XCTest + +final class StringCommandsTests: XCTestCase { + private static let testKey = "SortedSetCommandsTests" + + private let redis = RedisDriver(ownershipModel: .internal(threadCount: 1)) + deinit { try? redis.terminate() } + + private var connection: RedisConnection! + + override func setUp() { + do { + connection = try redis.makeConnection().wait() + } catch { + XCTFail("Failed to create NIORedisConnection!") + } + } + + override func tearDown() { + _ = try? connection.send(command: "FLUSHALL").wait() + connection.close() + connection = nil + } + + func test_get() throws { + try connection.set(#function, to: "value").wait() + let result = try connection.get(#function).wait() + XCTAssertEqual(result, "value") + } + + func test_mget() throws { + let keys = ["one", "two"] + try keys.forEach { _ = try connection.set($0, to: $0).wait() } + + let values = try connection.mget(keys + ["empty"]).wait() + XCTAssertEqual(values.count, 3) + XCTAssertEqual(values[0].string, "one") + XCTAssertEqual(values[1].string, "two") + XCTAssertEqual(values[2].isNull, true) + + XCTAssertEqual(try connection.mget(["empty", #function]).wait().count, 2) + } + + func test_set() { + XCTAssertNoThrow(try connection.set(#function, to: "value").wait()) + } + + func test_mset() throws { + let data = [ + "first": 1, + "second": 2 + ] + XCTAssertNoThrow(try connection.mset(data).wait()) + let values = try connection.mget(["first", "second"]).wait().compactMap { $0.string } + XCTAssertEqual(values.count, 2) + XCTAssertEqual(values[0], "1") + XCTAssertEqual(values[1], "2") + + XCTAssertNoThrow(try connection.mset(["first": 10]).wait()) + let val = try connection.get("first").wait() + XCTAssertEqual(val, "10") + } + + func test_msetnx() throws { + let data = [ + "first": 1, + "second": 2 + ] + var success = try connection.msetnx(data).wait() + XCTAssertEqual(success, true) + + success = try connection.msetnx(["first": 10, "second": 20]).wait() + XCTAssertEqual(success, false) + + let values = try connection.mget(["first", "second"]).wait().compactMap { $0.string } + XCTAssertEqual(values[0], "1") + XCTAssertEqual(values[1], "2") + } + + func test_increment() throws { + var result = try connection.increment(#function).wait() + XCTAssertEqual(result, 1) + result = try connection.increment(#function).wait() + XCTAssertEqual(result, 2) + } + + func test_incrementBy() throws { + var result = try connection.increment(#function, by: 10).wait() + XCTAssertEqual(result, 10) + result = try connection.increment(#function, by: -3).wait() + XCTAssertEqual(result, 7) + result = try connection.increment(#function, by: 0).wait() + XCTAssertEqual(result, 7) + } + + func test_incrementByFloat() throws { + var float = try connection.increment(#function, by: Float(3.0)).wait() + XCTAssertEqual(float, 3.0) + float = try connection.increment(#function, by: Float(-10.135901)).wait() + XCTAssertEqual(float, -7.135901) + + var double = try connection.increment(#function, by: Double(10.2839)).wait() + XCTAssertEqual(double, 3.147999) + double = try connection.increment(#function, by: Double(15.2938)).wait() + XCTAssertEqual(double, 18.441799) + } + + func test_decrement() throws { + var result = try connection.decrement(#function).wait() + XCTAssertEqual(result, -1) + result = try connection.decrement(#function).wait() + XCTAssertEqual(result, -2) + } + + func test_decrementBy() throws { + var result = try connection.decrement(#function, by: -10).wait() + XCTAssertEqual(result, 10) + result = try connection.decrement(#function, by: 3).wait() + XCTAssertEqual(result, 7) + result = try connection.decrement(#function, by: 0).wait() + XCTAssertEqual(result, 7) + } + + static var allTests = [ + ("test_get", test_get), + ("test_mget", test_mget), + ("test_set", test_set), + ("test_mset", test_mset), + ("test_msetnx", test_msetnx), + ("test_increment", test_increment), + ("test_incrementBy", test_incrementBy), + ("test_incrementByFloat", test_incrementByFloat), + ("test_decrement", test_decrement), + ("test_decrementBy", test_decrementBy), + ] +} diff --git a/Tests/NIORedisTests/XCTestManifests.swift b/Tests/NIORedisTests/XCTestManifests.swift index a5c7748..98fcab0 100644 --- a/Tests/NIORedisTests/XCTestManifests.swift +++ b/Tests/NIORedisTests/XCTestManifests.swift @@ -14,7 +14,8 @@ public func allTests() -> [XCTestCaseEntry] { testCase(RedisPipelineTests.allTests), testCase(HashCommandsTests.allTests), testCase(ListCommandsTests.allTests), - testCase(SortedSetCommandsTests.allTests) + testCase(SortedSetCommandsTests.allTests), + testCase(StringCommandsTests.allTests) ] } #endif