Skip to content

Commit eb1a847

Browse files
author
Nilanshu Sharma
committed
Standard return types and errors for hrandfield
Signed-off-by: Nilanshu Sharma <[email protected]>
1 parent b813e95 commit eb1a847

File tree

5 files changed

+296
-2
lines changed

5 files changed

+296
-2
lines changed

Sources/Valkey/Commands/Custom/HashCustomCommands.swift

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,150 @@ extension HSCAN {
6666
}
6767
}
6868
}
69+
70+
extension HRANDFIELD {
71+
/// Custom response type for HRANDFIELD command that handles all possible return scenarios
72+
@_documentation(visibility: internal)
73+
public struct Response: RESPTokenDecodable, Sendable {
74+
/// The raw RESP token containing the response
75+
public let token: RESPToken
76+
77+
@inlinable
78+
public init(fromRESP token: RESPToken) throws {
79+
self.token = token
80+
}
81+
82+
/// Get single random field when HRANDFIELD was called without COUNT
83+
/// - Returns: Random field name as ByteBuffer, or nil if key doesn't exist
84+
/// - Throws: RESPDecodeError if response format is unexpected
85+
@inlinable
86+
public func singleField() throws -> ByteBuffer? {
87+
switch token.value {
88+
case .null:
89+
return nil
90+
case .bulkString(let buffer):
91+
return buffer
92+
default:
93+
throw RESPDecodeError.tokenMismatch(expected: [.null, .bulkString], token: token)
94+
}
95+
}
96+
97+
/// Get multiple random fields when HRANDFIELD was called with COUNT but without WITHVALUES
98+
/// - Returns: Array of field names as ByteBuffer, or empty array if key doesn't exist
99+
/// - Throws: RESPDecodeError if response format is unexpected
100+
@inlinable
101+
public func multipleFields() throws -> [ByteBuffer] {
102+
switch token.value {
103+
case .null:
104+
return []
105+
case .array(let array):
106+
return try array.decode(as: [ByteBuffer].self)
107+
default:
108+
throw RESPDecodeError.tokenMismatch(expected: [.null, .array], token: token)
109+
}
110+
}
111+
112+
/// Get multiple random field-value pairs when HRANDFIELD was called with COUNT and WITHVALUES
113+
/// - Returns: Array of HashEntry (field-value pairs), or empty array if key doesn't exist
114+
/// - Throws: RESPDecodeError if response format is unexpected
115+
public func multipleFieldsWithValues() throws -> [HashEntry] {
116+
switch token.value {
117+
case .null:
118+
return []
119+
case .array(let array):
120+
return try _decodeArrayAsHashEntries(array)
121+
case .map(let map):
122+
return try _decodeMapAsHashEntries(map)
123+
default:
124+
throw RESPDecodeError.tokenMismatch(expected: [.null, .array, .map], token: token)
125+
}
126+
}
127+
128+
/// Helper method to decode RESP array as hash entries
129+
/// - Parameter array: RESP array to decode
130+
/// - Returns: Array of HashEntry objects
131+
/// - Throws: RESPDecodeError if format is invalid
132+
internal func _decodeArrayAsHashEntries(_ array: RESPToken.Array) throws -> [HashEntry] {
133+
// Convert to Swift array for easier access
134+
let elements = Array(array)
135+
136+
guard !elements.isEmpty else {
137+
return []
138+
}
139+
140+
switch elements[0].value {
141+
case .array:
142+
// Format: [[field1, value1], [field2, value2], ...]
143+
return try _decodeNestedArrayFormat(elements)
144+
default:
145+
// Format: [field1, value1, field2, value2, ...] (flat array)
146+
return try _decodeFlatArrayFormat(elements)
147+
}
148+
}
149+
150+
/// Helper method to decode nested array format
151+
/// - Parameter elements: Swift array of RESP tokens containing nested arrays
152+
/// - Returns: Array of HashEntry objects
153+
/// - Throws: RESPDecodeError if format is invalid
154+
internal func _decodeNestedArrayFormat(_ elements: [RESPToken]) throws -> [HashEntry] {
155+
var entries: [HashEntry] = []
156+
entries.reserveCapacity(elements.count)
157+
158+
for element in elements {
159+
guard case .array(let pairArray) = element.value else {
160+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: element)
161+
}
162+
163+
let pairElements = Array(pairArray)
164+
guard pairElements.count == 2 else {
165+
throw RESPDecodeError(.invalidArraySize, token: element)
166+
}
167+
168+
let field = try ByteBuffer(fromRESP: pairElements[0])
169+
let value = try ByteBuffer(fromRESP: pairElements[1])
170+
entries.append(HashEntry(field: field, value: value))
171+
}
172+
173+
return entries
174+
}
175+
176+
/// Helper method to decode flat array format
177+
/// - Parameter elements: Swift array of RESP tokens containing alternating field-value pairs
178+
/// - Returns: Array of HashEntry objects
179+
/// - Throws: RESPDecodeError if format is invalid
180+
internal func _decodeFlatArrayFormat(_ elements: [RESPToken]) throws -> [HashEntry] {
181+
guard elements.count % 2 == 0 else {
182+
throw RESPDecodeError(.invalidArraySize, token: token)
183+
}
184+
185+
var entries: [HashEntry] = []
186+
entries.reserveCapacity(elements.count / 2)
187+
188+
for i in stride(from: 0, to: elements.count, by: 2) {
189+
let field = try ByteBuffer(fromRESP: elements[i])
190+
let value = try ByteBuffer(fromRESP: elements[i + 1])
191+
entries.append(HashEntry(field: field, value: value))
192+
}
193+
194+
return entries
195+
}
196+
197+
/// Helper method to decode RESP map as hash entries
198+
/// - Parameter map: RESP map to decode
199+
/// - Returns: Array of HashEntry objects
200+
/// - Throws: RESPDecodeError if format is invalid
201+
internal func _decodeMapAsHashEntries(_ map: RESPToken.Map) throws -> [HashEntry] {
202+
var entries: [HashEntry] = []
203+
entries.reserveCapacity(map.count)
204+
205+
for pair in map {
206+
let field = try ByteBuffer(fromRESP: pair.key)
207+
let value = try ByteBuffer(fromRESP: pair.value)
208+
entries.append(HashEntry(field: field, value: value))
209+
}
210+
211+
return entries
212+
}
213+
214+
}
215+
}

Sources/Valkey/Commands/HashCommands.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,6 @@ public struct HRANDFIELD: ValkeyCommand {
807807
RESPPureToken("WITHVALUES", withvalues).encode(into: &commandEncoder)
808808
}
809809
}
810-
public typealias Response = RESPToken?
811810

812811
@inlinable public static var name: String { "HRANDFIELD" }
813812

@@ -1397,7 +1396,7 @@ extension ValkeyClientProtocol {
13971396
/// * [Array]: A list of fields. Returned in case `COUNT` was used.
13981397
/// * [Array]: Fields and their values. Returned in case `COUNT` and `WITHVALUES` were used. In RESP2 this is returned as a flat array.
13991398
@inlinable
1400-
public func hrandfield(_ key: ValkeyKey, options: HRANDFIELD.Options? = nil) async throws -> RESPToken? {
1399+
public func hrandfield(_ key: ValkeyKey, options: HRANDFIELD.Options? = nil) async throws -> HRANDFIELD.Response {
14011400
try await execute(HRANDFIELD(key, options: options))
14021401
}
14031402

Sources/ValkeyConnectionPool/ConnectionPool.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ extension DiscardingTaskGroup: TaskGroupProtocol {
593593
}
594594
}
595595

596+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
596597
extension TaskGroup<Void>: TaskGroupProtocol {
597598
@inlinable
598599
mutating func addTask_(operation: @escaping @Sendable () async -> Void) {

Sources/ValkeyConnectionPool/ConnectionRequest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
12
public struct ConnectionRequest<Connection: PooledConnection>: ConnectionRequestProtocol {
23
public typealias ID = Int
34

Tests/IntegrationTests/ClientIntegrationTests.swift

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,150 @@ struct ClientIntegratedTests {
627627
}
628628
}
629629

630+
@Test
631+
@available(valkeySwift 1.0, *)
632+
func testHrandfieldSingleField() async throws {
633+
var logger = Logger(label: "Valkey")
634+
logger.logLevel = .debug
635+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
636+
try await withKey(connection: connection) { key in
637+
// Set up test hash with known fields
638+
_ = try await connection.hset(key, data: [
639+
HSET.Data(field: "field1", value: "value1"),
640+
HSET.Data(field: "field2", value: "value2"),
641+
HSET.Data(field: "field3", value: "value3")
642+
])
643+
644+
// Test HRANDFIELD without options (should return single field)
645+
let response = try await connection.hrandfield(key)
646+
647+
let singleField = try response.singleField()
648+
#expect(singleField != nil)
649+
650+
let fieldName = String(buffer: singleField!)
651+
#expect(["field1", "field2", "field3"].contains(fieldName))
652+
}
653+
}
654+
}
655+
656+
@Test
657+
@available(valkeySwift 1.0, *)
658+
func testHrandfieldMultipleFields() async throws {
659+
var logger = Logger(label: "Valkey")
660+
logger.logLevel = .debug
661+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
662+
try await withKey(connection: connection) { key in
663+
// Set up test hash with known fields
664+
_ = try await connection.hset(key, data: [
665+
HSET.Data(field: "field1", value: "value1"),
666+
HSET.Data(field: "field2", value: "value2"),
667+
HSET.Data(field: "field3", value: "value3"),
668+
HSET.Data(field: "field4", value: "value4")
669+
])
670+
671+
// Test HRANDFIELD with COUNT option (no WITHVALUES)
672+
let options = HRANDFIELD.Options(count: 2, withvalues: false)
673+
let response = try await connection.hrandfield(key, options: options)
674+
675+
let multipleFields = try response.multipleFields()
676+
#expect(multipleFields.count == 2)
677+
678+
let fieldNames = multipleFields.map { String(buffer: $0) }
679+
for fieldName in fieldNames {
680+
#expect(["field1", "field2", "field3", "field4"].contains(fieldName))
681+
}
682+
683+
// Ensure we got unique fields
684+
let uniqueFieldNames = Set(fieldNames)
685+
#expect(uniqueFieldNames.count == fieldNames.count)
686+
}
687+
}
688+
}
689+
690+
@Test
691+
@available(valkeySwift 1.0, *)
692+
func testHrandfieldMultipleFieldsWithValues() async throws {
693+
var logger = Logger(label: "Valkey")
694+
logger.logLevel = .debug
695+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
696+
try await withKey(connection: connection) { key in
697+
// Set up test hash with known field-value pairs
698+
let testData = [
699+
("field1", "value1"),
700+
("field2", "value2"),
701+
("field3", "value3"),
702+
("field4", "value4")
703+
]
704+
705+
_ = try await connection.hset(key, data: testData.map {
706+
HSET.Data(field: $0.0, value: $0.1)
707+
})
708+
709+
// Test HRANDFIELD with COUNT and WITHVALUES options
710+
let options = HRANDFIELD.Options(count: 3, withvalues: true)
711+
let response = try await connection.hrandfield(key, options: options)
712+
713+
let fieldValuePairs = try response.multipleFieldsWithValues()
714+
#expect(fieldValuePairs.count == 3)
715+
716+
let expectedPairs = Dictionary(testData, uniquingKeysWith: { first, _ in first })
717+
718+
for entry in fieldValuePairs {
719+
let fieldName = String(buffer: entry.field)
720+
let fieldValue = String(buffer: entry.value)
721+
722+
#expect(expectedPairs[fieldName] == fieldValue)
723+
}
724+
}
725+
}
726+
}
727+
728+
@Test
729+
@available(valkeySwift 1.0, *)
730+
func testHrandfieldNonExistentKey() async throws {
731+
var logger = Logger(label: "Valkey")
732+
logger.logLevel = .debug
733+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
734+
try await withKey(connection: connection) { key in
735+
// Test HRANDFIELD on non-existent key
736+
let response = try await connection.hrandfield(key)
737+
738+
let singleField = try response.singleField()
739+
#expect(singleField == nil)
740+
741+
let multipleFields = try response.multipleFields()
742+
#expect(multipleFields.isEmpty)
743+
744+
let fieldValuePairs = try response.multipleFieldsWithValues()
745+
#expect(fieldValuePairs.isEmpty)
746+
}
747+
}
748+
}
749+
750+
@Test
751+
@available(valkeySwift 1.0, *)
752+
func testHrandfieldErrorHandling() async throws {
753+
var logger = Logger(label: "Valkey")
754+
logger.logLevel = .debug
755+
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
756+
try await withKey(connection: connection) { key in
757+
// Set up test hash
758+
_ = try await connection.hset(key, data: [
759+
HSET.Data(field: "field1", value: "value1")
760+
])
761+
762+
let response = try await connection.hrandfield(key)
763+
764+
// Test calling wrong method for response type should throw
765+
#expect(throws: RESPDecodeError.self) {
766+
_ = try response.multipleFields() // Should throw since this is a single field response
767+
}
768+
769+
#expect(throws: RESPDecodeError.self) {
770+
_ = try response.multipleFieldsWithValues() // Should throw since this is a single field response
771+
}
772+
}
773+
}
774+
}
775+
630776
}

0 commit comments

Comments
 (0)