Skip to content

Commit 610915a

Browse files
committed
Add new 'RedisTypes' module
Motivation: Redis is written in C, so even though it has concepts of "types" such as SortedSet or List its commands are all "free-floating" functions. This can make it unfamiliar for those new to Redis to work within its systems and understand the relation of all of the commands. RediStack can improve this by giving a way of having a consistent reference to a Redis type and all of its associated methods. Modifications: - Add: New library product called "RedisTypes" - Add: First type to "RedisTypes", `RedisSet` Result: Newcomers to Redis will have an easier time getting familiar with the APIs and working with its types by having wrappers that provide a familiar Swift Standard Library API tailored to Redis APIs.
1 parent 85cbb61 commit 610915a

File tree

4 files changed

+419
-5
lines changed

4 files changed

+419
-5
lines changed

Package.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44
// This source file is part of the RediStack open source project
55
//
6-
// Copyright (c) 2019 RediStack project authors
6+
// Copyright (c) 2019-2020 RediStack project authors
77
// Licensed under Apache License v2.0
88
//
99
// See LICENSE.txt for license information
@@ -19,7 +19,8 @@ let package = Package(
1919
name: "RediStack",
2020
products: [
2121
.library(name: "RediStack", targets: ["RediStack"]),
22-
.library(name: "RediStackTestUtils", targets: ["RediStackTestUtils"])
22+
.library(name: "RediStackTestUtils", targets: ["RediStackTestUtils"]),
23+
.library(name: "RedisTypes", targets: ["RedisTypes"])
2324
],
2425
dependencies: [
2526
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
@@ -28,10 +29,10 @@ let package = Package(
2829
],
2930
targets: [
3031
.target(name: "RediStack", dependencies: ["NIO", "Logging", "Metrics"]),
32+
.target(name: "RedisTypes", dependencies: ["RediStack"]),
3133
.target(name: "RediStackTestUtils", dependencies: ["NIO", "RediStack"]),
32-
.testTarget(name: "RediStackTests", dependencies: [
33-
"RediStack", "NIO", "RediStackTestUtils", "NIOTestUtils"
34-
]),
34+
.testTarget(name: "RediStackTests", dependencies: ["RediStack", "NIO", "RediStackTestUtils", "NIOTestUtils"]),
35+
.testTarget(name: "RedisTypesTests", dependencies: ["RediStack", "NIO", "RediStackTestUtils", "RedisTypes"]),
3536
.testTarget(name: "RediStackIntegrationTests", dependencies: ["RediStack", "NIO", "RediStackTestUtils"])
3637
]
3738
)

Sources/RedisTypes/RedisSet.swift

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2020 RediStack project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIO
16+
import RediStack
17+
18+
extension RedisClient {
19+
/// Creates a `RedisSet` reference to the value stored at `key` with values of the type specificed.
20+
///
21+
/// let setOfIDs = client.makeSetReference(key: "ids", type: Int.self)
22+
/// // setOfIDs represents a Set of `Int`.
23+
///
24+
/// - Parameters:
25+
/// - key: The Redis key to identify the Set.
26+
/// - type: The Swift type representation of the elements in the set.
27+
/// - Returns: A `RedisSet` for repeatedly interacting with a specific Set value in Redis.
28+
public func makeSet<Element>(key: RedisKey, type: Element.Type = Element.self) -> RedisSet<Element> {
29+
return RedisSet(identifier: key, client: self)
30+
}
31+
}
32+
33+
extension RedisError {
34+
// The verbatim message from Redis for index out of range errors to use in shortcutting network requests.
35+
internal static let indexOutOfRange = RedisError(reason: "ERR index out of range")
36+
}
37+
38+
/// A convenience object that references a specific Set value type in a Redis instance.
39+
///
40+
/// The main purpose of this object is if you have a persistent Set value stored in Redis that you will need to reference several times - such as an index.
41+
///
42+
/// It will allow you to give it a reusable `RediStack.RedisClient` and an ID to handle the proper calls to the client to fetch the desired data in the key.
43+
///
44+
/// Ideally, working with a `RedisSet` should feel as familiar as any other Swift `Collection`.
45+
///
46+
/// let client = ...
47+
/// let userIDSet = RedisSet<Int>(identifier: "users_ids", client: client)
48+
/// let count = userIDSet.insert(30).flatMap { _ in userIDSet.count }.wait()
49+
/// print(count) // Int(1)
50+
///
51+
/// - Note: Use of `.wait()` in the example is for simplicity.. Never call `.wait()` on a `NIO.EventLoop`!
52+
///
53+
/// - Important: `RedisSet<T>` instances have _reference_ semantics,
54+
/// as it holds a reference to a `RediStack.RedisClient` existential which could be a class.
55+
///
56+
/// It is also important to note that this will retain that instance in reference counts.
57+
///
58+
/// See [https://redis.io/topics/data-types-intro#sets](https://redis.io/topics/data-types-intro#sets)
59+
public struct RedisSet<Element> where Element: RESPValueConvertible {
60+
/// The key in Redis that this instance is a reference to.
61+
public var identifier: RedisKey { return self.id }
62+
63+
private let id: RedisKey
64+
private let client: RedisClient
65+
66+
/// Initializes a new reference to a specific Redis key that holds a Set value type.
67+
/// - Parameters:
68+
/// - identifier: The key identifier to reference this set.
69+
/// - client: The `RediStack.RedisClient` to use for making calls to Redis.
70+
public init(identifier: RedisKey, client: RedisClient) {
71+
self.id = identifier
72+
self.client = client
73+
}
74+
75+
/// Resolves the number of elements in the set.
76+
///
77+
/// See `RediStack.RedisClient.scard(of:)`
78+
public var count: EventLoopFuture<Int> { return self.client.scard(of: self.id) }
79+
/// Resolves a Boolean value that indicates whether the set is empty.
80+
public var isEmpty: EventLoopFuture<Bool> { return self.count.map { $0 == 0 } }
81+
/// Resolves all of elements in the set.
82+
///
83+
/// All member elements will be converted into the type `Element`, based on its conformance to `RediStack.RESPValueConvertible`.
84+
/// All `nil` values will be filtered from the result.
85+
///
86+
/// See `RediStack.RedisClient.smembers(of:)`
87+
public var allElements: EventLoopFuture<[Element]> {
88+
return self.client.smembers(of: self.id)
89+
.map { $0.compactMap(Element.init) }
90+
}
91+
92+
/// Resolves a Boolean value that indicates whether the given element exists in the set.
93+
///
94+
/// See `RediStack.RedisClient.sismember(_:of:)`
95+
/// - Parameter member: An element to look for in the set.
96+
/// - Returns: A `NIO.EventLoopFuture<Bool>` resolving `true` if `member` exists in the set; otherwise, `false`.
97+
public func contains(_ member: Element) -> EventLoopFuture<Bool> {
98+
return self.client.sismember(member, of: self.id)
99+
}
100+
}
101+
102+
// MARK: Inserting Elements
103+
104+
extension RedisSet {
105+
/// Inserts the given element(s) in the set if it is not already present.
106+
///
107+
/// See `RediStack.RedisClient.sadd(_:to:)`
108+
/// - Parameter newMember: An element to insert into the set.
109+
/// - Returns: A `NIO.EventLoopFuture<Bool>` resolving `true` if `newMember` was inserted into the set; otherwise, `false`.
110+
public func insert(_ newMember: Element) -> EventLoopFuture<Bool> {
111+
return self.insert(contentsOf: [newMember])
112+
.map { $0 == 1 }
113+
}
114+
115+
/// Inserts the elements of an array into the set that do not already exist.
116+
///
117+
/// See `RediStack.RedisClient.sadd(_:to:)`
118+
/// - Parameter newMembers: The elements to insert into the set.
119+
/// - Returns: A `NIO.EventLoopFuture<Int>` resolving the number of elements inserted into the set.
120+
public func insert(contentsOf newMembers: [Element]) -> EventLoopFuture<Int> {
121+
guard newMembers.count > 0 else { return self.client.eventLoop.makeSucceededFuture(0) }
122+
return self.client.sadd(newMembers, to: self.id)
123+
}
124+
}
125+
126+
// MARK: Removing Elements
127+
128+
extension RedisSet {
129+
/// Moves the given element from the current set to the other given set.
130+
///
131+
/// See `RediStack.RedisClient.smove(_:from:to:)`
132+
/// - Parameters:
133+
/// - member: The element in the set to move.
134+
/// - other:A set of the same type as the current set.
135+
/// - Returns: A `NIO.EventLoopFuture<Bool>` resolving `true` if the element was moved; otherwise, `false`.
136+
public func move(_ member: Element, to other: RedisSet<Element>) -> EventLoopFuture<Bool> {
137+
return self.client.smove(member, from: self.id, to: other.id)
138+
}
139+
140+
/// Removes the given element from the set.
141+
///
142+
/// See `RediStack.RedisClient.srem(_:from:)`
143+
/// - Parameter members: The element in the set to remove.
144+
/// - Returns: A `NIO.EventLoopFuture<Bool>` resolving `true` if `member` was removed from the set; otherwise, `false`.
145+
public func remove(_ member: Element) -> EventLoopFuture<Bool> {
146+
return self.remove([member])
147+
.map { $0 == 1 }
148+
}
149+
150+
/// Removes the given elements from the set.
151+
///
152+
/// See `RediStack.RedisClient.srem(_:from:)`
153+
/// - Parameter members: The elements to remove from the set.
154+
/// - Returns: A `NIO.EventLoopFuture<Int>` resolving the number of elements removed from the set.
155+
public func remove(_ members: [Element]) -> EventLoopFuture<Int> {
156+
guard members.count > 0 else { return self.client.eventLoop.makeSucceededFuture(0) }
157+
return self.client.srem(members, from: self.id)
158+
}
159+
160+
/// Removes all elements from the array.
161+
///
162+
/// See `RediStack.RedisClient.delete(_:)`
163+
/// - Returns: A `NIO.EventLoopFuture<Bool>` resolving `true` if all elements were removed; otherwise, `false`.
164+
public func removeAll() -> EventLoopFuture<Bool> {
165+
return self.client.delete([self.id])
166+
.map { $0 == 1 }
167+
}
168+
}
169+
170+
// MARK: Random Elements
171+
172+
extension RedisSet {
173+
/// Removes and resolves a random element in the set.
174+
///
175+
/// See `RediStack.RedisClient.spop(from:)`
176+
///
177+
/// - Note: This will convert a `RESPValue` response into the `Element`, depending on its conformance to `RESPValueConvertible`.
178+
/// If the conversion fails, the resolved value will be `nil`.
179+
///
180+
/// - Returns: A `NIO.EventLoopFuture<Element?>` resolving a randomly popped element from the set, or `nil` if the set was empty.
181+
public func popRandomElement() -> EventLoopFuture<Element?> {
182+
return self.client.spop(from: self.id)
183+
.map { response in
184+
guard response.count > 0 else { return nil }
185+
return Element(fromRESP: response[0])
186+
}
187+
}
188+
189+
/// Removes and resolves multiple elements from the set, up to the given `max` count.
190+
///
191+
/// See `RediStack.RedisClient.spop(from:max:)`
192+
///
193+
/// - Note: This will convert the elements from `RESPValue` representations into the `Element`, depending on its conformance to `RESPValueConvertible`.
194+
/// `nil` values from the conversion will be filtered from the resolved result.
195+
/// - Parameter count: The max number of elements that should be popped from the set.
196+
/// - Returns: A `NIO.EventLoopFuture<[Element]>` resolving between `0` and `max` count of random elements in the set.
197+
public func popRandomElements(max count: Int) -> EventLoopFuture<[Element]> {
198+
guard count >= 0 else { return self.client.eventLoop.makeFailedFuture(RedisError.indexOutOfRange) }
199+
guard count >= 1 else { return self.client.eventLoop.makeSucceededFuture([]) }
200+
return self.client.spop(from: self.id, max: count)
201+
.map { return $0.compactMap(Element.init) }
202+
}
203+
204+
/// Resolves a random element in the set.
205+
///
206+
/// See `RediStack.RedisClient.srandmember(from:max:)`
207+
///
208+
/// - Note: This will convert a `RESPValue` response into the `Element`, depending on its conformance to `RESPValueConvertible`.
209+
/// If the conversion fails, the resolved value will be `nil`.
210+
///
211+
/// - Returns: A `NIO.EventLoopFuture<Element?>` resolving a randoml element from the set, or `nil` if the set was empty.
212+
public func randomElement() -> EventLoopFuture<Element?> {
213+
return self.client.srandmember(from: self.id)
214+
.map { response in
215+
guard response.count > 0 else { return nil }
216+
return Element(fromRESP: response[0])
217+
}
218+
}
219+
220+
/// Resolves multiple elements from the set, up to the given `max` count.
221+
///
222+
/// // assume `intSet` has 3 elements
223+
/// let intSet: RedisSet<Int> = ...
224+
///
225+
/// // returns all 3 elements
226+
/// intSet.random(max: 4, allowDuplicates: false)
227+
/// // returns 4 elements, with a duplicate
228+
/// intSet.random(max: 4, allowDuplicates: true)
229+
///
230+
/// See `RediStack.RedisClient.srandmember(from:max:)`
231+
///
232+
/// - Note: This will convert the elements from `RESPValue` representations into the `Element`, depending on its conformance to `RESPValueConvertible`.
233+
/// `nil` values from the conversion will be filtered from the resolved result.
234+
/// - Parameters:
235+
/// - max: The max number of elements to select, as available.
236+
/// - allowDuplicates: Should duplicate elements be picked?
237+
/// - Returns: A `NIO.EventLoopFuture<[Element]>` resolving between `0` and `max` count of random elements in the set.
238+
public func randomElements(max: Int, allowDuplicates: Bool = false) -> EventLoopFuture<[Element]> {
239+
assert(max > 0, "Max should be a positive value. Use 'allowDuplicates' to handle proper value signing.")
240+
241+
let count = allowDuplicates ? -max : max
242+
return self.client.srandmember(from: self.id, max: count)
243+
.map { $0.compactMap(Element.init) }
244+
}
245+
}

0 commit comments

Comments
 (0)