Skip to content

Commit 42c7ac0

Browse files
authored
Custom function and script responses (#216)
* Custom function and script responses Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Add testSCRIPTfunctions Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Fix testFUNCTIONLIST Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Add response override for FUNCTION STATS Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Add command tests for scripts Add testCommandEncodesDecodes helper function Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Use RESPDecodeError Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * Cleanup RESPToken output Signed-off-by: Adam Fowler <adamfowler71@gmail.com> * FUNCTION.LIST.Script -> FUNCTION.LIST.Function Signed-off-by: Adam Fowler <adamfowler71@gmail.com> --------- Signed-off-by: Adam Fowler <adamfowler71@gmail.com>
1 parent 8ce232e commit 42c7ac0

File tree

8 files changed

+527
-213
lines changed

8 files changed

+527
-213
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// This source file is part of the valkey-swift project
3+
// Copyright (c) 2025 the valkey-swift project authors
4+
//
5+
// See LICENSE.txt for license information
6+
// SPDX-License-Identifier: Apache-2.0
7+
//
8+
// This file is autogenerated by ValkeyCommandsBuilder
9+
10+
import NIOCore
11+
12+
extension FUNCTION {
13+
public typealias LOADResponse = String
14+
}
15+
16+
extension FUNCTION.LIST {
17+
public typealias Response = [ResponseElement]
18+
public struct ResponseElement: RESPTokenDecodable, Sendable {
19+
public struct Function: RESPTokenDecodable, Sendable {
20+
public let name: String
21+
public let description: String?
22+
public let flags: [String]
23+
24+
public init(fromRESP token: RESPToken) throws {
25+
let map = try [String: RESPToken](fromRESP: token)
26+
guard let name = map["name"] else { throw RESPDecodeError.missingToken(key: "name", token: token) }
27+
guard let description = map["description"] else { throw RESPDecodeError.missingToken(key: "description", token: token) }
28+
guard let flags = map["flags"] else { throw RESPDecodeError.missingToken(key: "flags", token: token) }
29+
self.name = try String(fromRESP: name)
30+
self.description = try String?(fromRESP: description)
31+
self.flags = try [String](fromRESP: flags)
32+
}
33+
}
34+
public let libraryName: String
35+
public let engine: String
36+
public let functions: [Function]
37+
public let libraryCode: String?
38+
39+
public init(fromRESP token: RESPToken) throws {
40+
let map = try [String: RESPToken](fromRESP: token)
41+
guard let libraryName = map["library_name"] else { throw RESPDecodeError.missingToken(key: "library_name", token: token) }
42+
guard let engine = map["engine"] else { throw RESPDecodeError.missingToken(key: "engine", token: token) }
43+
guard let functions = map["functions"] else { throw RESPDecodeError.missingToken(key: "functions", token: token) }
44+
let libraryCode = map["library_code"]
45+
self.libraryName = try String(fromRESP: libraryName)
46+
self.engine = try String(fromRESP: engine)
47+
self.functions = try [Function](fromRESP: functions)
48+
self.libraryCode = try libraryCode.map { try String(fromRESP: $0) }
49+
}
50+
}
51+
}
52+
53+
extension FUNCTION.LOAD {
54+
public typealias Response = FUNCTION.LOADResponse
55+
}
56+
57+
extension FUNCTION.STATS {
58+
public struct Response: RESPTokenDecodable, Sendable {
59+
60+
public struct Script: RESPTokenDecodable, Sendable {
61+
public let name: String
62+
public let command: [ByteBuffer]
63+
public let durationInMilliseconds: Double
64+
65+
public init(fromRESP token: RESPToken) throws {
66+
let map = try [String: RESPToken](fromRESP: token)
67+
guard let name = map["name"] else { throw RESPDecodeError.missingToken(key: "name", token: token) }
68+
guard let command = map["command"] else { throw RESPDecodeError.missingToken(key: "command", token: token) }
69+
guard let duration = map["duration_ms"] else { throw RESPDecodeError.missingToken(key: "duration_ms", token: token) }
70+
self.name = try .init(fromRESP: name)
71+
self.command = try .init(fromRESP: command)
72+
self.durationInMilliseconds = try Double(fromRESP: duration)
73+
}
74+
}
75+
public struct Engine: RESPTokenDecodable, Sendable {
76+
public let libraryCount: Int
77+
public let functionCount: Int
78+
79+
public init(fromRESP token: RESPToken) throws {
80+
let map = try [String: RESPToken](fromRESP: token)
81+
guard let libraryCount = map["libraries_count"] else { throw RESPDecodeError.missingToken(key: "libraries_count", token: token) }
82+
guard let functionCount = map["functions_count"] else { throw RESPDecodeError.missingToken(key: "functions_count", token: token) }
83+
self.libraryCount = try .init(fromRESP: libraryCount)
84+
self.functionCount = try .init(fromRESP: functionCount)
85+
}
86+
}
87+
public let runningScript: Script
88+
public let engines: [String: Engine]
89+
public init(fromRESP token: RESPToken) throws {
90+
let map = try [String: RESPToken](fromRESP: token)
91+
guard let runningScript = map["running_script"] else { throw RESPDecodeError.missingToken(key: "running_script", token: token) }
92+
guard let engines = map["engines"] else { throw RESPDecodeError.missingToken(key: "engines", token: token) }
93+
self.runningScript = try .init(fromRESP: runningScript)
94+
self.engines = try .init(fromRESP: engines)
95+
}
96+
}
97+
}
98+
99+
extension SCRIPT {
100+
public typealias LOADResponse = String
101+
public typealias EXISTSResponse = [Int]
102+
public typealias SHOWResponse = String
103+
}
104+
105+
extension SCRIPT.LOAD {
106+
public typealias Response = SCRIPT.LOADResponse
107+
}
108+
109+
extension SCRIPT.EXISTS {
110+
public typealias Response = SCRIPT.EXISTSResponse
111+
}
112+
113+
extension SCRIPT.SHOW {
114+
public typealias Response = SCRIPT.SHOWResponse
115+
}

Sources/Valkey/Commands/ScriptingCommands.swift

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ public enum FUNCTION {
111111
/// Returns information about all libraries.
112112
@_documentation(visibility: internal)
113113
public struct LIST: ValkeyCommand {
114-
public typealias Response = RESPToken.Array
115-
116114
@inlinable public static var name: String { "FUNCTION LIST" }
117115

118116
public var libraryNamePattern: String?
@@ -131,8 +129,6 @@ public enum FUNCTION {
131129
/// Creates a library.
132130
@_documentation(visibility: internal)
133131
public struct LOAD<FunctionCode: RESPStringRenderable>: ValkeyCommand {
134-
public typealias Response = ByteBuffer
135-
136132
@inlinable public static var name: String { "FUNCTION LOAD" }
137133

138134
public var replace: Bool
@@ -186,8 +182,6 @@ public enum FUNCTION {
186182
/// Returns information about a function during execution.
187183
@_documentation(visibility: internal)
188184
public struct STATS: ValkeyCommand {
189-
public typealias Response = RESPToken.Map
190-
191185
@inlinable public static var name: String { "FUNCTION STATS" }
192186

193187
@inlinable public init() {
@@ -239,8 +233,6 @@ public enum SCRIPT {
239233
/// Determines whether server-side Lua scripts exist in the script cache.
240234
@_documentation(visibility: internal)
241235
public struct EXISTS<Sha1: RESPStringRenderable>: ValkeyCommand {
242-
public typealias Response = RESPToken.Array
243-
244236
@inlinable public static var name: String { "SCRIPT EXISTS" }
245237

246238
public var sha1s: [Sha1]
@@ -316,8 +308,6 @@ public enum SCRIPT {
316308
/// Loads a server-side Lua script to the script cache.
317309
@_documentation(visibility: internal)
318310
public struct LOAD<Script: RESPStringRenderable>: ValkeyCommand {
319-
public typealias Response = ByteBuffer
320-
321311
@inlinable public static var name: String { "SCRIPT LOAD" }
322312

323313
public var script: Script
@@ -334,8 +324,6 @@ public enum SCRIPT {
334324
/// Show server-side Lua script in the script cache.
335325
@_documentation(visibility: internal)
336326
public struct SHOW<Sha1: RESPStringRenderable>: ValkeyCommand {
337-
public typealias Response = ByteBuffer
338-
339327
@inlinable public static var name: String { "SCRIPT SHOW" }
340328

341329
public var sha1: Sha1
@@ -621,7 +609,7 @@ extension ValkeyClientProtocol {
621609
/// - Complexity: O(N) where N is the number of functions
622610
@inlinable
623611
@discardableResult
624-
public func functionList(libraryNamePattern: String? = nil, withcode: Bool = false) async throws -> RESPToken.Array {
612+
public func functionList(libraryNamePattern: String? = nil, withcode: Bool = false) async throws -> FUNCTION.LIST.Response {
625613
try await execute(FUNCTION.LIST(libraryNamePattern: libraryNamePattern, withcode: withcode))
626614
}
627615

@@ -633,7 +621,10 @@ extension ValkeyClientProtocol {
633621
/// - Response: [String]: The library name that was loaded
634622
@inlinable
635623
@discardableResult
636-
public func functionLoad<FunctionCode: RESPStringRenderable>(replace: Bool = false, functionCode: FunctionCode) async throws -> ByteBuffer {
624+
public func functionLoad<FunctionCode: RESPStringRenderable>(
625+
replace: Bool = false,
626+
functionCode: FunctionCode
627+
) async throws -> FUNCTION.LOADResponse {
637628
try await execute(FUNCTION.LOAD(replace: replace, functionCode: functionCode))
638629
}
639630

@@ -657,7 +648,7 @@ extension ValkeyClientProtocol {
657648
/// - Complexity: O(1)
658649
@inlinable
659650
@discardableResult
660-
public func functionStats() async throws -> RESPToken.Map {
651+
public func functionStats() async throws -> FUNCTION.STATS.Response {
661652
try await execute(FUNCTION.STATS())
662653
}
663654

@@ -679,7 +670,7 @@ extension ValkeyClientProtocol {
679670
/// - Response: [Array]: An array of integers that correspond to the specified SHA1 digest arguments.
680671
@inlinable
681672
@discardableResult
682-
public func scriptExists<Sha1: RESPStringRenderable>(sha1s: [Sha1]) async throws -> RESPToken.Array {
673+
public func scriptExists<Sha1: RESPStringRenderable>(sha1s: [Sha1]) async throws -> SCRIPT.EXISTSResponse {
683674
try await execute(SCRIPT.EXISTS(sha1s: sha1s))
684675
}
685676

@@ -725,7 +716,7 @@ extension ValkeyClientProtocol {
725716
/// - Response: [String]: The SHA1 digest of the script added into the script cache
726717
@inlinable
727718
@discardableResult
728-
public func scriptLoad<Script: RESPStringRenderable>(script: Script) async throws -> ByteBuffer {
719+
public func scriptLoad<Script: RESPStringRenderable>(script: Script) async throws -> SCRIPT.LOADResponse {
729720
try await execute(SCRIPT.LOAD(script: script))
730721
}
731722

@@ -737,7 +728,7 @@ extension ValkeyClientProtocol {
737728
/// - Response: [String]: Lua script if sha1 hash exists in script cache.
738729
@inlinable
739730
@discardableResult
740-
public func scriptShow<Sha1: RESPStringRenderable>(sha1: Sha1) async throws -> ByteBuffer {
731+
public func scriptShow<Sha1: RESPStringRenderable>(sha1: Sha1) async throws -> SCRIPT.SHOWResponse {
741732
try await execute(SCRIPT.SHOW(sha1: sha1))
742733
}
743734

Sources/Valkey/RESP/RESPToken.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -614,10 +614,10 @@ extension RESPToken.Value: CustomDebugStringConvertible {
614614

615615
func descriptionWith(indent tab: String = "", childIndent childTab: String = "", redact: Bool = true) -> String {
616616
switch self {
617-
case .simpleString(let buffer): "\(tab).simpleString(\(redact ? "\"***\"" : "\"\(String(buffer: buffer))\""))"
618-
case .simpleError(let buffer): "\(tab).simpleError(\("\"\(String(buffer: buffer))\""))"
617+
case .simpleString(let buffer): "\(tab).simpleString(\"\(String(buffer: buffer))\")"
618+
case .simpleError(let buffer): "\(tab).simpleError(\"\(String(buffer: buffer))\")"
619619
case .bulkString(let buffer): "\(tab).bulkString(\(redact ? "\"***\"" : "\"\(String(buffer: buffer))\""))"
620-
case .bulkError(let buffer): "\(tab).bulkError(\("\"\(String(buffer: buffer))\""))"
620+
case .bulkError(let buffer): "\(tab).bulkError(\"\(String(buffer: buffer))\")"
621621
case .verbatimString(let buffer): "\(tab).verbatimString(\(redact ? "\"txt:***\"" : "\"\(String(buffer: buffer))\""))"
622622
case .number(let integer): "\(tab).number(\(integer))"
623623
case .double(let double): "\(tab).double(\(double))"

Sources/_ValkeyCommandsBuilder/ValkeyCommandsRender.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ private let disableResponseCalculationCommands: Set<String> = [
1818
"CLUSTER MYID",
1919
"CLUSTER MYSHARDID",
2020
"CLUSTER SHARDS",
21+
"FUNCTION LIST",
22+
"FUNCTION LOAD",
23+
"FUNCTION STATS",
2124
"GEODIST",
2225
"GEOPOS",
2326
"GEOSEARCH",
@@ -27,6 +30,9 @@ private let disableResponseCalculationCommands: Set<String> = [
2730
"ROLE",
2831
"SCAN",
2932
"SSCAN",
33+
"SCRIPT EXISTS",
34+
"SCRIPT LOAD",
35+
"SCRIPT SHOW",
3036
"XAUTOCLAIM",
3137
"XCLAIM",
3238
"XPENDING",

Tests/IntegrationTests/ClientIntegrationTests.swift

Lines changed: 0 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -369,83 +369,6 @@ struct ClientIntegratedTests {
369369
}
370370
}
371371

372-
@Test
373-
@available(valkeySwift 1.0, *)
374-
func testRole() async throws {
375-
var logger = Logger(label: "Valkey")
376-
logger.logLevel = .debug
377-
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
378-
let role = try await connection.role()
379-
switch role {
380-
case .primary:
381-
break
382-
case .replica, .sentinel:
383-
Issue.record()
384-
}
385-
}
386-
}
387-
388-
@available(valkeySwift 1.0, *)
389-
@Test("Array with count using LMPOP")
390-
func testArrayWithCount() async throws {
391-
var logger = Logger(label: "Valkey")
392-
logger.logLevel = .trace
393-
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
394-
try await withKey(connection: connection) { key in
395-
try await withKey(connection: connection) { key2 in
396-
try await connection.lpush(key, elements: ["a"])
397-
try await connection.lpush(key2, elements: ["b"])
398-
try await connection.lpush(key2, elements: ["c"])
399-
try await connection.lpush(key2, elements: ["d"])
400-
let rt1 = try await connection.lmpop(keys: [key, key2], where: .right)
401-
let (element) = try rt1?.values.decodeElements(as: (String).self)
402-
#expect(rt1?.key == key)
403-
#expect(element == "a")
404-
let rt2 = try await connection.lmpop(keys: [key, key2], where: .right)
405-
let elements2 = try rt2?.values.decode(as: [String].self)
406-
#expect(rt2?.key == key2)
407-
#expect(elements2 == ["b"])
408-
let rt3 = try await connection.lmpop(keys: [key, key2], where: .right, count: 2)
409-
let elements3 = try rt3?.values.decode(as: [String].self)
410-
#expect(rt3?.key == key2)
411-
#expect(elements3 == ["c", "d"])
412-
}
413-
}
414-
}
415-
}
416-
417-
@available(valkeySwift 1.0, *)
418-
@Test
419-
func testLMOVE() async throws {
420-
var logger = Logger(label: "Valkey")
421-
logger.logLevel = .trace
422-
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
423-
try await withKey(connection: connection) { key in
424-
try await withKey(connection: connection) { key2 in
425-
let rtEmpty = try await connection.lmove(source: key, destination: key2, wherefrom: .right, whereto: .left)
426-
#expect(rtEmpty == nil)
427-
try await connection.lpush(key, elements: ["a"])
428-
try await connection.lpush(key, elements: ["b"])
429-
try await connection.lpush(key, elements: ["c"])
430-
try await connection.lpush(key, elements: ["d"])
431-
let list1Before = try await connection.lrange(key, start: 0, stop: -1).decode(as: [String].self)
432-
#expect(list1Before == ["d", "c", "b", "a"])
433-
let list2Before = try await connection.lrange(key2, start: 0, stop: -1).decode(as: [String].self)
434-
#expect(list2Before == [])
435-
for expectedValue in ["a", "b", "c", "d"] {
436-
var rt = try #require(try await connection.lmove(source: key, destination: key2, wherefrom: .right, whereto: .left))
437-
let value = rt.readString(length: 1)
438-
#expect(value == expectedValue)
439-
}
440-
let list1After = try await connection.lrange(key, start: 0, stop: -1).decode(as: [String].self)
441-
#expect(list1After == [])
442-
let list2After = try await connection.lrange(key2, start: 0, stop: -1).decode(as: [String].self)
443-
#expect(list2After == ["d", "c", "b", "a"])
444-
}
445-
}
446-
}
447-
}
448-
449372
@available(valkeySwift 1.0, *)
450373
@Test("Test command error is thrown")
451374
func testCommandError() async throws {
@@ -595,34 +518,6 @@ struct ClientIntegratedTests {
595518
}
596519
}
597520

598-
@available(valkeySwift 1.0, *)
599-
@Test
600-
func testGEOPOS() async throws {
601-
var logger = Logger(label: "Valkey")
602-
logger.logLevel = .trace
603-
try await withValkeyConnection(.hostname(valkeyHostname, port: 6379), logger: logger) { connection in
604-
try await withKey(connection: connection) { key in
605-
let count = try await connection.geoadd(
606-
key,
607-
data: [.init(longitude: 1.0, latitude: 53.0, member: "Edinburgh"), .init(longitude: 1.4, latitude: 53.5, member: "Glasgow")]
608-
)
609-
#expect(count == 2)
610-
let search = try await connection.geosearch(
611-
key,
612-
from: .fromlonlat(.init(longitude: 0.0, latitude: 53.0)),
613-
by: .circle(.init(radius: 10000, unit: .mi)),
614-
withcoord: true,
615-
withdist: true,
616-
withhash: true
617-
)
618-
print(search.map { $0.member })
619-
try print(search.map { try $0.attributes[0].decode(as: Double.self) })
620-
try print(search.map { try $0.attributes[1].decode(as: String.self) })
621-
try print(search.map { try $0.attributes[2].decode(as: GeoCoordinates.self) })
622-
}
623-
}
624-
}
625-
626521
@available(valkeySwift 1.0, *)
627522
@Test
628523
func testClientInfo() async throws {

0 commit comments

Comments
 (0)