Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Sources/Valkey/Commands/Custom/ListCustomCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,31 @@ extension BRPOP {
/// * [Array]: The key from which the element was popped and the value of the popped element
public typealias Response = ListEntry?
}

/// Custom response type for LPOP and RPOP commands
/// Handles the different return types based on whether count parameter is provided
@_documentation(visibility: internal)
public struct ListPopResponse: RESPTokenDecodable, Sendable, Hashable {

private let token: RESPToken

public init(_ token: RESPToken) throws {
self.token = token
}

/// Gets the single element when count was not provided
/// - Returns: ByteBuffer if a single element was returned, nil otherwise
public func element() throws -> ByteBuffer? {
// Handle .null as it is expected when the key doesn't exist
if token.value == .null {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for null here shouldn't be needed, that has already been done because the LPOP/RPOP response is optional. Which means the function can return a non-optional value. It should be RESPBulkString instead of ByteBuffer

return nil
}
return try ByteBuffer(token)
}

/// Gets the multiple elements when count was provided
/// - Returns: Array of ByteBuffer if multiple elements were returned, nil otherwise
public func elements() throws -> [ByteBuffer]? {
try [ByteBuffer]?(token)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be optional

}
}
8 changes: 4 additions & 4 deletions Sources/Valkey/Commands/ListCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ public struct LMPOP: ValkeyCommand {
/// Returns and removes one or more elements from the beginning of a list. Deletes the list if the last element was popped.
@_documentation(visibility: internal)
public struct LPOP: ValkeyCommand {
public typealias Response = RESPToken?
public typealias Response = ListPopResponse?

@inlinable public static var name: String { "LPOP" }

Expand Down Expand Up @@ -564,7 +564,7 @@ public struct LTRIM: ValkeyCommand {
/// Returns and removes one or more elements from the end of a list. Deletes the list if the last element was popped.
@_documentation(visibility: internal)
public struct RPOP: ValkeyCommand {
public typealias Response = RESPToken?
public typealias Response = ListPopResponse?

@inlinable public static var name: String { "RPOP" }

Expand Down Expand Up @@ -817,7 +817,7 @@ extension ValkeyClientProtocol {
/// * [Array]: In case `count` argument was given, a list of popped elements
@inlinable
@discardableResult
public func lpop(_ key: ValkeyKey, count: Int? = nil) async throws -> RESPToken? {
public func lpop(_ key: ValkeyKey, count: Int? = nil) async throws -> LPOP.Response {
try await execute(LPOP(key, count: count))
}

Expand Down Expand Up @@ -925,7 +925,7 @@ extension ValkeyClientProtocol {
/// * [Array]: When 'COUNT' was given, list of popped elements.
@inlinable
@discardableResult
public func rpop(_ key: ValkeyKey, count: Int? = nil) async throws -> RESPToken? {
public func rpop(_ key: ValkeyKey, count: Int? = nil) async throws -> RPOP.Response {
try await execute(RPOP(key, count: count))
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/_ValkeyCommandsBuilder/ValkeyCommandsRender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ private let disableResponseCalculationCommands: Set<String> = [
"ROLE",
"LMOVE",
"LMPOP",
"LPOP",
"RPOP",
"ROLE",
"SCAN",
"SSCAN",
Expand Down
72 changes: 72 additions & 0 deletions Tests/IntegrationTests/CommandIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,76 @@ struct CommandIntegratedTests {
}
}

@Test
@available(valkeySwift 1.0, *)
func testLpopCustomResponse() async throws {
var logger = Logger(label: "Valkey")
logger.logLevel = .debug
try await withValkeyClient(.hostname(valkeyHostname, port: 6379), logger: logger) { client in
try await withKey(connection: client) { key in
// Test empty key - LPOP should handle null response
var lpopResponse = try await client.lpop(key)
#expect(lpopResponse == nil)

// Set up list with elements
_ = try await client.rpush(key, elements: ["first", "second", "third", "fourth"])

// Test single element LPOP (no count)
lpopResponse = try await client.lpop(key)
#expect(lpopResponse != nil)
#expect(try lpopResponse!.element() == ByteBuffer(string: "first"))
#expect(try lpopResponse!.elements() == [ByteBuffer(string: "first")])

// Test multiple elements LPOP (with count)
lpopResponse = try await client.lpop(key, count: 2)
#expect(lpopResponse != nil)
#expect(try lpopResponse!.elements() == [ByteBuffer(string: "second"), ByteBuffer(string: "third")])
do {
_ = try lpopResponse!.element()
Issue.record("Expected RESPDecodeError.tokenMismatch to be thrown")
} catch let error as RESPDecodeError {
#expect(error.errorCode == .tokenMismatch)
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
}
}

@Test
@available(valkeySwift 1.0, *)
func testRpopCustomResponse() async throws {
var logger = Logger(label: "Valkey")
logger.logLevel = .debug
try await withValkeyClient(.hostname(valkeyHostname, port: 6379), logger: logger) { client in
try await withKey(connection: client) { key in
// Test empty key - RPOP should return nil
var rpopResponse = try await client.rpop(key)
#expect(rpopResponse == nil)

// Set up list with elements
_ = try await client.rpush(key, elements: ["first", "second", "third", "fourth", "fifth"])

// Test single element RPOP (no count)
rpopResponse = try await client.rpop(key)
#expect(rpopResponse != nil)
#expect(try rpopResponse!.element() == ByteBuffer(string: "fifth"))
#expect(try rpopResponse!.elements() == [ByteBuffer(string: "fifth")])

// Test multiple elements RPOP (with count)
rpopResponse = try await client.rpop(key, count: 3)
#expect(rpopResponse != nil)
#expect(try rpopResponse!.elements() == [ByteBuffer(string: "fourth"), ByteBuffer(string: "third"), ByteBuffer(string: "second")])
do {
_ = try rpopResponse!.element()
Issue.record("Expected RESPDecodeError.tokenMismatch to be thrown")
} catch let error as RESPDecodeError {
#expect(error.errorCode == .tokenMismatch)
} catch {
Issue.record("Unexpected error type: \(error)")
}
}
}
}

}
22 changes: 18 additions & 4 deletions Tests/ValkeyTests/CommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,24 @@ struct CommandTests {
(request: .command(["LPOP", "key1"]), response: .bulkString("one")),
(request: .command(["LPOP", "key1", "3"]), response: .array([.bulkString("two"), .bulkString("three"), .bulkString("four")]))
) { connection in
var values = try await connection.lpop("key1")
#expect(try values?.decode(as: [String].self) == ["one"])
values = try await connection.lpop("key1", count: 3)
#expect(try values?.decode(as: [String].self) == ["two", "three", "four"])
// Test single element LPOP
var lpopResponse = try await connection.lpop("key1")
#expect(lpopResponse != nil)
#expect(try lpopResponse!.element() == ByteBuffer(string: "one"))
#expect(try lpopResponse!.elements() == [ByteBuffer(string: "one")])

// Test multiple elements LPOP with count
lpopResponse = try await connection.lpop("key1", count: 3)
#expect(lpopResponse != nil)
do {
_ = try lpopResponse!.element()
Issue.record("Expected RESPDecodeError.tokenMismatch to be thrown")
} catch let error as RESPDecodeError {
#expect(error.errorCode == .tokenMismatch)
} catch {
Issue.record("Unexpected error type: \(error)")
}
#expect(try lpopResponse!.elements() == [ByteBuffer(string: "two"), ByteBuffer(string: "three"), ByteBuffer(string: "four")])
}
}

Expand Down
Loading