Skip to content

Commit b060412

Browse files
nilanshu-sharmaNilanshu Sharmaadam-fowler
authored
Standard return types and errors for hrandfield (#262)
* Standard return types and errors for hrandfield Signed-off-by: Nilanshu Sharma <[email protected]> * Consolidating tests and other minor fixes Signed-off-by: Nilanshu Sharma <[email protected]> * Fixing test format check Signed-off-by: Nilanshu Sharma <[email protected]> * Addressing PR comments Signed-off-by: Nilanshu Sharma <[email protected]> * Fixing format soundness Signed-off-by: Nilanshu Sharma <[email protected]> * Fixing tests Signed-off-by: Nilanshu Sharma <[email protected]> --------- Signed-off-by: Nilanshu Sharma <[email protected]> Co-authored-by: Nilanshu Sharma <[email protected]> Co-authored-by: Adam Fowler <[email protected]>
1 parent c8b7686 commit b060412

File tree

3 files changed

+155
-2
lines changed

3 files changed

+155
-2
lines changed

Sources/Valkey/Commands/Custom/HashCustomCommands.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,87 @@ extension HSCAN {
6666
}
6767
}
6868
}
69+
70+
extension HRANDFIELD {
71+
/// Custom response type for HRANDFIELD command that handles all possible return scenarios
72+
public struct Response: RESPTokenDecodable, Sendable {
73+
/// The raw RESP token containing the response
74+
public let token: RESPToken
75+
76+
public init(fromRESP token: RESPToken) throws {
77+
self.token = token
78+
}
79+
80+
/// Get single random field when HRANDFIELD was called without COUNT
81+
/// - Returns: Random field name as ByteBuffer, or nil if key doesn't exist
82+
/// - Throws: RESPDecodeError if response format is unexpected
83+
public func singleField() throws -> ByteBuffer? {
84+
// Handle .null as it is expected when the key doesn't exist
85+
if token.value == .null {
86+
return nil
87+
}
88+
return try ByteBuffer(fromRESP: token)
89+
}
90+
91+
/// Get multiple random fields when HRANDFIELD was called with COUNT but without WITHVALUES
92+
/// - Returns: Array of field names as ByteBuffer, or empty array if key doesn't exist
93+
/// - Throws: RESPDecodeError if response format is unexpected
94+
@inlinable
95+
public func multipleFields() throws -> [ByteBuffer]? {
96+
try [ByteBuffer]?(fromRESP: token)
97+
}
98+
99+
/// Get multiple random field-value pairs when HRANDFIELD was called with COUNT and WITHVALUES
100+
/// - Returns: Array of HashEntry (field-value pairs), or nil if key doesn't exist
101+
/// - Throws: RESPDecodeError if response format is unexpected
102+
public func multipleFieldsWithValues() throws -> [HashEntry]? {
103+
switch token.value {
104+
case .null:
105+
return nil
106+
case .array(let array):
107+
guard array.count > 0 else {
108+
return []
109+
}
110+
111+
// Check first element to determine format
112+
var iterator = array.makeIterator()
113+
guard let firstElement = iterator.next() else {
114+
return []
115+
}
116+
switch firstElement.value {
117+
case .array:
118+
// Array of arrays format - can use HashEntry decode
119+
return try [HashEntry]?(fromRESP: token)
120+
default:
121+
// Flat array format - handle manually
122+
return try _decodeFlatArrayFormat(array)
123+
}
124+
default:
125+
throw RESPDecodeError.tokenMismatch(expected: [.null, .array], token: token)
126+
}
127+
}
128+
129+
/// Helper method to decode flat array format
130+
/// - Parameter array: RESP array containing alternating field-value pairs
131+
/// - Returns: Array of HashEntry objects
132+
/// - Throws: RESPDecodeError if format is invalid
133+
private func _decodeFlatArrayFormat(_ array: RESPToken.Array) throws -> [HashEntry] {
134+
guard array.count % 2 == 0 else {
135+
throw RESPDecodeError(.invalidArraySize, token: token)
136+
}
137+
138+
var entries: [HashEntry] = []
139+
entries.reserveCapacity(array.count / 2)
140+
141+
// Iterate over pairs
142+
var iterator = array.makeIterator()
143+
while let field = iterator.next(), let value = iterator.next() {
144+
let fieldBuffer = try ByteBuffer(fromRESP: field)
145+
let valueBuffer = try ByteBuffer(fromRESP: value)
146+
entries.append(HashEntry(field: fieldBuffer, value: valueBuffer))
147+
}
148+
149+
return entries
150+
}
151+
}
152+
}

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

Tests/IntegrationTests/CommandIntegrationTests.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,74 @@ struct CommandIntegratedTests {
216216
_ = try await client.scriptExists(sha1s: [sha1])
217217
}
218218
}
219+
220+
@Test
221+
@available(valkeySwift 1.0, *)
222+
func testHrandfield() async throws {
223+
var logger = Logger(label: "Valkey")
224+
logger.logLevel = .debug
225+
try await withValkeyClient(.hostname(valkeyHostname, port: 6379), logger: logger) { client in
226+
try await withKey(connection: client) { key in
227+
228+
// Non-existent hash
229+
var response = try await client.hrandfield(key)
230+
var singleField = try response.singleField()
231+
#expect(singleField == nil)
232+
var multipleFields = try response.multipleFields()
233+
#expect(multipleFields == nil)
234+
var fieldValuePairs = try response.multipleFieldsWithValues()
235+
#expect(fieldValuePairs == nil)
236+
237+
// Hash with multiple fields
238+
_ = try await client.hset(
239+
key,
240+
data: [
241+
HSET.Data(field: "field1", value: "value1"),
242+
HSET.Data(field: "field2", value: "value2"),
243+
HSET.Data(field: "field3", value: "value3"),
244+
]
245+
)
246+
247+
// Get Single Field
248+
response = try await client.hrandfield(key)
249+
singleField = try response.singleField()
250+
#expect(singleField != nil)
251+
let fieldName = String(buffer: singleField!)
252+
#expect(["field1", "field2", "field3"].contains(fieldName))
253+
254+
// Get multiple fields
255+
var options = HRANDFIELD.Options(count: 2, withvalues: false)
256+
response = try await client.hrandfield(key, options: options)
257+
multipleFields = try response.multipleFields()
258+
#expect(multipleFields != nil)
259+
if let unwrappedFields = multipleFields {
260+
#expect(unwrappedFields.count == 2)
261+
let fieldNames = unwrappedFields.map { String(buffer: $0) }
262+
for fieldName in fieldNames {
263+
#expect(["field1", "field2", "field3"].contains(fieldName))
264+
}
265+
// Ensure we got unique fields
266+
let uniqueFieldNames = Set(fieldNames)
267+
#expect(uniqueFieldNames.count == fieldNames.count)
268+
}
269+
270+
// Get multiple fields with values
271+
options = HRANDFIELD.Options(count: 3, withvalues: true)
272+
response = try await client.hrandfield(key, options: options)
273+
fieldValuePairs = try response.multipleFieldsWithValues()
274+
#expect(fieldValuePairs != nil)
275+
if let unwrappedFieldValuePairs = fieldValuePairs {
276+
#expect(unwrappedFieldValuePairs.count == 3)
277+
var expectedPairs: [String: String] = [:]
278+
for pair in unwrappedFieldValuePairs {
279+
expectedPairs[String(buffer: pair.field)] = String(buffer: pair.value)
280+
}
281+
#expect(expectedPairs["field1"] == "value1")
282+
#expect(expectedPairs["field2"] == "value2")
283+
#expect(expectedPairs["field3"] == "value3")
284+
}
285+
}
286+
}
287+
}
288+
219289
}

0 commit comments

Comments
 (0)