Skip to content

Commit 783cc25

Browse files
authored
Set custom commands responses (#127)
* Set command custom responses Signed-off-by: Adam Fowler <[email protected]> * Add tests Signed-off-by: Adam Fowler <[email protected]> --------- Signed-off-by: Adam Fowler <[email protected]>
1 parent 1ae551f commit 783cc25

File tree

6 files changed

+130
-22
lines changed

6 files changed

+130
-22
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the valkey-swift project
4+
//
5+
// Copyright (c) 2025 the valkey-swift authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See valkey-swift/CONTRIBUTORS.txt for the list of valkey-swift authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
extension SPOP {
16+
public typealias Response = [RESPToken]?
17+
}
18+
19+
extension SSCAN {
20+
public struct Response: RESPTokenDecodable, Sendable {
21+
public let cursor: Int
22+
public let elements: RESPToken.Array
23+
24+
public init(fromRESP token: RESPToken) throws {
25+
// cursor is encoded as a bulkString, but should be
26+
let (cursorString, elements) = try token.decodeArrayElements(as: (String, RESPToken.Array).self)
27+
guard let cursor = Int(cursorString) else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
28+
self.cursor = cursor
29+
self.elements = elements
30+
}
31+
}
32+
}

Sources/Valkey/Commands/SetCommands.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,6 @@ public struct SMOVE<Member: RESPStringRenderable>: ValkeyCommand {
253253
/// Returns one or more random members from a set after removing them. Deletes the set if the last member was popped.
254254
@_documentation(visibility: internal)
255255
public struct SPOP: ValkeyCommand {
256-
public typealias Response = RESPToken?
257-
258256
public var key: ValkeyKey
259257
public var count: Int?
260258

@@ -315,8 +313,6 @@ public struct SREM<Member: RESPStringRenderable>: ValkeyCommand {
315313
/// Iterates over members of a set.
316314
@_documentation(visibility: internal)
317315
public struct SSCAN: ValkeyCommand {
318-
public typealias Response = RESPToken.Array
319-
320316
public var key: ValkeyKey
321317
public var cursor: Int
322318
public var pattern: String?
@@ -518,7 +514,7 @@ extension ValkeyConnectionProtocol {
518514
/// * [String]: The removed member when 'COUNT' is not given.
519515
/// * [Array]: List to the removed members when 'COUNT' is given.
520516
@inlinable
521-
public func spop(_ key: ValkeyKey, count: Int? = nil) async throws -> RESPToken? {
517+
public func spop(_ key: ValkeyKey, count: Int? = nil) async throws -> SPOP.Response {
522518
try await send(command: SPOP(key, count: count))
523519
}
524520

@@ -559,7 +555,7 @@ extension ValkeyConnectionProtocol {
559555
/// - Complexity: O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.
560556
/// - Response: [Array]: Cursor and scan response in array form.
561557
@inlinable
562-
public func sscan(_ key: ValkeyKey, cursor: Int, pattern: String? = nil, count: Int? = nil) async throws -> RESPToken.Array {
558+
public func sscan(_ key: ValkeyKey, cursor: Int, pattern: String? = nil, count: Int? = nil) async throws -> SSCAN.Response {
563559
try await send(command: SSCAN(key, cursor: cursor, pattern: pattern, count: count))
564560
}
565561

Sources/Valkey/RESP/RESPTokenDecodable.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ extension RESPToken: RESPTokenDecodable {
6363
as: (repeat (each Value)).Type = (repeat (each Value)).self
6464
) throws -> (repeat each Value) {
6565
switch self.value {
66-
case .array(let array):
66+
case .array(let array), .set(let array):
6767
try array.decodeElements()
6868
default:
6969
throw RESPParsingError(code: .unexpectedType, buffer: self.base)
@@ -234,7 +234,7 @@ extension Array: RESPTokenDecodable where Element: RESPTokenDecodable {
234234
@inlinable
235235
public init(fromRESP token: RESPToken) throws {
236236
switch token.value {
237-
case .array(let respArray), .push(let respArray):
237+
case .array(let respArray), .set(let respArray), .push(let respArray):
238238
do {
239239
var array: [Element] = []
240240
for respElement in respArray {
@@ -248,6 +248,8 @@ extension Array: RESPTokenDecodable where Element: RESPTokenDecodable {
248248
let value = try Element(fromRESP: token)
249249
self = [value]
250250
}
251+
case .null:
252+
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
251253
default:
252254
let value = try Element(fromRESP: token)
253255
self = [value]
@@ -266,8 +268,11 @@ extension Set: RESPTokenDecodable where Element: RESPTokenDecodable {
266268
set.insert(element)
267269
}
268270
self = set
269-
default:
271+
case .null:
270272
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
273+
default:
274+
let value = try Element(fromRESP: token)
275+
self = [value]
271276
}
272277
}
273278
}
@@ -301,7 +306,7 @@ extension RESPToken.Array: RESPTokenDecodable {
301306
@inlinable
302307
public init(fromRESP token: RESPToken) throws {
303308
switch token.value {
304-
case .array(let respArray), .push(let respArray):
309+
case .array(let respArray), .set(let respArray), .push(let respArray):
305310
self = respArray
306311
default:
307312
throw RESPParsingError(code: .unexpectedType, buffer: token.base)

Sources/_ValkeyCommandsBuilder/ValkeyCommandsRender.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ func renderValkeyCommands(_ commands: [String: ValkeyCommand], fullCommandList:
1919
"BZPOPMIN",
2020
"CLUSTER SHARDS",
2121
"LMPOP",
22+
"SPOP",
23+
"SSCAN",
2224
"XAUTOCLAIM",
2325
"XCLAIM",
2426
"XPENDING",

Tests/IntegrationTests/ValkeyTests.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ struct GeneratedCommands {
140140
}
141141
}
142142

143+
@Test
144+
@available(valkeySwift 1.0, *)
145+
func testSPOP() async throws {
146+
var logger = Logger(label: "Valkey")
147+
logger.logLevel = .debug
148+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
149+
try await withKey(connection: connection) { key in
150+
_ = try await connection.sadd(key, members: (0..<256).map { "test\($0)" })
151+
let response = try await connection.sscan(key, cursor: 0, count: 32)
152+
let response2 = try await connection.sscan(key, cursor: response.cursor, count: 32)
153+
print(try response.elements.decode(as: [String].self))
154+
print(try response2.elements.decode(as: [String].self))
155+
}
156+
}
157+
}
158+
143159
@Test
144160
@available(valkeySwift 1.0, *)
145161
func testUnixTime() async throws {
@@ -373,18 +389,6 @@ struct GeneratedCommands {
373389
}
374390
}
375391
}
376-
/*
377-
@Test
378-
@available(valkeySwift 1.0, *)
379-
func testClientName() async throws {
380-
var logger = Logger(label: "Valkey")
381-
logger.logLevel = .debug
382-
let valkeyClient = ValkeyClient(.hostname(valkeyHostname), logger: logger)
383-
try await valkeyClient.withConnection(name: "phileasfogg", logger: logger) { connection in
384-
let name = try await connection.clientGetname()
385-
#expect(try name?.decode() == "phileasfogg")
386-
}
387-
}*/
388392

389393
@Test
390394
@available(valkeySwift 1.0, *)

Tests/ValkeyTests/CommandTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,75 @@ struct CommandTests {
5151
}
5252
}
5353

54+
struct SetCommands {
55+
@Test
56+
@available(valkeySwift 1.0, *)
57+
func spop() async throws {
58+
let channel = NIOAsyncTestingChannel()
59+
let logger = Logger(label: "test")
60+
let connection = try await ValkeyConnection.setupChannelAndConnect(channel, configuration: .init(), logger: logger)
61+
try await channel.processHello()
62+
63+
try await withThrowingTaskGroup(of: Void.self) { group in
64+
group.addTask {
65+
var result = try #require(try await connection.spop("key1"))
66+
#expect(try result.decode(as: [String].self) == ["mytext"])
67+
result = try #require(try await connection.spop("key2", count: 2))
68+
#expect(try result.decode(as: [String].self) == ["mytext1", "mytext2"])
69+
let nilResult = try await connection.spop("key3")
70+
#expect(nilResult == nil)
71+
}
72+
group.addTask {
73+
var outbound = try await channel.waitForOutboundWrite(as: ByteBuffer.self)
74+
#expect(outbound == RESPToken(.command(["SPOP", "key1"])).base)
75+
try await channel.writeInbound(RESPToken(.bulkString("mytext")).base)
76+
77+
outbound = try await channel.waitForOutboundWrite(as: ByteBuffer.self)
78+
#expect(String(buffer: outbound) == String(buffer: RESPToken(.command(["SPOP", "key2", "2"])).base))
79+
try await channel.writeInbound(RESPToken(.array([.bulkString("mytext1"), .bulkString("mytext2")])).base)
80+
81+
outbound = try await channel.waitForOutboundWrite(as: ByteBuffer.self)
82+
#expect(outbound == RESPToken(.command(["SPOP", "key3"])).base)
83+
try await channel.writeInbound(RESPToken(.null).base)
84+
}
85+
try await group.waitForAll()
86+
}
87+
}
88+
89+
@Test
90+
@available(valkeySwift 1.0, *)
91+
func sscan() async throws {
92+
let channel = NIOAsyncTestingChannel()
93+
let logger = Logger(label: "test")
94+
let connection = try await ValkeyConnection.setupChannelAndConnect(channel, configuration: .init(), logger: logger)
95+
try await channel.processHello()
96+
97+
try await withThrowingTaskGroup(of: Void.self) { group in
98+
group.addTask {
99+
let result = try await connection.sscan("key1", cursor: 0, pattern: "test*", count: 2)
100+
#expect(result.cursor == 8)
101+
#expect(try result.elements.decode(as: [String].self) == ["entry1", "entry2"])
102+
}
103+
group.addTask {
104+
let outbound = try await channel.waitForOutboundWrite(as: ByteBuffer.self)
105+
#expect(outbound == RESPToken(.command(["SSCAN", "key1", "0", "MATCH", "test*", "COUNT", "2"])).base)
106+
try await channel.writeInbound(
107+
RESPToken(
108+
.array([
109+
.bulkString("8"),
110+
.array([
111+
.bulkString("entry1"),
112+
.bulkString("entry2"),
113+
]),
114+
])
115+
).base
116+
)
117+
}
118+
try await group.waitForAll()
119+
}
120+
}
121+
}
122+
54123
struct StringCommands {
55124
@Test
56125
@available(valkeySwift 1.0, *)

0 commit comments

Comments
 (0)