@@ -53,3 +53,148 @@ extension CLIENT.TRACKINGINFO {
5353 }
5454
5555}
56+
57+ extension CLIENT . LIST {
58+ /// Field name in a CLIENT LIST response.
59+ ///
60+ /// Represents a field name from the CLIENT LIST output. Uses raw representable pattern
61+ /// to handle both known and unknown fields gracefully, allowing version-safe parsing.
62+ public struct Field : RawRepresentable , Hashable , Sendable , CustomStringConvertible {
63+ public let rawValue : String
64+
65+ public init ( rawValue: String ) {
66+ self . rawValue = rawValue
67+ }
68+
69+ public var description : String { self . rawValue }
70+
71+ /// The unique client ID
72+ public static var id : Field { . init( rawValue: " id " ) }
73+ /// The address and port of the client (format: ip:port)
74+ public static var addr : Field { . init( rawValue: " addr " ) }
75+ /// The address and port of the local address client connected to (bind address)
76+ public static var laddr : Field { . init( rawValue: " laddr " ) }
77+ /// The file descriptor corresponding to the socket
78+ public static var fd : Field { . init( rawValue: " fd " ) }
79+ /// The connection name
80+ public static var name : Field { . init( rawValue: " name " ) }
81+ /// The total duration of the connection in seconds
82+ public static var age : Field { . init( rawValue: " age " ) }
83+ /// The idle time of the connection in seconds
84+ public static var idle : Field { . init( rawValue: " idle " ) }
85+ /// The client flags (see documentation for flag meanings)
86+ public static var flags : Field { . init( rawValue: " flags " ) }
87+ /// The current database ID
88+ public static var db : Field { . init( rawValue: " db " ) }
89+ /// The number of channel subscriptions
90+ public static var sub : Field { . init( rawValue: " sub " ) }
91+ /// The number of pattern matching subscriptions
92+ public static var psub : Field { . init( rawValue: " psub " ) }
93+ /// The number of shard channel subscriptions
94+ public static var ssub : Field { . init( rawValue: " ssub " ) }
95+ /// The number of commands in a MULTI/EXEC context
96+ public static var multi : Field { . init( rawValue: " multi " ) }
97+ /// The query buffer length (0 means no query pending)
98+ public static var qbuf : Field { . init( rawValue: " qbuf " ) }
99+ /// The free space of the query buffer (0 means the buffer is full)
100+ public static var qbufFree : Field { . init( rawValue: " qbuf-free " ) }
101+ /// The incomplete arguments for the next command (already extracted from query buffer)
102+ public static var argvMem : Field { . init( rawValue: " argv-mem " ) }
103+ /// The memory used by buffered multi commands
104+ public static var multiMem : Field { . init( rawValue: " multi-mem " ) }
105+ /// The output buffer length
106+ public static var obl : Field { . init( rawValue: " obl " ) }
107+ /// The output list length (replies that are queued)
108+ public static var oll : Field { . init( rawValue: " oll " ) }
109+ /// The output buffer memory usage
110+ public static var omem : Field { . init( rawValue: " omem " ) }
111+ /// The total memory consumed by this client
112+ public static var totMem : Field { . init( rawValue: " tot-mem " ) }
113+ /// The file descriptor events (r/w)
114+ public static var events : Field { . init( rawValue: " events " ) }
115+ /// The last command played
116+ public static var cmd : Field { . init( rawValue: " cmd " ) }
117+ /// The authenticated username of the client
118+ public static var user : Field { . init( rawValue: " user " ) }
119+ /// The client ID of current client tracking redirection
120+ public static var redir : Field { . init( rawValue: " redir " ) }
121+ /// The RESP protocol version used by the client
122+ public static var resp : Field { . init( rawValue: " resp " ) }
123+ /// The client library name
124+ public static var libName : Field { . init( rawValue: " lib-name " ) }
125+ /// The client library version
126+ public static var libVer : Field { . init( rawValue: " lib-ver " ) }
127+ /// The read buffer size
128+ public static var rbs : Field { . init( rawValue: " rbs " ) }
129+ /// The read buffer peak
130+ public static var rbp : Field { . init( rawValue: " rbp " ) }
131+ }
132+
133+ /// Response type for CLIENT LIST command.
134+ ///
135+ /// Returns an array of client information dictionaries, where each dictionary
136+ /// maps field names to their string values. This approach gracefully handles
137+ /// new fields that may be added in future Valkey versions.
138+ public struct Response : RESPTokenDecodable , Sendable {
139+ /// Array of client information dictionaries
140+ public let clients : [ [ Field : Substring ] ]
141+
142+ /// Creates a CLIENT LIST response from the response token you provide.
143+ ///
144+ /// Parses the bulk string response from CLIENT LIST, which contains one line
145+ /// per client connection with space-separated key=value pairs.
146+ ///
147+ /// - Parameter token: The response token containing CLIENT LIST data.
148+ public init ( _ token: RESPToken ) throws ( RESPDecodeError) {
149+ switch token. value {
150+ case . verbatimString:
151+ let fullString = try String ( token)
152+
153+ // Verbatim strings must have a 3-letter encoding prefix followed by colon (e.g., "txt:")
154+ guard fullString. count >= 4 ,
155+ fullString. prefix ( 3 ) . allSatisfy ( { $0. isLetter } ) ,
156+ fullString. dropFirst ( 3 ) . first == " : "
157+ else {
158+ throw RESPDecodeError ( . cannotParseVerbatimString, token: token)
159+ }
160+
161+ // Strip the "xxx:" prefix to get the actual content
162+ let string = String ( fullString. dropFirst ( 4 ) )
163+ self . clients = Self . parseClientListData ( string)
164+
165+ case . bulkString:
166+ let string = try String ( token)
167+ self . clients = Self . parseClientListData ( string)
168+
169+ default :
170+ throw RESPDecodeError . tokenMismatch ( expected: [ . bulkString, . verbatimString] , token: token)
171+ }
172+ }
173+
174+ /// Parse CLIENT LIST data from a string into client dictionaries
175+ private static func parseClientListData( _ string: String ) -> [ [ Field : Substring ] ] {
176+ var clients : [ [ Field : Substring ] ] = [ ]
177+
178+ // Use SplitStringSequence for efficient parsing
179+ for line in string. splitSequence ( separator: " \n " ) {
180+ var client : [ Field : Substring ] = [ : ]
181+
182+ // Split by spaces and parse key=value pairs
183+ for component in line. splitSequence ( separator: " " ) {
184+ if !component. contains ( " = " ) {
185+ continue
186+ }
187+ let parts = component. splitMaxSplitsSequence ( separator: " = " , maxSplits: 1 )
188+ var partsIterator = parts. makeIterator ( )
189+ guard let key = partsIterator. next ( ) else { continue }
190+ let field = Field ( rawValue: String ( key) )
191+ client [ field] = partsIterator. next ( ) ?? " "
192+ }
193+
194+ clients. append ( client)
195+ }
196+
197+ return clients
198+ }
199+ }
200+ }
0 commit comments