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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +58 -0
  3. data/.rubocop_todo.yml +22 -0
  4. data/README.md +95 -0
  5. data/Rakefile +23 -0
  6. data/lib/valkey/bindings.rb +224 -0
  7. data/lib/valkey/commands/bitmap_commands.rb +86 -0
  8. data/lib/valkey/commands/cluster_commands.rb +259 -0
  9. data/lib/valkey/commands/connection_commands.rb +318 -0
  10. data/lib/valkey/commands/function_commands.rb +255 -0
  11. data/lib/valkey/commands/generic_commands.rb +525 -0
  12. data/lib/valkey/commands/geo_commands.rb +87 -0
  13. data/lib/valkey/commands/hash_commands.rb +587 -0
  14. data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
  15. data/lib/valkey/commands/json_commands.rb +389 -0
  16. data/lib/valkey/commands/list_commands.rb +348 -0
  17. data/lib/valkey/commands/module_commands.rb +125 -0
  18. data/lib/valkey/commands/pubsub_commands.rb +237 -0
  19. data/lib/valkey/commands/scripting_commands.rb +286 -0
  20. data/lib/valkey/commands/server_commands.rb +961 -0
  21. data/lib/valkey/commands/set_commands.rb +220 -0
  22. data/lib/valkey/commands/sorted_set_commands.rb +971 -0
  23. data/lib/valkey/commands/stream_commands.rb +636 -0
  24. data/lib/valkey/commands/string_commands.rb +359 -0
  25. data/lib/valkey/commands/transaction_commands.rb +175 -0
  26. data/lib/valkey/commands/vector_search_commands.rb +271 -0
  27. data/lib/valkey/commands.rb +68 -0
  28. data/lib/valkey/errors.rb +41 -0
  29. data/lib/valkey/libglide_ffi.so +0 -0
  30. data/lib/valkey/opentelemetry.rb +207 -0
  31. data/lib/valkey/pipeline.rb +20 -0
  32. data/lib/valkey/protobuf/command_request_pb.rb +51 -0
  33. data/lib/valkey/protobuf/connection_request_pb.rb +51 -0
  34. data/lib/valkey/protobuf/response_pb.rb +39 -0
  35. data/lib/valkey/pubsub_callback.rb +10 -0
  36. data/lib/valkey/request_error_type.rb +10 -0
  37. data/lib/valkey/request_type.rb +436 -0
  38. data/lib/valkey/response_type.rb +20 -0
  39. data/lib/valkey/utils.rb +253 -0
  40. data/lib/valkey/version.rb +5 -0
  41. data/lib/valkey.rb +551 -0
  42. metadata +119 -0
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ VERSION = "1.0.0"
5
+ end