redis 4.2.5 → 5.0.7

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -0
  3. data/README.md +102 -162
  4. data/lib/redis/client.rb +84 -589
  5. data/lib/redis/commands/bitmaps.rb +66 -0
  6. data/lib/redis/commands/cluster.rb +28 -0
  7. data/lib/redis/commands/connection.rb +53 -0
  8. data/lib/redis/commands/geo.rb +84 -0
  9. data/lib/redis/commands/hashes.rb +254 -0
  10. data/lib/redis/commands/hyper_log_log.rb +37 -0
  11. data/lib/redis/commands/keys.rb +437 -0
  12. data/lib/redis/commands/lists.rb +339 -0
  13. data/lib/redis/commands/pubsub.rb +54 -0
  14. data/lib/redis/commands/scripting.rb +114 -0
  15. data/lib/redis/commands/server.rb +188 -0
  16. data/lib/redis/commands/sets.rb +214 -0
  17. data/lib/redis/commands/sorted_sets.rb +884 -0
  18. data/lib/redis/commands/streams.rb +402 -0
  19. data/lib/redis/commands/strings.rb +314 -0
  20. data/lib/redis/commands/transactions.rb +115 -0
  21. data/lib/redis/commands.rb +237 -0
  22. data/lib/redis/distributed.rb +220 -75
  23. data/lib/redis/errors.rb +15 -41
  24. data/lib/redis/hash_ring.rb +26 -26
  25. data/lib/redis/pipeline.rb +66 -120
  26. data/lib/redis/subscribe.rb +23 -15
  27. data/lib/redis/version.rb +1 -1
  28. data/lib/redis.rb +110 -3441
  29. metadata +27 -54
  30. data/lib/redis/cluster/command.rb +0 -81
  31. data/lib/redis/cluster/command_loader.rb +0 -34
  32. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  33. data/lib/redis/cluster/node.rb +0 -107
  34. data/lib/redis/cluster/node_key.rb +0 -31
  35. data/lib/redis/cluster/node_loader.rb +0 -37
  36. data/lib/redis/cluster/option.rb +0 -90
  37. data/lib/redis/cluster/slot.rb +0 -86
  38. data/lib/redis/cluster/slot_loader.rb +0 -49
  39. data/lib/redis/cluster.rb +0 -295
  40. data/lib/redis/connection/command_helper.rb +0 -39
  41. data/lib/redis/connection/hiredis.rb +0 -67
  42. data/lib/redis/connection/registry.rb +0 -13
  43. data/lib/redis/connection/ruby.rb +0 -427
  44. data/lib/redis/connection/synchrony.rb +0 -146
  45. data/lib/redis/connection.rb +0 -11
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Strings
6
+ # Decrement the integer value of a key by one.
7
+ #
8
+ # @example
9
+ # redis.decr("value")
10
+ # # => 4
11
+ #
12
+ # @param [String] key
13
+ # @return [Integer] value after decrementing it
14
+ def decr(key)
15
+ send_command([:decr, key])
16
+ end
17
+
18
+ # Decrement the integer value of a key by the given number.
19
+ #
20
+ # @example
21
+ # redis.decrby("value", 5)
22
+ # # => 0
23
+ #
24
+ # @param [String] key
25
+ # @param [Integer] decrement
26
+ # @return [Integer] value after decrementing it
27
+ def decrby(key, decrement)
28
+ send_command([:decrby, key, Integer(decrement)])
29
+ end
30
+
31
+ # Increment the integer value of a key by one.
32
+ #
33
+ # @example
34
+ # redis.incr("value")
35
+ # # => 6
36
+ #
37
+ # @param [String] key
38
+ # @return [Integer] value after incrementing it
39
+ def incr(key)
40
+ send_command([:incr, key])
41
+ end
42
+
43
+ # Increment the integer value of a key by the given integer number.
44
+ #
45
+ # @example
46
+ # redis.incrby("value", 5)
47
+ # # => 10
48
+ #
49
+ # @param [String] key
50
+ # @param [Integer] increment
51
+ # @return [Integer] value after incrementing it
52
+ def incrby(key, increment)
53
+ send_command([:incrby, key, Integer(increment)])
54
+ end
55
+
56
+ # Increment the numeric value of a key by the given float number.
57
+ #
58
+ # @example
59
+ # redis.incrbyfloat("value", 1.23)
60
+ # # => 1.23
61
+ #
62
+ # @param [String] key
63
+ # @param [Float] increment
64
+ # @return [Float] value after incrementing it
65
+ def incrbyfloat(key, increment)
66
+ send_command([:incrbyfloat, key, Float(increment)], &Floatify)
67
+ end
68
+
69
+ # Set the string value of a key.
70
+ #
71
+ # @param [String] key
72
+ # @param [String] value
73
+ # @param [Hash] options
74
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
75
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
76
+ # - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds.
77
+ # - `:pxat => Integer` : Set the specified Unix time at which the key will expire, in milliseconds.
78
+ # - `:nx => true`: Only set the key if it does not already exist.
79
+ # - `:xx => true`: Only set the key if it already exist.
80
+ # - `:keepttl => true`: Retain the time to live associated with the key.
81
+ # - `:get => true`: Return the old string stored at key, or nil if key did not exist.
82
+ # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
83
+ def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
84
+ args = [:set, key, value.to_s]
85
+ args << "EX" << Integer(ex) if ex
86
+ args << "PX" << Integer(px) if px
87
+ args << "EXAT" << Integer(exat) if exat
88
+ args << "PXAT" << Integer(pxat) if pxat
89
+ args << "NX" if nx
90
+ args << "XX" if xx
91
+ args << "KEEPTTL" if keepttl
92
+ args << "GET" if get
93
+
94
+ if nx || xx
95
+ send_command(args, &BoolifySet)
96
+ else
97
+ send_command(args)
98
+ end
99
+ end
100
+
101
+ # Set the time to live in seconds of a key.
102
+ #
103
+ # @param [String] key
104
+ # @param [Integer] ttl
105
+ # @param [String] value
106
+ # @return [String] `"OK"`
107
+ def setex(key, ttl, value)
108
+ send_command([:setex, key, Integer(ttl), value.to_s])
109
+ end
110
+
111
+ # Set the time to live in milliseconds of a key.
112
+ #
113
+ # @param [String] key
114
+ # @param [Integer] ttl
115
+ # @param [String] value
116
+ # @return [String] `"OK"`
117
+ def psetex(key, ttl, value)
118
+ send_command([:psetex, key, Integer(ttl), value.to_s])
119
+ end
120
+
121
+ # Set the value of a key, only if the key does not exist.
122
+ #
123
+ # @param [String] key
124
+ # @param [String] value
125
+ # @return [Boolean] whether the key was set or not
126
+ def setnx(key, value)
127
+ send_command([:setnx, key, value.to_s], &Boolify)
128
+ end
129
+
130
+ # Set one or more values.
131
+ #
132
+ # @example
133
+ # redis.mset("key1", "v1", "key2", "v2")
134
+ # # => "OK"
135
+ #
136
+ # @param [Array<String>] args array of keys and values
137
+ # @return [String] `"OK"`
138
+ #
139
+ # @see #mapped_mset
140
+ def mset(*args)
141
+ send_command([:mset] + args)
142
+ end
143
+
144
+ # Set one or more values.
145
+ #
146
+ # @example
147
+ # redis.mapped_mset({ "f1" => "v1", "f2" => "v2" })
148
+ # # => "OK"
149
+ #
150
+ # @param [Hash] hash keys mapping to values
151
+ # @return [String] `"OK"`
152
+ #
153
+ # @see #mset
154
+ def mapped_mset(hash)
155
+ mset(hash.flatten)
156
+ end
157
+
158
+ # Set one or more values, only if none of the keys exist.
159
+ #
160
+ # @example
161
+ # redis.msetnx("key1", "v1", "key2", "v2")
162
+ # # => true
163
+ #
164
+ # @param [Array<String>] args array of keys and values
165
+ # @return [Boolean] whether or not all values were set
166
+ #
167
+ # @see #mapped_msetnx
168
+ def msetnx(*args)
169
+ send_command([:msetnx, *args], &Boolify)
170
+ end
171
+
172
+ # Set one or more values, only if none of the keys exist.
173
+ #
174
+ # @example
175
+ # redis.mapped_msetnx({ "key1" => "v1", "key2" => "v2" })
176
+ # # => true
177
+ #
178
+ # @param [Hash] hash keys mapping to values
179
+ # @return [Boolean] whether or not all values were set
180
+ #
181
+ # @see #msetnx
182
+ def mapped_msetnx(hash)
183
+ msetnx(hash.flatten)
184
+ end
185
+
186
+ # Get the value of a key.
187
+ #
188
+ # @param [String] key
189
+ # @return [String]
190
+ def get(key)
191
+ send_command([:get, key])
192
+ end
193
+
194
+ # Get the values of all the given keys.
195
+ #
196
+ # @example
197
+ # redis.mget("key1", "key2")
198
+ # # => ["v1", "v2"]
199
+ #
200
+ # @param [Array<String>] keys
201
+ # @return [Array<String>] an array of values for the specified keys
202
+ #
203
+ # @see #mapped_mget
204
+ def mget(*keys, &blk)
205
+ keys.flatten!(1)
206
+ send_command([:mget, *keys], &blk)
207
+ end
208
+
209
+ # Get the values of all the given keys.
210
+ #
211
+ # @example
212
+ # redis.mapped_mget("key1", "key2")
213
+ # # => { "key1" => "v1", "key2" => "v2" }
214
+ #
215
+ # @param [Array<String>] keys array of keys
216
+ # @return [Hash] a hash mapping the specified keys to their values
217
+ #
218
+ # @see #mget
219
+ def mapped_mget(*keys)
220
+ mget(*keys) do |reply|
221
+ if reply.is_a?(Array)
222
+ Hash[keys.zip(reply)]
223
+ else
224
+ reply
225
+ end
226
+ end
227
+ end
228
+
229
+ # Overwrite part of a string at key starting at the specified offset.
230
+ #
231
+ # @param [String] key
232
+ # @param [Integer] offset byte offset
233
+ # @param [String] value
234
+ # @return [Integer] length of the string after it was modified
235
+ def setrange(key, offset, value)
236
+ send_command([:setrange, key, Integer(offset), value.to_s])
237
+ end
238
+
239
+ # Get a substring of the string stored at a key.
240
+ #
241
+ # @param [String] key
242
+ # @param [Integer] start zero-based start offset
243
+ # @param [Integer] stop zero-based end offset. Use -1 for representing
244
+ # the end of the string
245
+ # @return [Integer] `0` or `1`
246
+ def getrange(key, start, stop)
247
+ send_command([:getrange, key, Integer(start), Integer(stop)])
248
+ end
249
+
250
+ # Append a value to a key.
251
+ #
252
+ # @param [String] key
253
+ # @param [String] value value to append
254
+ # @return [Integer] length of the string after appending
255
+ def append(key, value)
256
+ send_command([:append, key, value])
257
+ end
258
+
259
+ # Set the string value of a key and return its old value.
260
+ #
261
+ # @param [String] key
262
+ # @param [String] value value to replace the current value with
263
+ # @return [String] the old value stored in the key, or `nil` if the key
264
+ # did not exist
265
+ def getset(key, value)
266
+ send_command([:getset, key, value.to_s])
267
+ end
268
+
269
+ # Get the value of key and delete the key. This command is similar to GET,
270
+ # except for the fact that it also deletes the key on success.
271
+ #
272
+ # @param [String] key
273
+ # @return [String] the old value stored in the key, or `nil` if the key
274
+ # did not exist
275
+ def getdel(key)
276
+ send_command([:getdel, key])
277
+ end
278
+
279
+ # Get the value of key and optionally set its expiration. GETEX is similar to
280
+ # GET, but is a write command with additional options. When no options are
281
+ # provided, GETEX behaves like GET.
282
+ #
283
+ # @param [String] key
284
+ # @param [Hash] options
285
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
286
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
287
+ # - `:exat => true`: Set the specified Unix time at which the key will
288
+ # expire, in seconds.
289
+ # - `:pxat => true`: Set the specified Unix time at which the key will
290
+ # expire, in milliseconds.
291
+ # - `:persist => true`: Remove the time to live associated with the key.
292
+ # @return [String] The value of key, or nil when key does not exist.
293
+ def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
294
+ args = [:getex, key]
295
+ args << "EX" << Integer(ex) if ex
296
+ args << "PX" << Integer(px) if px
297
+ args << "EXAT" << Integer(exat) if exat
298
+ args << "PXAT" << Integer(pxat) if pxat
299
+ args << "PERSIST" if persist
300
+
301
+ send_command(args)
302
+ end
303
+
304
+ # Get the length of the value stored in a key.
305
+ #
306
+ # @param [String] key
307
+ # @return [Integer] the length of the value stored in the key, or 0
308
+ # if the key does not exist
309
+ def strlen(key)
310
+ send_command([:strlen, key])
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Transactions
6
+ # Mark the start of a transaction block.
7
+ #
8
+ # @example With a block
9
+ # redis.multi do |multi|
10
+ # multi.set("key", "value")
11
+ # multi.incr("counter")
12
+ # end # => ["OK", 6]
13
+ #
14
+ # @yield [multi] the commands that are called inside this block are cached
15
+ # and written to the server upon returning from it
16
+ # @yieldparam [Redis] multi `self`
17
+ #
18
+ # @return [Array<...>]
19
+ # - an array with replies
20
+ #
21
+ # @see #watch
22
+ # @see #unwatch
23
+ def multi
24
+ synchronize do |client|
25
+ client.multi do |raw_transaction|
26
+ yield MultiConnection.new(raw_transaction)
27
+ end
28
+ end
29
+ end
30
+
31
+ # Watch the given keys to determine execution of the MULTI/EXEC block.
32
+ #
33
+ # Using a block is optional, but is necessary for thread-safety.
34
+ #
35
+ # An `#unwatch` is automatically issued if an exception is raised within the
36
+ # block that is a subclass of StandardError and is not a ConnectionError.
37
+ #
38
+ # @example With a block
39
+ # redis.watch("key") do
40
+ # if redis.get("key") == "some value"
41
+ # redis.multi do |multi|
42
+ # multi.set("key", "other value")
43
+ # multi.incr("counter")
44
+ # end
45
+ # else
46
+ # redis.unwatch
47
+ # end
48
+ # end
49
+ # # => ["OK", 6]
50
+ #
51
+ # @example Without a block
52
+ # redis.watch("key")
53
+ # # => "OK"
54
+ #
55
+ # @param [String, Array<String>] keys one or more keys to watch
56
+ # @return [Object] if using a block, returns the return value of the block
57
+ # @return [String] if not using a block, returns `OK`
58
+ #
59
+ # @see #unwatch
60
+ # @see #multi
61
+ def watch(*keys)
62
+ synchronize do |client|
63
+ res = client.call_v([:watch] + keys)
64
+
65
+ if block_given?
66
+ begin
67
+ yield(self)
68
+ rescue ConnectionError
69
+ raise
70
+ rescue StandardError
71
+ unwatch
72
+ raise
73
+ end
74
+ else
75
+ res
76
+ end
77
+ end
78
+ end
79
+
80
+ # Forget about all watched keys.
81
+ #
82
+ # @return [String] `OK`
83
+ #
84
+ # @see #watch
85
+ # @see #multi
86
+ def unwatch
87
+ send_command([:unwatch])
88
+ end
89
+
90
+ # Execute all commands issued after MULTI.
91
+ #
92
+ # Only call this method when `#multi` was called **without** a block.
93
+ #
94
+ # @return [nil, Array<...>]
95
+ # - when commands were not executed, `nil`
96
+ # - when commands were executed, an array with their replies
97
+ #
98
+ # @see #multi
99
+ # @see #discard
100
+ def exec
101
+ send_command([:exec])
102
+ end
103
+
104
+ # Discard all commands issued after MULTI.
105
+ #
106
+ # @return [String] `"OK"`
107
+ #
108
+ # @see #multi
109
+ # @see #exec
110
+ def discard
111
+ send_command([:discard])
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis/commands/bitmaps"
4
+ require "redis/commands/cluster"
5
+ require "redis/commands/connection"
6
+ require "redis/commands/geo"
7
+ require "redis/commands/hashes"
8
+ require "redis/commands/hyper_log_log"
9
+ require "redis/commands/keys"
10
+ require "redis/commands/lists"
11
+ require "redis/commands/pubsub"
12
+ require "redis/commands/scripting"
13
+ require "redis/commands/server"
14
+ require "redis/commands/sets"
15
+ require "redis/commands/sorted_sets"
16
+ require "redis/commands/streams"
17
+ require "redis/commands/strings"
18
+ require "redis/commands/transactions"
19
+
20
+ class Redis
21
+ module Commands
22
+ include Bitmaps
23
+ include Cluster
24
+ include Connection
25
+ include Geo
26
+ include Hashes
27
+ include HyperLogLog
28
+ include Keys
29
+ include Lists
30
+ include Pubsub
31
+ include Scripting
32
+ include Server
33
+ include Sets
34
+ include SortedSets
35
+ include Streams
36
+ include Strings
37
+ include Transactions
38
+
39
+ # Commands returning 1 for true and 0 for false may be executed in a pipeline
40
+ # where the method call will return nil. Propagate the nil instead of falsely
41
+ # returning false.
42
+ Boolify = lambda { |value|
43
+ value != 0 unless value.nil?
44
+ }
45
+
46
+ BoolifySet = lambda { |value|
47
+ case value
48
+ when "OK"
49
+ true
50
+ when nil
51
+ false
52
+ else
53
+ value
54
+ end
55
+ }
56
+
57
+ Hashify = lambda { |value|
58
+ if value.respond_to?(:each_slice)
59
+ value.each_slice(2).to_h
60
+ else
61
+ value
62
+ end
63
+ }
64
+
65
+ Pairify = lambda { |value|
66
+ if value.respond_to?(:each_slice)
67
+ value.each_slice(2).to_a
68
+ else
69
+ value
70
+ end
71
+ }
72
+
73
+ Floatify = lambda { |value|
74
+ case value
75
+ when "inf"
76
+ Float::INFINITY
77
+ when "-inf"
78
+ -Float::INFINITY
79
+ when String
80
+ Float(value)
81
+ else
82
+ value
83
+ end
84
+ }
85
+
86
+ FloatifyPairs = lambda { |value|
87
+ return value unless value.respond_to?(:each_slice)
88
+
89
+ value.each_slice(2).map do |member, score|
90
+ [member, Floatify.call(score)]
91
+ end
92
+ }
93
+
94
+ HashifyInfo = lambda { |reply|
95
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
96
+ lines.map! { |line| line.split(':', 2) }
97
+ lines.compact!
98
+ lines.to_h
99
+ }
100
+
101
+ HashifyStreams = lambda { |reply|
102
+ case reply
103
+ when nil
104
+ {}
105
+ else
106
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
107
+ end
108
+ }
109
+
110
+ EMPTY_STREAM_RESPONSE = [nil].freeze
111
+ private_constant :EMPTY_STREAM_RESPONSE
112
+
113
+ HashifyStreamEntries = lambda { |reply|
114
+ reply.compact.map do |entry_id, values|
115
+ [entry_id, values&.each_slice(2)&.to_h]
116
+ end
117
+ }
118
+
119
+ HashifyStreamAutoclaim = lambda { |reply|
120
+ {
121
+ 'next' => reply[0],
122
+ 'entries' => reply[1].compact.map do |entry, values|
123
+ [entry, values.each_slice(2)&.to_h]
124
+ end
125
+ }
126
+ }
127
+
128
+ HashifyStreamAutoclaimJustId = lambda { |reply|
129
+ {
130
+ 'next' => reply[0],
131
+ 'entries' => reply[1]
132
+ }
133
+ }
134
+
135
+ HashifyStreamPendings = lambda { |reply|
136
+ {
137
+ 'size' => reply[0],
138
+ 'min_entry_id' => reply[1],
139
+ 'max_entry_id' => reply[2],
140
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
141
+ }
142
+ }
143
+
144
+ HashifyStreamPendingDetails = lambda { |reply|
145
+ reply.map do |arr|
146
+ {
147
+ 'entry_id' => arr[0],
148
+ 'consumer' => arr[1],
149
+ 'elapsed' => arr[2],
150
+ 'count' => arr[3]
151
+ }
152
+ end
153
+ }
154
+
155
+ HashifyClusterNodeInfo = lambda { |str|
156
+ arr = str.split(' ')
157
+ {
158
+ 'node_id' => arr[0],
159
+ 'ip_port' => arr[1],
160
+ 'flags' => arr[2].split(','),
161
+ 'master_node_id' => arr[3],
162
+ 'ping_sent' => arr[4],
163
+ 'pong_recv' => arr[5],
164
+ 'config_epoch' => arr[6],
165
+ 'link_state' => arr[7],
166
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
167
+ }
168
+ }
169
+
170
+ HashifyClusterSlots = lambda { |reply|
171
+ reply.map do |arr|
172
+ first_slot, last_slot = arr[0..1]
173
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
174
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
175
+ {
176
+ 'start_slot' => first_slot,
177
+ 'end_slot' => last_slot,
178
+ 'master' => master,
179
+ 'replicas' => replicas
180
+ }
181
+ end
182
+ }
183
+
184
+ HashifyClusterNodes = lambda { |reply|
185
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
186
+ }
187
+
188
+ HashifyClusterSlaves = lambda { |reply|
189
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
190
+ }
191
+
192
+ Noop = ->(reply) { reply }
193
+
194
+ # Sends a command to Redis and returns its reply.
195
+ #
196
+ # Replies are converted to Ruby objects according to the RESP protocol, so
197
+ # you can expect a Ruby array, integer or nil when Redis sends one. Higher
198
+ # level transformations, such as converting an array of pairs into a Ruby
199
+ # hash, are up to consumers.
200
+ #
201
+ # Redis error replies are raised as Ruby exceptions.
202
+ def call(*command)
203
+ send_command(command)
204
+ end
205
+
206
+ # Interact with the sentinel command (masters, master, slaves, failover)
207
+ #
208
+ # @param [String] subcommand e.g. `masters`, `master`, `slaves`
209
+ # @param [Array<String>] args depends on subcommand
210
+ # @return [Array<String>, Hash<String, String>, String] depends on subcommand
211
+ def sentinel(subcommand, *args)
212
+ subcommand = subcommand.to_s.downcase
213
+ send_command([:sentinel, subcommand] + args) do |reply|
214
+ case subcommand
215
+ when "get-master-addr-by-name"
216
+ reply
217
+ else
218
+ if reply.is_a?(Array)
219
+ if reply[0].is_a?(Array)
220
+ reply.map(&Hashify)
221
+ else
222
+ Hashify.call(reply)
223
+ end
224
+ else
225
+ reply
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def method_missing(*command) # rubocop:disable Style/MissingRespondToMissing
234
+ send_command(command)
235
+ end
236
+ end
237
+ end