Skip to content

Commit 6423299

Browse files
committed
60 -- Provide Strong Option Types in SortedSet Commands
Motivation: While working through issue #59, it was noticed just how "stringly" the SortedSet command options for `zadd`, `zinterstore`, and `zunionstore` were, and Swift provides ways of having strong type safety for these options. Modifications: - Add `RedisSortedSetAddOption` and `RedisSortedSetAggregateMethod` to replace the String API in `zadd`, `zinterstore`, and `zunionstore` - Fix an implication of how `overestimatedCountBeingAdded` documentation for `Array where Element == RESPValue` for `add(contentsOf:overestimatedCountBeingAdded:_:)` Result: Users should have a more discoverable and straightforward way that isn't error prone for calling `zadd`, `zinterstore`, and `zunionstore` with Redis supported options.
1 parent 392be14 commit 6423299

File tree

3 files changed

+58
-35
lines changed

3 files changed

+58
-35
lines changed

Sources/RedisNIO/Commands/SortedSetCommands.swift

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ extension RedisClient {
4646

4747
// MARK: General
4848

49+
/// The supported options for the `zadd` command with Redis SortedSet types.
50+
/// - Important: Per Redis documentation, `.onlyUpdateExistingElements` and `.onlyAddNewElements` are mutually exclusive!
51+
/// - Note: `INCR` is not supported by this library in `zadd`. Use the `zincrby(:element:in:)` method instead.
52+
/// See [https://redis.io/commands/zadd#zadd-options-redis-302-or-greater](https://redis.io/commands/zadd#zadd-options-redis-302-or-greater)
53+
public enum RedisSortedSetAddOption: String {
54+
/// When adding elements, any that do not already exist in the SortedSet will be ignored and the score of the existing element will be updated.
55+
case onlyUpdateExistingElements = "XX"
56+
/// When adding elements, any that already exist in the SortedSet will be ignored and the score of the existing element will not be updated.
57+
case onlyAddNewElements = "NX"
58+
/// `zadd` normally returns the number of new elements added to the set,
59+
/// but this option will instead have the command return the number of elements changed.
60+
///
61+
/// "Changed" in this context are new elements added, and elements that had their score updated.
62+
case returnChangedCount = "CH"
63+
}
64+
4965
extension RedisClient {
5066
/// Adds elements to a sorted set, assigning their score to the values provided.
5167
///
@@ -59,28 +75,21 @@ extension RedisClient {
5975
public func zadd<Value: RESPValueConvertible>(
6076
_ elements: [(element: Value, score: Double)],
6177
to key: String,
62-
options: Set<String> = []
78+
options: Set<RedisSortedSetAddOption> = []
6379
) -> EventLoopFuture<Int> {
64-
guard !options.contains("INCR") else {
65-
return self.eventLoop.makeFailedFuture(RedisNIOError.unsupportedOperation(
66-
method: #function,
67-
message: "INCR option is unsupported. Use zincrby(_:element:in:) instead."
68-
))
69-
}
70-
7180
assert(options.count <= 2, "Invalid number of options provided.")
72-
assert(options.allSatisfy(["XX", "NX", "CH"].contains), "Unsupported option provided!")
7381
assert(
74-
!(options.contains("XX") && options.contains("NX")),
75-
"XX and NX options are mutually exclusive."
82+
!(options.contains(.onlyAddNewElements) && options.contains(.onlyUpdateExistingElements)),
83+
".onlyAddNewElements and .onlyUpdateExistingElements options are mutually exclusive."
7684
)
7785

7886
var args: [RESPValue] = [.init(bulk: key)]
79-
args.append(convertingContentsOf: options)
80-
81-
for (element, score) in elements {
82-
args.append(.init(bulk: score.description))
83-
args.append(element.convertedToRESPValue())
87+
args.add(contentsOf: options) { (array, option) in
88+
array.append(.init(bulk: option.rawValue))
89+
}
90+
args.add(contentsOf: elements, overestimatedCountBeingAdded: elements.count * 2) { (array, next) in
91+
array.append(.init(bulk: next.score.description))
92+
array.append(next.element.convertedToRESPValue())
8493
}
8594

8695
return send(command: "ZADD", with: args)
@@ -99,7 +108,7 @@ extension RedisClient {
99108
public func zadd<Value: RESPValueConvertible>(
100109
_ element: (element: Value, score: Double),
101110
to key: String,
102-
options: Set<String> = []
111+
options: Set<RedisSortedSetAddOption> = []
103112
) -> EventLoopFuture<Bool> {
104113
return zadd([element], to: key, options: options)
105114
.map { return $0 == 1 }
@@ -490,6 +499,20 @@ extension RedisClient {
490499

491500
// MARK: Intersect and Union
492501

502+
/// The supported methods for aggregating results from the `zunionstore` or `zinterstore` commands in Redis.
503+
///
504+
/// For more information on these values, see
505+
/// [https://redis.io/commands/zunionstore](https://redis.io/commands/zunionstore)
506+
/// [https://redis.io/commands/zinterstore](https://redis.io/commands/zinterstore)
507+
public enum RedisSortedSetAggregateMethod: String {
508+
/// Add the score of all matching elements in the source SortedSets.
509+
case sum = "SUM"
510+
/// Use the minimum score of the matching elements in the source SortedSets.
511+
case min = "MIN"
512+
/// Use the maximum score of the matching elements in the source SortedSets.
513+
case max = "MAX"
514+
}
515+
493516
extension RedisClient {
494517
/// Calculates the union of two or more sorted sets and stores the result.
495518
/// - Note: This operation overwrites any value stored at the destination key.
@@ -499,14 +522,14 @@ extension RedisClient {
499522
/// - destination: The key of the new sorted set from the result.
500523
/// - sources: The list of sorted set keys to treat as the source of the union.
501524
/// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
502-
/// - aggregateMethod: The method of aggregating the values of the union. Supported values are "SUM", "MIN", and "MAX".
525+
/// - aggregateMethod: The method of aggregating the values of the union. If one isn't specified, Redis will default to `.sum`.
503526
/// - Returns: The number of elements in the new sorted set.
504527
@inlinable
505528
public func zunionstore(
506529
as destination: String,
507530
sources: [String],
508531
weights: [Int]? = nil,
509-
aggregateMethod aggregate: String? = nil
532+
aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
510533
) -> EventLoopFuture<Int> {
511534
return _zopstore(command: "ZUNIONSTORE", sources, destination, weights, aggregate)
512535
}
@@ -519,14 +542,14 @@ extension RedisClient {
519542
/// - destination: The key of the new sorted set from the result.
520543
/// - sources: The list of sorted set keys to treat as the source of the intersection.
521544
/// - weights: The multiplying factor to apply to the corresponding `sources` key based on index of the two parameters.
522-
/// - aggregateMethod: The method of aggregating the values of the intersection. Supported values are "SUM", "MIN", and "MAX".
545+
/// - aggregateMethod: The method of aggregating the values of the intersection. If one isn't specified, Redis will default to `.sum`.
523546
/// - Returns: The number of elements in the new sorted set.
524547
@inlinable
525548
public func zinterstore(
526549
as destination: String,
527550
sources: [String],
528551
weights: [Int]? = nil,
529-
aggregateMethod aggregate: String? = nil
552+
aggregateMethod aggregate: RedisSortedSetAggregateMethod? = nil
530553
) -> EventLoopFuture<Int> {
531554
return _zopstore(command: "ZINTERSTORE", sources, destination, weights, aggregate)
532555
}
@@ -537,8 +560,8 @@ extension RedisClient {
537560
_ sources: [String],
538561
_ destination: String,
539562
_ weights: [Int]?,
540-
_ aggregate: String?) -> EventLoopFuture<Int>
541-
{
563+
_ aggregate: RedisSortedSetAggregateMethod?
564+
) -> EventLoopFuture<Int> {
542565
assert(sources.count > 0, "At least 1 source key should be provided.")
543566

544567
var args: [RESPValue] = [
@@ -556,10 +579,8 @@ extension RedisClient {
556579
}
557580

558581
if let a = aggregate {
559-
assert(a == "SUM" || a == "MIN" || a == "MAX", "Aggregate method provided is unsupported.")
560-
561582
args.append(.init(bulk: "AGGREGATE"))
562-
args.append(.init(bulk: a))
583+
args.append(.init(bulk: a.rawValue))
563584
}
564585

565586
return send(command: command, with: args)

Sources/RedisNIO/Extensions/StandardLibrary.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extension Array where Element == RESPValue {
3030
/// Adds the elements of a collection to this array, delegating the details of how they are added to the given closure.
3131
///
3232
/// When your closure will be doing more than a simple transform of the element value, such as when you're adding both the key _and_ value from a `KeyValuePair`,
33-
/// you should set the `overestimatedCountBeingAdded` to a value you do not expect to reach in order to prevent multiple allocations from the increasing
33+
/// you should set the `overestimatedCountBeingAdded` to a value you do not expect to exceed in order to prevent multiple allocations from the increasing
3434
/// element count.
3535
///
3636
/// For example:

Tests/RedisNIOTests/Commands/SortedSetCommandsTests.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,22 @@ final class SortedSetCommandsTests: XCTestCase {
4747
func test_zadd() throws {
4848
_ = try connection.send(command: "FLUSHALL").wait()
4949

50-
XCTAssertThrowsError(try connection.zadd([(30, 2)], to: #function, options: ["INCR"]).wait())
51-
5250
var count = try connection.zadd([(30, 2)], to: #function).wait()
5351
XCTAssertEqual(count, 1)
5452
count = try connection.zadd([(30, 5)], to: #function).wait()
5553
XCTAssertEqual(count, 0)
56-
count = try connection.zadd([(30, 6), (31, 0), (32, 1)], to: #function, options: ["NX"]).wait()
54+
count = try connection.zadd([(30, 6), (31, 0), (32, 1)], to: #function, options: [.onlyAddNewElements]).wait()
5755
XCTAssertEqual(count, 2)
58-
count = try connection.zadd([(32, 2), (33, 3)], to: #function, options: ["XX", "CH"]).wait()
56+
count = try connection.zadd(
57+
[(32, 2), (33, 3)],
58+
to: #function,
59+
options: [.onlyUpdateExistingElements, .returnChangedCount]
60+
).wait()
5961
XCTAssertEqual(count, 1)
6062

61-
var success = try connection.zadd((30, 7), to: #function, options: ["CH"]).wait()
63+
var success = try connection.zadd((30, 7), to: #function, options: [.returnChangedCount]).wait()
6264
XCTAssertTrue(success)
63-
success = try connection.zadd((30, 8), to: #function, options: ["NX"]).wait()
65+
success = try connection.zadd((30, 8), to: #function, options: [.onlyAddNewElements]).wait()
6466
XCTAssertFalse(success)
6567
}
6668

@@ -227,7 +229,7 @@ final class SortedSetCommandsTests: XCTestCase {
227229
as: #function+#file,
228230
sources: [key, #function, #file],
229231
weights: [3, 2, 1],
230-
aggregateMethod: "MAX"
232+
aggregateMethod: .max
231233
).wait()
232234
XCTAssertEqual(unionCount, 10)
233235
let rank = try connection.zrank(of: 10, in: #function+#file).wait()
@@ -243,7 +245,7 @@ final class SortedSetCommandsTests: XCTestCase {
243245
as: #file,
244246
sources: [key, #function],
245247
weights: [3, 2],
246-
aggregateMethod: "MIN"
248+
aggregateMethod: .min
247249
).wait()
248250
XCTAssertEqual(unionCount, 2)
249251
let rank = try connection.zrank(of: 10, in: #file).wait()

0 commit comments

Comments
 (0)