mirror of
https://github.com/swift-server/RediStack.git
synced 2026-06-02 07:37:33 +00:00
Add SET options
Motivation: SET has a range of options for setting expirations and conditionally setting a value. Modification: - Add another `set` function with a range of options. Options are modelled as `struct`s backed by private `enum`s to allow additional options to be added without breaking API. - Added tests Result: Options may be specified with `set`, and resolves #67
This commit is contained in:
committed by
Nathan Harris
parent
5749215e5d
commit
ddfc7b0ae0
@@ -93,6 +93,106 @@ extension RedisClient {
|
||||
|
||||
// MARK: Set
|
||||
|
||||
/// A condition which must hold true in order for a key to be set.
|
||||
///
|
||||
/// See [https://redis.io/commands/set](https://redis.io/commands/set)
|
||||
public struct RedisSetCommandCondition: Hashable {
|
||||
private enum Condition: String, Hashable {
|
||||
case keyExists = "XX"
|
||||
case keyDoesNotExist = "NX"
|
||||
}
|
||||
|
||||
private let condition: Condition?
|
||||
private init(_ condition: Condition?) {
|
||||
self.condition = condition
|
||||
}
|
||||
|
||||
/// The `RESPValue` representation of the condition.
|
||||
@usableFromInline
|
||||
internal var commandArgument: RESPValue? {
|
||||
return self.condition.map { RESPValue(from: $0.rawValue) }
|
||||
}
|
||||
}
|
||||
|
||||
extension RedisSetCommandCondition {
|
||||
/// No condition is required to be met in order to set the key's value.
|
||||
public static let none = RedisSetCommandCondition(.none)
|
||||
|
||||
/// Only set the key if it already exists.
|
||||
///
|
||||
/// Redis documentation refers to this as the option "XX".
|
||||
public static let keyExists = RedisSetCommandCondition(.keyExists)
|
||||
|
||||
/// Only set the key if it does not already exist.
|
||||
///
|
||||
/// Redis documentation refers to this as the option "NX".
|
||||
public static let keyDoesNotExist = RedisSetCommandCondition(.keyDoesNotExist)
|
||||
}
|
||||
|
||||
/// The expiration to apply when setting a key.
|
||||
///
|
||||
/// See [https://redis.io/commands/set](https://redis.io/commands/set)
|
||||
public struct RedisSetCommandExpiration: Hashable {
|
||||
private enum Expiration: Hashable {
|
||||
case keepExisting
|
||||
case seconds(Int)
|
||||
case milliseconds(Int)
|
||||
}
|
||||
|
||||
private let expiration: Expiration
|
||||
private init(_ expiration: Expiration) {
|
||||
self.expiration = expiration
|
||||
}
|
||||
|
||||
/// An array of `RESPValue`s representing this expiration.
|
||||
@usableFromInline
|
||||
internal func asCommandArguments() -> [RESPValue] {
|
||||
switch self.expiration {
|
||||
case .keepExisting:
|
||||
return [RESPValue(from: "KEEPTTL")]
|
||||
case .seconds(let amount):
|
||||
return [RESPValue(from: "EX"), amount.convertedToRESPValue()]
|
||||
case .milliseconds(let amount):
|
||||
return [RESPValue(from: "PX"), amount.convertedToRESPValue()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RedisSetCommandExpiration {
|
||||
/// Retain the existing expiration associated with the key, if one exists.
|
||||
///
|
||||
/// Redis documentation refers to this as "KEEPTTL".
|
||||
/// - Important: This is option is only available in Redis 6.0+. An error will be returned if this value is sent in lower versions of Redis.
|
||||
public static let keepExisting = RedisSetCommandExpiration(.keepExisting)
|
||||
|
||||
/// Expire the key after the given number of seconds.
|
||||
///
|
||||
/// Redis documentation refers to this as the option "EX".
|
||||
/// - Important: The actual amount used will be the specified value or `1`, whichever is larger.
|
||||
public static func seconds(_ amount: Int) -> RedisSetCommandExpiration {
|
||||
return RedisSetCommandExpiration(.seconds(max(amount, 1)))
|
||||
}
|
||||
|
||||
/// Expire the key after the given number of milliseconds.
|
||||
///
|
||||
/// Redis documentation refers to this as the option "PX".
|
||||
/// - Important: The actual amount used will be the specified value or `1`, whichever is larger.
|
||||
public static func milliseconds(_ amount: Int) -> RedisSetCommandExpiration {
|
||||
return RedisSetCommandExpiration(.milliseconds(max(amount, 1)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a `SET` command.
|
||||
public enum RedisSetCommandResult: Hashable {
|
||||
/// The command completed successfully.
|
||||
case ok
|
||||
|
||||
/// The command was not performed because a condition was not met.
|
||||
///
|
||||
/// See `RedisSetCommandCondition`.
|
||||
case conditionNotMet
|
||||
}
|
||||
|
||||
extension RedisClient {
|
||||
/// Append a value to the end of an existing entry.
|
||||
/// - Note: If the key does not exist, it is created and set as an empty string, so `APPEND` will be similar to `SET` in this special case.
|
||||
@@ -133,6 +233,45 @@ extension RedisClient {
|
||||
.map { _ in () }
|
||||
}
|
||||
|
||||
/// Sets the key to the provided value with options to control how it is set.
|
||||
///
|
||||
/// [https://redis.io/commands/set](https://redis.io/commands/set)
|
||||
/// - Important: Regardless of the type of data stored at the key, it will be overwritten to a "string" data type.
|
||||
///
|
||||
/// ie. If the key is a reference to a Sorted Set, its value will be overwritten to be a "string" data type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key to use to uniquely identify this value.
|
||||
/// - value: The value to set the key to.
|
||||
/// - condition: The condition under which the key should be set.
|
||||
/// - expiration: The expiration to use when setting the key. No expiration is set if `nil`.
|
||||
/// - Returns: A `NIO.EventLoopFuture` indicating the result of the operation;
|
||||
/// `.ok` if the operation was successful and `.conditionNotMet` if the specified `condition` was not met.
|
||||
///
|
||||
/// If the condition `.none` was used, then the result value will always be `.ok`.
|
||||
public func set<Value: RESPValueConvertible>(
|
||||
_ key: RedisKey,
|
||||
to value: Value,
|
||||
onCondition condition: RedisSetCommandCondition,
|
||||
expiration: RedisSetCommandExpiration? = nil
|
||||
) -> EventLoopFuture<RedisSetCommandResult> {
|
||||
var args: [RESPValue] = [
|
||||
.init(from: key),
|
||||
value.convertedToRESPValue()
|
||||
]
|
||||
|
||||
if let conditionArgument = condition.commandArgument {
|
||||
args.append(conditionArgument)
|
||||
}
|
||||
|
||||
if let expiration = expiration {
|
||||
args.append(contentsOf: expiration.asCommandArguments())
|
||||
}
|
||||
|
||||
return self.send(command: "SET", with: args)
|
||||
.map { return $0.isNull ? .conditionNotMet : .ok }
|
||||
}
|
||||
|
||||
/// Sets the key to the provided value if the key does not exist.
|
||||
///
|
||||
/// [https://redis.io/commands/setnx](https://redis.io/commands/setnx)
|
||||
|
||||
@@ -52,6 +52,70 @@ final class StringCommandsTests: RediStackIntegrationTestCase {
|
||||
XCTAssertEqual(val, "value")
|
||||
}
|
||||
|
||||
func test_set_condition() throws {
|
||||
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyExists).wait(), .conditionNotMet)
|
||||
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyDoesNotExist).wait(), .ok)
|
||||
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyDoesNotExist).wait(), .conditionNotMet)
|
||||
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .keyExists).wait(), .ok)
|
||||
XCTAssertEqual(try connection.set(#function, to: "value", onCondition: .none).wait(), .ok)
|
||||
}
|
||||
|
||||
func test_set_expiration() throws {
|
||||
let expireInSecondsKey = RedisKey("\(#function)-seconds")
|
||||
let expireInSecondsResult = connection.set(
|
||||
expireInSecondsKey,
|
||||
to: "value",
|
||||
onCondition: .none,
|
||||
expiration: .seconds(42)
|
||||
)
|
||||
XCTAssertEqual(try expireInSecondsResult.wait(), .ok)
|
||||
|
||||
let ttl = try connection.ttl(expireInSecondsKey).wait()
|
||||
switch ttl {
|
||||
case .keyDoesNotExist, .unlimited:
|
||||
XCTFail("Unexpected TTL for key \(expireInSecondsKey)")
|
||||
case .limited(let lifetime):
|
||||
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
|
||||
XCTAssertLessThanOrEqual(lifetime.timeAmount, .seconds(42))
|
||||
}
|
||||
|
||||
let expireInMillisecondsKey = RedisKey("\(#function)-milliseconds")
|
||||
let expireInMillisecondsResult = connection.set(
|
||||
expireInMillisecondsKey,
|
||||
to: "value",
|
||||
onCondition: .none,
|
||||
expiration: .milliseconds(42_000)
|
||||
)
|
||||
|
||||
XCTAssertEqual(try expireInMillisecondsResult.wait(), .ok)
|
||||
|
||||
let pttl = try connection.ttl(expireInMillisecondsKey).wait()
|
||||
switch pttl {
|
||||
case .keyDoesNotExist, .unlimited:
|
||||
XCTFail("Unexpected TTL for key \(expireInMillisecondsKey)")
|
||||
case .limited(let lifetime):
|
||||
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
|
||||
XCTAssertLessThanOrEqual(lifetime.timeAmount, .milliseconds(42_000))
|
||||
}
|
||||
}
|
||||
|
||||
func test_set_condition_and_expiration() throws {
|
||||
let setFailedResult = connection.set(#function, to: "value", onCondition: .keyExists, expiration: .seconds(42))
|
||||
XCTAssertEqual(try setFailedResult.wait(), .conditionNotMet)
|
||||
|
||||
let setResult = connection.set(#function, to: "value", onCondition: .keyDoesNotExist, expiration: .seconds(42))
|
||||
XCTAssertEqual(try setResult.wait(), .ok)
|
||||
|
||||
let ttl = try connection.ttl(#function).wait()
|
||||
switch ttl {
|
||||
case .keyDoesNotExist, .unlimited:
|
||||
XCTFail("Unexpected TTL for key \(#function)")
|
||||
case .limited(let lifetime):
|
||||
XCTAssertGreaterThan(lifetime.timeAmount, .nanoseconds(0))
|
||||
XCTAssertLessThanOrEqual(lifetime.timeAmount, .seconds(42))
|
||||
}
|
||||
}
|
||||
|
||||
func test_setnx() throws {
|
||||
XCTAssertTrue(try connection.setnx(#function, to: "value").wait())
|
||||
XCTAssertFalse(try connection.setnx(#function, to: "value").wait())
|
||||
|
||||
@@ -157,6 +157,9 @@ extension StringCommandsTests {
|
||||
("test_mget", test_mget),
|
||||
("test_mset", test_mset),
|
||||
("test_msetnx", test_msetnx),
|
||||
("test_set_condition_and_expiration", test_set_condition_and_expiration),
|
||||
("test_set_condition", test_set_condition),
|
||||
("test_set_expiration", test_set_expiration),
|
||||
("test_set", test_set),
|
||||
("test_setnx", test_setnx),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user