valkey-rb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +58 -0
- data/.rubocop_todo.yml +22 -0
- data/README.md +95 -0
- data/Rakefile +23 -0
- data/lib/valkey/bindings.rb +224 -0
- data/lib/valkey/commands/bitmap_commands.rb +86 -0
- data/lib/valkey/commands/cluster_commands.rb +259 -0
- data/lib/valkey/commands/connection_commands.rb +318 -0
- data/lib/valkey/commands/function_commands.rb +255 -0
- data/lib/valkey/commands/generic_commands.rb +525 -0
- data/lib/valkey/commands/geo_commands.rb +87 -0
- data/lib/valkey/commands/hash_commands.rb +587 -0
- data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
- data/lib/valkey/commands/json_commands.rb +389 -0
- data/lib/valkey/commands/list_commands.rb +348 -0
- data/lib/valkey/commands/module_commands.rb +125 -0
- data/lib/valkey/commands/pubsub_commands.rb +237 -0
- data/lib/valkey/commands/scripting_commands.rb +286 -0
- data/lib/valkey/commands/server_commands.rb +961 -0
- data/lib/valkey/commands/set_commands.rb +220 -0
- data/lib/valkey/commands/sorted_set_commands.rb +971 -0
- data/lib/valkey/commands/stream_commands.rb +636 -0
- data/lib/valkey/commands/string_commands.rb +359 -0
- data/lib/valkey/commands/transaction_commands.rb +175 -0
- data/lib/valkey/commands/vector_search_commands.rb +271 -0
- data/lib/valkey/commands.rb +68 -0
- data/lib/valkey/errors.rb +41 -0
- data/lib/valkey/libglide_ffi.so +0 -0
- data/lib/valkey/opentelemetry.rb +207 -0
- data/lib/valkey/pipeline.rb +20 -0
- data/lib/valkey/protobuf/command_request_pb.rb +51 -0
- data/lib/valkey/protobuf/connection_request_pb.rb +51 -0
- data/lib/valkey/protobuf/response_pb.rb +39 -0
- data/lib/valkey/pubsub_callback.rb +10 -0
- data/lib/valkey/request_error_type.rb +10 -0
- data/lib/valkey/request_type.rb +436 -0
- data/lib/valkey/response_type.rb +20 -0
- data/lib/valkey/utils.rb +253 -0
- data/lib/valkey/version.rb +5 -0
- data/lib/valkey.rb +551 -0
- metadata +119 -0
data/lib/valkey/utils.rb
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Valkey
|
|
4
|
+
# Valkey Utils module
|
|
5
|
+
#
|
|
6
|
+
# This module provides utility functions for transforming and processing
|
|
7
|
+
# data structures commonly used in Valkey commands.
|
|
8
|
+
#
|
|
9
|
+
# It includes methods for converting values to boolean, hash, or float,
|
|
10
|
+
# as well as methods for handling specific Valkey command responses.
|
|
11
|
+
#
|
|
12
|
+
module Utils
|
|
13
|
+
Boolify = lambda { |value|
|
|
14
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
15
|
+
|
|
16
|
+
value != 0 unless value.nil?
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
BoolifySet = lambda { |value|
|
|
20
|
+
case value
|
|
21
|
+
when "OK"
|
|
22
|
+
true
|
|
23
|
+
when nil
|
|
24
|
+
false
|
|
25
|
+
else
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Hashify = lambda { |value|
|
|
31
|
+
if value.respond_to?(:each_slice)
|
|
32
|
+
value.each_slice(2).to_h
|
|
33
|
+
else
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Pairify = lambda { |value|
|
|
39
|
+
if value.respond_to?(:each_slice)
|
|
40
|
+
value.each_slice(2).to_a
|
|
41
|
+
else
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Floatify = lambda { |value|
|
|
47
|
+
case value
|
|
48
|
+
when "inf"
|
|
49
|
+
Float::INFINITY
|
|
50
|
+
when "-inf"
|
|
51
|
+
-Float::INFINITY
|
|
52
|
+
when String
|
|
53
|
+
Float(value)
|
|
54
|
+
else
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
FloatifyPair = lambda { |(first, score)|
|
|
60
|
+
[first, Floatify.call(score)]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
FloatifyPairs = lambda { |value|
|
|
64
|
+
return value unless value.respond_to?(:each_slice)
|
|
65
|
+
|
|
66
|
+
value.each_slice(2).map(&FloatifyPair)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
HashifyInfo = lambda { |reply|
|
|
70
|
+
lines = reply.split("\r\n").grep_v(/^(#|$)/)
|
|
71
|
+
lines.map! { |line| line.split(':', 2) }
|
|
72
|
+
lines.compact!
|
|
73
|
+
lines.to_h
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
HashifyStreams = lambda { |reply|
|
|
77
|
+
case reply
|
|
78
|
+
when nil
|
|
79
|
+
{}
|
|
80
|
+
else
|
|
81
|
+
reply.transform_values { |entries| HashifyStreamEntries.call(entries) }
|
|
82
|
+
end
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
EMPTY_STREAM_RESPONSE = [nil].freeze
|
|
86
|
+
private_constant :EMPTY_STREAM_RESPONSE
|
|
87
|
+
|
|
88
|
+
HashifyStreamEntries = lambda { |reply|
|
|
89
|
+
return [] if reply.nil?
|
|
90
|
+
|
|
91
|
+
return [] if !reply.is_a?(Array) || reply.empty?
|
|
92
|
+
|
|
93
|
+
# Reply format: [[entry_id, [field1, value1, field2, value2, ...]], ...]
|
|
94
|
+
# Match redis-rb: return flat arrays [["id", ["field", "value", ...]], ...]
|
|
95
|
+
# Check if first element is a pair [entry_id, values_array]
|
|
96
|
+
first_elem = reply.first
|
|
97
|
+
if first_elem.is_a?(Array) && first_elem.length == 2
|
|
98
|
+
# Already in pair format: [[entry_id, [fields...]], ...]
|
|
99
|
+
reply.compact.map do |entry_id, values|
|
|
100
|
+
# Return flat array format like redis-rb, not hash
|
|
101
|
+
values_array = if values.nil?
|
|
102
|
+
[]
|
|
103
|
+
elsif values.is_a?(Array)
|
|
104
|
+
values
|
|
105
|
+
else
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
108
|
+
[entry_id, values_array]
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
# Flat array format: [entry_id1, [field1, value1, ...], entry_id2, [field2, value2, ...], ...]
|
|
112
|
+
reply.compact.each_slice(2).map do |entry_id, values|
|
|
113
|
+
# Return flat array format like redis-rb, not hash
|
|
114
|
+
values_array = if values.nil?
|
|
115
|
+
[]
|
|
116
|
+
elsif values.is_a?(Array)
|
|
117
|
+
values
|
|
118
|
+
else
|
|
119
|
+
[]
|
|
120
|
+
end
|
|
121
|
+
[entry_id, values_array]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
HashifyStreamAutoclaim = lambda { |reply|
|
|
127
|
+
{
|
|
128
|
+
'next' => reply[0],
|
|
129
|
+
'entries' => if reply[1].nil?
|
|
130
|
+
[]
|
|
131
|
+
elsif reply[1].is_a?(Array)
|
|
132
|
+
# Reply[1] is already an array of entries: [[id, [field, value, ...]], ...]
|
|
133
|
+
# Use HashifyStreamEntries to convert them properly
|
|
134
|
+
HashifyStreamEntries.call(reply[1])
|
|
135
|
+
else
|
|
136
|
+
[]
|
|
137
|
+
end
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
HashifyStreamAutoclaimJustId = lambda { |reply|
|
|
142
|
+
{
|
|
143
|
+
'next' => reply[0],
|
|
144
|
+
'entries' => reply[1]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
HashifyStreamPendings = lambda { |reply|
|
|
149
|
+
{
|
|
150
|
+
'size' => reply[0],
|
|
151
|
+
'min_entry_id' => reply[1],
|
|
152
|
+
'max_entry_id' => reply[2],
|
|
153
|
+
'consumers' => reply[3].nil? ? {} : reply[3].to_h
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
HashifyStreamPendingDetails = lambda { |reply|
|
|
158
|
+
reply.map do |arr|
|
|
159
|
+
{
|
|
160
|
+
'entry_id' => arr[0],
|
|
161
|
+
'consumer' => arr[1],
|
|
162
|
+
'elapsed' => arr[2],
|
|
163
|
+
'count' => arr[3]
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
HashifyClusterNodeInfo = lambda { |str|
|
|
169
|
+
arr = str.split
|
|
170
|
+
{
|
|
171
|
+
'node_id' => arr[0],
|
|
172
|
+
'ip_port' => arr[1],
|
|
173
|
+
'flags' => arr[2].split(','),
|
|
174
|
+
'master_node_id' => arr[3],
|
|
175
|
+
'ping_sent' => arr[4],
|
|
176
|
+
'pong_recv' => arr[5],
|
|
177
|
+
'config_epoch' => arr[6],
|
|
178
|
+
'link_state' => arr[7],
|
|
179
|
+
'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
HashifyClusterSlots = lambda { |reply|
|
|
184
|
+
reply.map do |arr|
|
|
185
|
+
first_slot, last_slot = arr[0..1]
|
|
186
|
+
master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
|
|
187
|
+
replicas = arr[3..].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
|
|
188
|
+
{
|
|
189
|
+
'start_slot' => first_slot,
|
|
190
|
+
'end_slot' => last_slot,
|
|
191
|
+
'master' => master,
|
|
192
|
+
'replicas' => replicas
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
HashifyClusterNodes = lambda { |reply|
|
|
198
|
+
reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
HashifyClusterSlaves = lambda { |reply|
|
|
202
|
+
reply.map { |str| HashifyClusterNodeInfo.call(str) }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Noop = ->(reply) { reply }
|
|
206
|
+
|
|
207
|
+
# Parse Redis URL format: redis://[:password@]host[:port][/db]
|
|
208
|
+
# Also supports: rediss:// (SSL)
|
|
209
|
+
#
|
|
210
|
+
# @param [String] url Redis URL to parse
|
|
211
|
+
# @return [Hash] Parsed connection options with keys: :host, :port, :password, :username, :db, :ssl
|
|
212
|
+
# @example
|
|
213
|
+
# parse_redis_url('redis://:secret@localhost:6379/15')
|
|
214
|
+
# # => { host: 'localhost', port: 6379, password: 'secret', db: 15, ssl: false }
|
|
215
|
+
#
|
|
216
|
+
# parse_redis_url('rediss://user:secret@localhost:6380/0')
|
|
217
|
+
# # => { host: 'localhost', port: 6380, username: 'user', password: 'secret', db: 0, ssl: true }
|
|
218
|
+
def self.parse_redis_url(url)
|
|
219
|
+
return {} unless url.is_a?(String) && !url.empty?
|
|
220
|
+
|
|
221
|
+
# Match redis:// or rediss:// URLs
|
|
222
|
+
# Format: redis[s]://[username:password@]host[:port][/db][?param=value]
|
|
223
|
+
# Supports: redis://host, redis://user:pass@host, redis://:pass@host
|
|
224
|
+
# The regex handles:
|
|
225
|
+
# - No auth: redis://host...
|
|
226
|
+
# - Username and password: redis://user:pass@host...
|
|
227
|
+
# - Password only: redis://:pass@host...
|
|
228
|
+
match = url.match(%r{\A(redis|rediss)://(?:([^:@]*):([^@]+)@)?([^:/]+)(?::(\d+))?(?:/(\d+))?(?:\?.*)?\z})
|
|
229
|
+
|
|
230
|
+
return {} unless match
|
|
231
|
+
|
|
232
|
+
scheme = match[1]
|
|
233
|
+
username = match[2]
|
|
234
|
+
password = match[3]
|
|
235
|
+
host = match[4]
|
|
236
|
+
port = match[5]&.to_i
|
|
237
|
+
db = match[6]&.to_i
|
|
238
|
+
ssl = scheme == "rediss"
|
|
239
|
+
|
|
240
|
+
result = {
|
|
241
|
+
host: host,
|
|
242
|
+
port: port || 6379,
|
|
243
|
+
ssl: ssl
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
result[:username] = username if username && !username.empty?
|
|
247
|
+
result[:password] = password if password && !password.empty?
|
|
248
|
+
result[:db] = db if db
|
|
249
|
+
|
|
250
|
+
result
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|