valkey-namespace 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a385b6f2e0f44403c0fe05fecc1b03823583fc5a8ae177e1b2e0a78ac6e83d31
4
+ data.tar.gz: 26a7e394ad8a9e6609f2397922a6084a162305fe372045faecbfff6c2f97efb1
5
+ SHA512:
6
+ metadata.gz: fb54cb5c43cbec71678dfedf72c1d42b041813bf5f038d0efb80e9f7335c84965d22f4fb99a35b2d1dd9675f754de468781ffa247e93878af4e863b5590bddb2
7
+ data.tar.gz: 7afd85a87386b5d52f7f1cddc2be5f49f9a39156cf75f1751083ed1998e63997a8c55c78dce0026691602cac85a6f9b9a5aee6754a33d660364074259cd91564
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valkey Community
4
+
5
+ This project is based on redis-namespace:
6
+ Copyright (c) 2009 Chris Wanstrath
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
data/NOTICE ADDED
@@ -0,0 +1,25 @@
1
+ valkey-namespace
2
+ Copyright (c) 2026 Valkey Community
3
+
4
+ This project is a derivative work based on redis-namespace:
5
+ https://github.com/resque/redis-namespace
6
+
7
+ redis-namespace
8
+ Copyright (c) 2009 Chris Wanstrath
9
+
10
+ Both projects are licensed under the MIT License.
11
+
12
+ The original redis-namespace implementation has been adapted to work with
13
+ the Valkey database and the valkey-glide-ruby client library. The core
14
+ architecture, command registry patterns, and namespacing logic are derived
15
+ from redis-namespace.
16
+
17
+ Key changes in valkey-namespace:
18
+ - Adapted to work with Valkey instead of Redis
19
+ - Uses valkey-glide-ruby client instead of redis-rb
20
+ - Updated naming conventions (Redis → Valkey)
21
+ - Updated environment variables (REDIS_NAMESPACE_* → VALKEY_NAMESPACE_*)
22
+ - Added examples and additional documentation
23
+
24
+ We are grateful to the redis-namespace project and its contributors for
25
+ their excellent work that made this adaptation possible.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ valkey-namespace
2
+ ===============
3
+
4
+ Valkey::Namespace provides an interface to a namespaced subset of your [Valkey][] keyspace (e.g., keys with a common beginning), and requires the [valkey-glide-ruby][] gem.
5
+
6
+ ```ruby
7
+ require 'valkey-namespace'
8
+ # => true
9
+
10
+ valkey_connection = Valkey.new
11
+ # => #<Valkey client>
12
+ namespaced_valkey = Valkey::Namespace.new(:ns, valkey: valkey_connection)
13
+ # => #<Valkey::Namespace v1.0.0 with client for ns>
14
+
15
+ namespaced_valkey.set('foo', 'bar') # valkey_connection.set('ns:foo', 'bar')
16
+ # => "OK"
17
+
18
+ # Valkey::Namespace automatically prepended our namespace to the key
19
+ # before sending it to our valkey client.
20
+
21
+ namespaced_valkey.get('foo')
22
+ # => "bar"
23
+ valkey_connection.get('foo')
24
+ # => nil
25
+ valkey_connection.get('ns:foo')
26
+ # => "bar"
27
+
28
+ namespaced_valkey.del('foo')
29
+ # => 1
30
+ namespaced_valkey.get('foo')
31
+ # => nil
32
+ valkey_connection.get('ns:foo')
33
+ # => nil
34
+ ```
35
+
36
+ Valkey::Namespace also supports `Proc` as a namespace and will take the result string as namespace at runtime.
37
+
38
+ ```ruby
39
+ valkey_connection = Valkey.new
40
+ namespaced_valkey = Valkey::Namespace.new(Proc.new { Tenant.current_tenant }, valkey: valkey_connection)
41
+ ```
42
+
43
+ Installation
44
+ ============
45
+
46
+ Valkey::Namespace is packaged as the valkey-namespace gem, and hosted on rubygems.org.
47
+
48
+ From the command line:
49
+
50
+ $ gem install valkey-namespace
51
+
52
+ Or in your Gemfile:
53
+
54
+ ```ruby
55
+ gem 'valkey-namespace'
56
+ ```
57
+
58
+ Caveats
59
+ =======
60
+
61
+ `Valkey::Namespace` provides a namespaced interface to `Valkey` by keeping an internal registry of the method signatures in `Valkey` provided by the [valkey-glide-ruby][] gem; we keep track of which arguments need the namespace added, and which return values need the namespace removed.
62
+
63
+ Blind Passthrough
64
+ -----------------
65
+ If your version of this gem doesn't know about a particular command, it can't namespace it. Historically, this has meant that Valkey::Namespace blindly passes unknown commands on to the underlying valkey connection without modification which can lead to surprising effects.
66
+
67
+ As of v1.0.0, blind passthrough has been deprecated, and the functionality will be removed entirely in 2.0.
68
+
69
+ If you come across a command that is not yet supported, please open an issue on the [issue tracker][] or submit a pull-request.
70
+
71
+ Administrative Commands
72
+ -----------------------
73
+ The effects of some valkey commands cannot be limited to a particular namespace (e.g., `FLUSHALL`, which literally truncates all databases in your valkey server, regardless of keyspace). Historically, this has meant that Valkey::Namespace intentionally passes administrative commands on to the underlying valkey connection without modification, which can lead to surprising effects.
74
+
75
+ As of v1.0.0, the direct use of administrative commands has been deprecated, and the functionality will be removed entirely in 2.0; while such commands are often useful for testing or administration, their meaning is inherently hidden when placed behind an interface that implies it will namespace everything.
76
+
77
+ The preferred way to send an administrative command is on the valkey connection itself, which is publicly exposed as `Valkey::Namespace#valkey`:
78
+
79
+ ```ruby
80
+ namespaced.valkey.flushall()
81
+ # => "OK"
82
+ ```
83
+
84
+ 2.x Planned Breaking Changes
85
+ ============================
86
+
87
+ As mentioned above, 2.0 will remove blind passthrough and the administrative command passthrough.
88
+ By default in 1.0+, deprecation warnings are present and enabled;
89
+ they can be silenced by initializing `Valkey::Namespace` with `warning: false` or by setting the `VALKEY_NAMESPACE_QUIET` environment variable.
90
+
91
+ Early opt-in
92
+ ------------
93
+
94
+ To enable testing against the 2.x interface before its release, in addition to deprecation warnings, early opt-in to these changes can be enabled by initializing `Valkey::Namespace` with `deprecations: true` or by setting the `VALKEY_NAMESPACE_DEPRECATIONS` environment variable.
95
+ This should only be done once all warnings have been addressed.
96
+
97
+ Compatibility
98
+ =============
99
+
100
+ This gem is designed to work with [valkey-glide-ruby][], which provides a drop-in replacement for redis-rb. It supports all Valkey commands and is API-compatible with Redis 6.2, 7.0, 7.1, and 7.2.
101
+
102
+ Authors
103
+ =======
104
+
105
+ This project is based on [redis-namespace][] and adapted for Valkey.
106
+
107
+ Original redis-namespace authors who contributed significantly:
108
+ - Chris Wanstrath (@defunkt)
109
+ - Ryan Biesemeyer (@yaauie)
110
+ - Steve Klabnik (@steveklabnik)
111
+ - Terence Lee (@hone)
112
+ - Eoin Coffey (@ecoffey)
113
+
114
+ valkey-namespace adaptation:
115
+ - Valkey Community
116
+
117
+ ## License
118
+
119
+ This project is licensed under the MIT License, the same as redis-namespace.
120
+ See the LICENSE file for details.
121
+
122
+ ## Acknowledgments
123
+
124
+ Special thanks to the redis-namespace project and its contributors for creating
125
+ the original implementation that this project is based on.
126
+
127
+ [Valkey]: https://valkey.io
128
+ [valkey-glide-ruby]: https://github.com/valkey-io/valkey-glide-ruby
129
+ [redis-namespace]: https://github.com/resque/redis-namespace
130
+ [issue tracker]: https://github.com/valkey-io/valkey-namespace/issues
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ class Valkey
2
+ class Namespace
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,625 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is based on redis-namespace (https://github.com/resque/redis-namespace)
4
+ # Copyright (c) 2009 Chris Wanstrath
5
+ # Adapted for Valkey by the Valkey Community
6
+ # Licensed under the MIT License
7
+
8
+ require 'valkey'
9
+ require 'valkey/namespace/version'
10
+
11
+ class Valkey
12
+ class Namespace
13
+ # The following tables define how input parameters and result
14
+ # values should be modified for the namespace.
15
+ #
16
+ # COMMANDS is a hash. Each key is the name of a command and each
17
+ # value is a two element array.
18
+ #
19
+ # The first element in the value array describes how to modify the
20
+ # arguments passed. It can be one of:
21
+ #
22
+ # nil
23
+ # Do nothing.
24
+ # :first
25
+ # Add the namespace to the first argument passed, e.g.
26
+ # GET key => GET namespace:key
27
+ # :all
28
+ # Add the namespace to all arguments passed, e.g.
29
+ # MGET key1 key2 => MGET namespace:key1 namespace:key2
30
+ # :exclude_first
31
+ # Add the namespace to all arguments but the first, e.g.
32
+ # :exclude_last
33
+ # Add the namespace to all arguments but the last, e.g.
34
+ # BLPOP key1 key2 timeout =>
35
+ # BLPOP namespace:key1 namespace:key2 timeout
36
+ # :exclude_options
37
+ # Add the namespace to all arguments, except the last argument,
38
+ # if the last argument is a hash of options.
39
+ # ZUNIONSTORE key1 2 key2 key3 WEIGHTS 2 1 =>
40
+ # ZUNIONSTORE namespace:key1 2 namespace:key2 namespace:key3 WEIGHTS 2 1
41
+ # :alternate
42
+ # Add the namespace to every other argument, e.g.
43
+ # MSET key1 value1 key2 value2 =>
44
+ # MSET namespace:key1 value1 namespace:key2 value2
45
+ # :sort
46
+ # Add namespace to first argument if it is non-nil
47
+ # Add namespace to second arg's :by and :store if second arg is a Hash
48
+ # Add namespace to each element in second arg's :get if second arg is
49
+ # a Hash; forces second arg's :get to be an Array if present.
50
+ # :eval_style
51
+ # Add namespace to each element in keys argument (via options hash or multi-args)
52
+ # :scan_style
53
+ # Add namespace to :match option, or supplies "#{namespace}:*" if not present.
54
+ #
55
+ # The second element in the value array describes how to modify
56
+ # the return value of the Valkey call. It can be one of:
57
+ #
58
+ # nil
59
+ # Do nothing.
60
+ # :all
61
+ # Add the namespace to all elements returned, e.g.
62
+ # key1 key2 => namespace:key1 namespace:key2
63
+ NAMESPACED_COMMANDS = {
64
+ "append" => [ :first ],
65
+ "bitcount" => [ :first ],
66
+ "bitfield" => [ :first ],
67
+ "bitop" => [ :exclude_first ],
68
+ "bitpos" => [ :first ],
69
+ "blpop" => [ :exclude_last, :first ],
70
+ "brpop" => [ :exclude_last, :first ],
71
+ "brpoplpush" => [ :exclude_last ],
72
+ "bzpopmin" => [ :first ],
73
+ "bzpopmax" => [ :first ],
74
+ "debug" => [ :exclude_first ],
75
+ "decr" => [ :first ],
76
+ "decrby" => [ :first ],
77
+ "del" => [ :all ],
78
+ "dump" => [ :first ],
79
+ "exists" => [ :all ],
80
+ "exists?" => [ :all ],
81
+ "expire" => [ :first ],
82
+ "expireat" => [ :first ],
83
+ "expiretime" => [ :first ],
84
+ "eval" => [ :eval_style ],
85
+ "evalsha" => [ :eval_style ],
86
+ "get" => [ :first ],
87
+ "getex" => [ :first ],
88
+ "getbit" => [ :first ],
89
+ "getrange" => [ :first ],
90
+ "getset" => [ :first ],
91
+ "hset" => [ :first ],
92
+ "hsetnx" => [ :first ],
93
+ "hget" => [ :first ],
94
+ "hincrby" => [ :first ],
95
+ "hincrbyfloat" => [ :first ],
96
+ "hmget" => [ :first ],
97
+ "hmset" => [ :first ],
98
+ "hdel" => [ :first ],
99
+ "hexists" => [ :first ],
100
+ "hlen" => [ :first ],
101
+ "hkeys" => [ :first ],
102
+ "hscan" => [ :first ],
103
+ "hscan_each" => [ :first ],
104
+ "hvals" => [ :first ],
105
+ "hgetall" => [ :first ],
106
+ "incr" => [ :first ],
107
+ "incrby" => [ :first ],
108
+ "incrbyfloat" => [ :first ],
109
+ "keys" => [ :first, :all ],
110
+ "lindex" => [ :first ],
111
+ "linsert" => [ :first ],
112
+ "llen" => [ :first ],
113
+ "lpop" => [ :first ],
114
+ "lpos" => [ :first ],
115
+ "lpush" => [ :first ],
116
+ "lpushx" => [ :first ],
117
+ "lrange" => [ :first ],
118
+ "lrem" => [ :first ],
119
+ "lset" => [ :first ],
120
+ "ltrim" => [ :first ],
121
+ "mapped_hmset" => [ :first ],
122
+ "mapped_hmget" => [ :first ],
123
+ "mapped_mget" => [ :all, :all ],
124
+ "mapped_mset" => [ :all ],
125
+ "mapped_msetnx" => [ :all ],
126
+ "mget" => [ :all ],
127
+ "monitor" => [ :monitor ],
128
+ "move" => [ :first ],
129
+ "mset" => [ :alternate ],
130
+ "msetnx" => [ :alternate ],
131
+ "object" => [ :exclude_first ],
132
+ "persist" => [ :first ],
133
+ "pexpire" => [ :first ],
134
+ "pexpireat" => [ :first ],
135
+ "pexpiretime" => [ :first ],
136
+ "pfadd" => [ :first ],
137
+ "pfcount" => [ :all ],
138
+ "pfmerge" => [ :all ],
139
+ "psetex" => [ :first ],
140
+ "psubscribe" => [ :all ],
141
+ "pttl" => [ :first ],
142
+ "publish" => [ :first ],
143
+ "punsubscribe" => [ :all ],
144
+ "rename" => [ :all ],
145
+ "renamenx" => [ :all ],
146
+ "restore" => [ :first ],
147
+ "rpop" => [ :first ],
148
+ "rpoplpush" => [ :all ],
149
+ "rpush" => [ :first ],
150
+ "rpushx" => [ :first ],
151
+ "sadd" => [ :first ],
152
+ "sadd?" => [ :first ],
153
+ "scard" => [ :first ],
154
+ "scan" => [ :scan_style, :second ],
155
+ "scan_each" => [ :scan_style, :all ],
156
+ "sdiff" => [ :all ],
157
+ "sdiffstore" => [ :all ],
158
+ "set" => [ :first ],
159
+ "setbit" => [ :first ],
160
+ "setex" => [ :first ],
161
+ "setnx" => [ :first ],
162
+ "setrange" => [ :first ],
163
+ "sinter" => [ :all ],
164
+ "sinterstore" => [ :all ],
165
+ "sismember" => [ :first ],
166
+ "smembers" => [ :first ],
167
+ "smismember" => [ :first ],
168
+ "smove" => [ :exclude_last ],
169
+ "sort" => [ :sort ],
170
+ "spop" => [ :first ],
171
+ "srandmember" => [ :first ],
172
+ "srem" => [ :first ],
173
+ "srem?" => [ :first ],
174
+ "sscan" => [ :first ],
175
+ "sscan_each" => [ :first ],
176
+ "strlen" => [ :first ],
177
+ "subscribe" => [ :all ],
178
+ "sunion" => [ :all ],
179
+ "sunionstore" => [ :all ],
180
+ "ttl" => [ :first ],
181
+ "type" => [ :first ],
182
+ "unlink" => [ :all ],
183
+ "unsubscribe" => [ :all ],
184
+ "zadd" => [ :first ],
185
+ "zcard" => [ :first ],
186
+ "zcount" => [ :first ],
187
+ "zincrby" => [ :first ],
188
+ "zinterstore" => [ :exclude_options ],
189
+ "zpopmin" => [ :first ],
190
+ "zpopmax" => [ :first ],
191
+ "zrange" => [ :first ],
192
+ "zrangebyscore" => [ :first ],
193
+ "zrangebylex" => [ :first ],
194
+ "zrank" => [ :first ],
195
+ "zrem" => [ :first ],
196
+ "zremrangebyrank" => [ :first ],
197
+ "zremrangebyscore" => [ :first ],
198
+ "zremrangebylex" => [ :first ],
199
+ "zrevrange" => [ :first ],
200
+ "zrevrangebyscore" => [ :first ],
201
+ "zrevrangebylex" => [ :first ],
202
+ "zrevrank" => [ :first ],
203
+ "zscan" => [ :first ],
204
+ "zscan_each" => [ :first ],
205
+ "zscore" => [ :first ],
206
+ "zunionstore" => [ :exclude_options ]
207
+ }
208
+ TRANSACTION_COMMANDS = {
209
+ "discard" => [],
210
+ "exec" => [],
211
+ "multi" => [],
212
+ "unwatch" => [ :all ],
213
+ "watch" => [ :all ],
214
+ }
215
+ HELPER_COMMANDS = {
216
+ "auth" => [],
217
+ "disconnect!" => [],
218
+ "close" => [],
219
+ "echo" => [],
220
+ "ping" => [],
221
+ "time" => [],
222
+ }
223
+ ADMINISTRATIVE_COMMANDS = {
224
+ "bgrewriteaof" => [],
225
+ "bgsave" => [],
226
+ "config" => [],
227
+ "dbsize" => [],
228
+ "flushall" => [],
229
+ "flushdb" => [],
230
+ "info" => [],
231
+ "lastsave" => [],
232
+ "quit" => [],
233
+ "randomkey" => [],
234
+ "save" => [],
235
+ "script" => [],
236
+ "select" => [],
237
+ "shutdown" => [],
238
+ "slaveof" => [],
239
+ }
240
+
241
+ DEPRECATED_COMMANDS = [
242
+ ADMINISTRATIVE_COMMANDS
243
+ ].compact.reduce(:merge)
244
+
245
+ COMMANDS = [
246
+ NAMESPACED_COMMANDS,
247
+ TRANSACTION_COMMANDS,
248
+ HELPER_COMMANDS,
249
+ ADMINISTRATIVE_COMMANDS,
250
+ ].compact.reduce(:merge)
251
+
252
+ # Support 1.8.7 by providing a namespaced reference to Enumerable::Enumerator
253
+ Enumerator = Enumerable::Enumerator unless defined?(::Enumerator)
254
+
255
+ attr_writer :namespace
256
+ attr_reader :valkey
257
+ attr_accessor :warning
258
+
259
+ def initialize(namespace, options = {})
260
+ @namespace = namespace
261
+ @valkey = options[:valkey] || Valkey.new
262
+ @warning = !!options.fetch(:warning) do
263
+ !ENV['VALKEY_NAMESPACE_QUIET']
264
+ end
265
+ @deprecations = !!options.fetch(:deprecations) do
266
+ ENV['VALKEY_NAMESPACE_DEPRECATIONS']
267
+ end
268
+ end
269
+
270
+ def deprecations?
271
+ @deprecations
272
+ end
273
+
274
+ def warning?
275
+ @warning
276
+ end
277
+
278
+ # Ruby defines a now deprecated type method so we need to override it here
279
+ # since it will never hit method_missing
280
+ def type(key)
281
+ call_with_namespace(:type, key)
282
+ end
283
+
284
+ alias_method :self_respond_to?, :respond_to?
285
+
286
+ # emulate Ruby 1.9+ and keep respond_to_missing? logic together.
287
+ def respond_to?(command, include_private=false)
288
+ return !deprecations? if DEPRECATED_COMMANDS.include?(command.to_s.downcase)
289
+
290
+ respond_to_missing?(command, include_private) or super
291
+ end
292
+
293
+ def keys(query = nil)
294
+ call_with_namespace(:keys, query || '*')
295
+ end
296
+
297
+ def multi(&block)
298
+ if block_given?
299
+ namespaced_block(:multi, &block)
300
+ else
301
+ call_with_namespace(:multi)
302
+ end
303
+ end
304
+
305
+ def pipelined(&block)
306
+ namespaced_block(:pipelined, &block)
307
+ end
308
+
309
+ def namespace(desired_namespace = nil)
310
+ if desired_namespace
311
+ yield Valkey::Namespace.new(desired_namespace,
312
+ :valkey => @valkey)
313
+ end
314
+
315
+ @namespace.respond_to?(:call) ? @namespace.call : @namespace
316
+ end
317
+
318
+ def full_namespace
319
+ valkey.is_a?(Namespace) ? "#{valkey.full_namespace}:#{namespace}" : namespace.to_s
320
+ end
321
+
322
+ def connection
323
+ @valkey.connection.tap { |info| info[:namespace] = namespace }
324
+ end
325
+
326
+ def exec
327
+ call_with_namespace(:exec)
328
+ end
329
+
330
+ def eval(*args)
331
+ call_with_namespace(:eval, *args)
332
+ end
333
+ ruby2_keywords(:eval) if respond_to?(:ruby2_keywords, true)
334
+
335
+ # This operation can run for a very long time if the namespace contains lots of keys!
336
+ # It should be used in tests, or when the namespace is small enough
337
+ # and you are sure you know what you are doing.
338
+ def clear
339
+ if warning?
340
+ warn("This operation can run for a very long time if the namespace contains lots of keys! " +
341
+ "It should be used in tests, or when the namespace is small enough " +
342
+ "and you are sure you know what you are doing.")
343
+ end
344
+
345
+ batch_size = 1000
346
+
347
+ if supports_scan?
348
+ cursor = "0"
349
+ begin
350
+ cursor, keys = scan(cursor, count: batch_size)
351
+ del(*keys) unless keys.empty?
352
+ end until cursor == "0"
353
+ else
354
+ all_keys = keys("*")
355
+ all_keys.each_slice(batch_size) do |keys|
356
+ del(*keys)
357
+ end
358
+ end
359
+ end
360
+
361
+ ADMINISTRATIVE_COMMANDS.keys.each do |command|
362
+ define_method(command) do |*args, &block|
363
+ raise NoMethodError if deprecations?
364
+
365
+ if warning?
366
+ warn("Passing '#{command}' command to valkey as is; " +
367
+ "administrative commands cannot be effectively namespaced " +
368
+ "and should be called on the valkey connection directly; " +
369
+ "passthrough has been deprecated and will be removed in " +
370
+ "valkey-namespace 2.0 (at #{call_site})"
371
+ )
372
+ end
373
+ call_with_namespace(command, *args, &block)
374
+ end
375
+ ruby2_keywords(command) if respond_to?(:ruby2_keywords, true)
376
+ end
377
+
378
+ COMMANDS.keys.each do |command|
379
+ next if ADMINISTRATIVE_COMMANDS.include?(command)
380
+ next if method_defined?(command)
381
+
382
+ define_method(command) do |*args, &block|
383
+ call_with_namespace(command, *args, &block)
384
+ end
385
+ ruby2_keywords(command) if respond_to?(:ruby2_keywords, true)
386
+ end
387
+
388
+ def method_missing(command, *args, &block)
389
+ normalized_command = command.to_s.downcase
390
+
391
+ if COMMANDS.include?(normalized_command)
392
+ send(normalized_command, *args, &block)
393
+ elsif @valkey.respond_to?(normalized_command) && !deprecations?
394
+ # blind passthrough is deprecated and will be removed in 2.0
395
+ # valkey-namespace does not know how to handle this command.
396
+ # Passing it to @valkey as is, where valkey-namespace shows
397
+ # a warning message if @warning is set.
398
+ if warning?
399
+ warn("Passing '#{command}' command to valkey as is; blind " +
400
+ "passthrough has been deprecated and will be removed in " +
401
+ "valkey-namespace 2.0 (at #{call_site})")
402
+ end
403
+
404
+ wrapped_send(@valkey, command, args, &block)
405
+ else
406
+ super
407
+ end
408
+ end
409
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
410
+
411
+ def inspect
412
+ "<#{self.class.name} v#{VERSION} with client "\
413
+ "for #{full_namespace}>"
414
+ end
415
+
416
+ def respond_to_missing?(command, include_all=false)
417
+ normalized_command = command.to_s.downcase
418
+
419
+ case
420
+ when COMMANDS.include?(normalized_command)
421
+ true
422
+ when !deprecations? && valkey.respond_to?(command, include_all)
423
+ true
424
+ else
425
+ defined?(super) && super
426
+ end
427
+ end
428
+
429
+ def call_with_namespace(command, *args, &block)
430
+ handling = COMMANDS[command.to_s.downcase]
431
+
432
+ if handling.nil?
433
+ fail("Valkey::Namespace does not know how to handle '#{command}'.")
434
+ end
435
+
436
+ (before, after) = handling
437
+
438
+ # Modify the local *args array in-place, no need to copy it.
439
+ args.map! {|arg| clone_args(arg)}
440
+
441
+ # Add the namespace to any parameters that are keys.
442
+ case before
443
+ when :first
444
+ args[0] = add_namespace(args[0]) if args[0]
445
+ args[-1] = ruby2_keywords_hash(args[-1]) if args[-1].is_a?(Hash)
446
+ when :all
447
+ args = add_namespace(args)
448
+ when :exclude_first
449
+ first = args.shift
450
+ args = add_namespace(args)
451
+ args.unshift(first) if first
452
+ when :exclude_last
453
+ last = args.pop unless args.length == 1
454
+ args = add_namespace(args)
455
+ args.push(last) if last
456
+ when :exclude_options
457
+ if args.last.is_a?(Hash)
458
+ last = ruby2_keywords_hash(args.pop)
459
+ args = add_namespace(args)
460
+ args.push(last)
461
+ else
462
+ args = add_namespace(args)
463
+ end
464
+ when :alternate
465
+ args = args.flatten
466
+ args.each_with_index { |a, i| args[i] = add_namespace(a) if i.even? }
467
+ when :sort
468
+ args[0] = add_namespace(args[0]) if args[0]
469
+ if args[1].is_a?(Hash)
470
+ [:by, :store].each do |key|
471
+ args[1][key] = add_namespace(args[1][key]) if args[1][key]
472
+ end
473
+
474
+ args[1][:get] = Array(args[1][:get])
475
+
476
+ args[1][:get].each_index do |i|
477
+ args[1][:get][i] = add_namespace(args[1][:get][i]) unless args[1][:get][i] == "#"
478
+ end
479
+ args[1] = ruby2_keywords_hash(args[1])
480
+ end
481
+ when :eval_style
482
+ # valkey.eval() and evalsha() can either take the form:
483
+ #
484
+ # valkey.eval(script, [key1, key2], [argv1, argv2])
485
+ #
486
+ # Or:
487
+ #
488
+ # valkey.eval(script, :keys => ['k1', 'k2'], :argv => ['arg1', 'arg2'])
489
+ #
490
+ # This is a tricky + annoying special case, where we only want the `keys`
491
+ # argument to be namespaced.
492
+ if args.last.is_a?(Hash)
493
+ args.last[:keys] = add_namespace(args.last[:keys])
494
+ else
495
+ args[1] = add_namespace(args[1])
496
+ end
497
+ when :scan_style
498
+ options = (args.last.kind_of?(Hash) ? args.pop : {})
499
+ options[:match] = add_namespace(options.fetch(:match, '*'))
500
+ args << ruby2_keywords_hash(options)
501
+
502
+ if block
503
+ original_block = block
504
+ block = proc { |key| original_block.call rem_namespace(key) }
505
+ end
506
+ end
507
+
508
+ # Dispatch the command to Valkey and store the result.
509
+ result = wrapped_send(@valkey, command, args, &block)
510
+
511
+ # Remove the namespace from results that are keys.
512
+ case after
513
+ when :all
514
+ result = rem_namespace(result)
515
+ when :first
516
+ result[0] = rem_namespace(result[0]) if result
517
+ when :second
518
+ result[1] = rem_namespace(result[1]) if result
519
+ end
520
+
521
+ result
522
+ end
523
+ ruby2_keywords(:call_with_namespace) if respond_to?(:ruby2_keywords, true)
524
+
525
+ protected
526
+
527
+ def valkey=(valkey)
528
+ @valkey = valkey
529
+ end
530
+
531
+ private
532
+
533
+ if Hash.respond_to?(:ruby2_keywords_hash)
534
+ def ruby2_keywords_hash(kwargs)
535
+ Hash.ruby2_keywords_hash(kwargs)
536
+ end
537
+ else
538
+ def ruby2_keywords_hash(kwargs)
539
+ kwargs
540
+ end
541
+ end
542
+
543
+ def wrapped_send(valkey_client, command, args = [], &block)
544
+ if valkey_client.class.name == "ConnectionPool"
545
+ valkey_client.with do |pool_connection|
546
+ pool_connection.send(command, *args, &block)
547
+ end
548
+ else
549
+ valkey_client.send(command, *args, &block)
550
+ end
551
+ end
552
+
553
+ # Avoid modifying the caller's (pass-by-reference) arguments.
554
+ def clone_args(arg)
555
+ if arg.is_a?(Array)
556
+ arg.map {|sub_arg| clone_args(sub_arg)}
557
+ elsif arg.is_a?(Hash)
558
+ Hash[arg.map {|k, v| [clone_args(k), clone_args(v)]}]
559
+ else
560
+ arg # Some objects (e.g. symbol) can't be dup'd.
561
+ end
562
+ end
563
+
564
+ def call_site
565
+ caller.reject { |l| l.start_with?(__FILE__) }.first
566
+ end
567
+
568
+ def namespaced_block(command, &block)
569
+ if block.arity == 0
570
+ wrapped_send(valkey, command, &block)
571
+ else
572
+ outer_block = proc { |r| copy = dup; copy.valkey = r; yield copy }
573
+ wrapped_send(valkey, command, &outer_block)
574
+ end
575
+ end
576
+
577
+ def add_namespace(key)
578
+ return key unless key && namespace
579
+
580
+ case key
581
+ when Array
582
+ key.map! {|k| add_namespace k}
583
+ when Hash
584
+ key.keys.each {|k| key[add_namespace(k)] = key.delete(k)}
585
+ key
586
+ else
587
+ "#{namespace}:#{key}"
588
+ end
589
+ end
590
+
591
+ def rem_namespace(key)
592
+ return key unless key && namespace
593
+
594
+ case key
595
+ when Array
596
+ key.map {|k| rem_namespace k}
597
+ when Hash
598
+ Hash[*key.map {|k, v| [ rem_namespace(k), v ]}.flatten]
599
+ when Enumerator
600
+ create_enumerator do |yielder|
601
+ key.each { |k| yielder.yield rem_namespace(k) }
602
+ end
603
+ else
604
+ key.to_s.sub(/\A#{namespace}:/, '')
605
+ end
606
+ end
607
+
608
+ def create_enumerator(&block)
609
+ # Enumerator in 1.8.7 *requires* a single argument, so we need to use
610
+ # its Generator class, which matches the block syntax of 1.9.x's
611
+ # Enumerator class.
612
+ if RUBY_VERSION.start_with?('1.8')
613
+ require 'generator' unless defined?(Generator)
614
+ Generator.new(&block).to_enum
615
+ else
616
+ Enumerator.new(&block)
617
+ end
618
+ end
619
+
620
+ def supports_scan?
621
+ # Valkey supports SCAN command (inherited from Redis 2.8.0+)
622
+ true
623
+ end
624
+ end
625
+ end
@@ -0,0 +1 @@
1
+ require 'valkey/namespace'
@@ -0,0 +1,27 @@
1
+ require 'bundler/setup'
2
+ require 'valkey-namespace'
3
+ require 'rspec/its'
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |expectations|
7
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
8
+ end
9
+
10
+ config.mock_with :rspec do |mocks|
11
+ mocks.verify_partial_doubles = true
12
+ end
13
+
14
+ config.shared_context_metadata_behavior = :apply_to_host_groups
15
+ config.filter_run_when_matching :focus
16
+ config.example_status_persistence_file_path = "spec/examples.txt"
17
+ config.disable_monkey_patching!
18
+ config.warnings = true
19
+
20
+ if config.files_to_run.one?
21
+ config.default_formatter = "doc"
22
+ end
23
+
24
+ config.profile_examples = 10
25
+ config.order = :random
26
+ Kernel.srand config.seed
27
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ describe Valkey::Namespace do
4
+ let(:valkey) { double('valkey') }
5
+ let(:namespace) { 'test' }
6
+ let(:namespaced) { Valkey::Namespace.new(namespace, valkey: valkey) }
7
+
8
+ describe '#initialize' do
9
+ it 'accepts a namespace and valkey connection' do
10
+ expect(namespaced.namespace).to eq('test')
11
+ expect(namespaced.valkey).to eq(valkey)
12
+ end
13
+
14
+ it 'creates a new Valkey connection if none provided' do
15
+ allow(Valkey).to receive(:new).and_return(valkey)
16
+ ns = Valkey::Namespace.new('test')
17
+ expect(ns.valkey).to eq(valkey)
18
+ end
19
+
20
+ it 'accepts a Proc as namespace' do
21
+ proc_namespace = Proc.new { 'dynamic' }
22
+ ns = Valkey::Namespace.new(proc_namespace, valkey: valkey)
23
+ expect(ns.namespace).to eq('dynamic')
24
+ end
25
+ end
26
+
27
+ describe '#set and #get' do
28
+ it 'namespaces the key for set' do
29
+ expect(valkey).to receive(:set).with('test:foo', 'bar')
30
+ namespaced.set('foo', 'bar')
31
+ end
32
+
33
+ it 'namespaces the key for get' do
34
+ expect(valkey).to receive(:get).with('test:foo').and_return('bar')
35
+ expect(namespaced.get('foo')).to eq('bar')
36
+ end
37
+ end
38
+
39
+ describe '#keys' do
40
+ it 'namespaces the pattern and removes namespace from results' do
41
+ expect(valkey).to receive(:keys).with('test:*').and_return(['test:foo', 'test:bar'])
42
+ expect(namespaced.keys).to eq(['foo', 'bar'])
43
+ end
44
+
45
+ it 'accepts a custom pattern' do
46
+ expect(valkey).to receive(:keys).with('test:foo*').and_return(['test:foo1', 'test:foo2'])
47
+ expect(namespaced.keys('foo*')).to eq(['foo1', 'foo2'])
48
+ end
49
+ end
50
+
51
+ describe '#del' do
52
+ it 'namespaces all keys' do
53
+ expect(valkey).to receive(:del).with('test:foo', 'test:bar').and_return(2)
54
+ expect(namespaced.del('foo', 'bar')).to eq(2)
55
+ end
56
+ end
57
+
58
+ describe '#mget' do
59
+ it 'namespaces all keys' do
60
+ expect(valkey).to receive(:mget).with('test:foo', 'test:bar').and_return(['val1', 'val2'])
61
+ expect(namespaced.mget('foo', 'bar')).to eq(['val1', 'val2'])
62
+ end
63
+ end
64
+
65
+ describe '#mset' do
66
+ it 'namespaces alternating keys' do
67
+ expect(valkey).to receive(:mset).with('test:foo', 'val1', 'test:bar', 'val2')
68
+ namespaced.mset('foo', 'val1', 'bar', 'val2')
69
+ end
70
+ end
71
+
72
+ describe '#full_namespace' do
73
+ it 'returns the namespace as string' do
74
+ expect(namespaced.full_namespace).to eq('test')
75
+ end
76
+
77
+ context 'with nested namespaces' do
78
+ it 'combines namespaces with colons' do
79
+ inner_ns = Valkey::Namespace.new('inner', valkey: namespaced)
80
+ expect(inner_ns.full_namespace).to eq('test:inner')
81
+ end
82
+ end
83
+ end
84
+
85
+ describe '#inspect' do
86
+ it 'includes version and namespace' do
87
+ expect(namespaced.inspect).to include('Valkey::Namespace')
88
+ expect(namespaced.inspect).to include('v1.0.0')
89
+ expect(namespaced.inspect).to include('test')
90
+ end
91
+ end
92
+
93
+ describe 'administrative commands' do
94
+ it 'warns when using flushall' do
95
+ allow(valkey).to receive(:flushall)
96
+ expect { namespaced.flushall }.to output(/administrative commands/).to_stderr
97
+ end
98
+
99
+ it 'raises NoMethodError when deprecations enabled' do
100
+ ns = Valkey::Namespace.new('test', valkey: valkey, deprecations: true)
101
+ expect { ns.flushall }.to raise_error(NoMethodError)
102
+ end
103
+ end
104
+
105
+ describe '#warning?' do
106
+ it 'returns true by default' do
107
+ expect(namespaced.warning?).to be true
108
+ end
109
+
110
+ it 'can be disabled via option' do
111
+ ns = Valkey::Namespace.new('test', valkey: valkey, warning: false)
112
+ expect(ns.warning?).to be false
113
+ end
114
+
115
+ it 'can be disabled via environment variable' do
116
+ ENV['VALKEY_NAMESPACE_QUIET'] = '1'
117
+ ns = Valkey::Namespace.new('test', valkey: valkey)
118
+ expect(ns.warning?).to be false
119
+ ENV.delete('VALKEY_NAMESPACE_QUIET')
120
+ end
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: valkey-namespace
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Valkey Community
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: valkey
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-its
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: connection_pool
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: |
84
+ Adds a Valkey::Namespace class which can be used to namespace calls
85
+ to Valkey. This is useful when using a single instance of Valkey with
86
+ multiple, different applications.
87
+ email:
88
+ - maintainer@example.com
89
+ executables: []
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - LICENSE
94
+ - NOTICE
95
+ - README.md
96
+ - Rakefile
97
+ - lib/valkey-namespace.rb
98
+ - lib/valkey/namespace.rb
99
+ - lib/valkey/namespace/version.rb
100
+ - spec/spec_helper.rb
101
+ - spec/valkey_spec.rb
102
+ homepage: https://github.com/valkey-io/valkey-namespace
103
+ licenses:
104
+ - MIT
105
+ metadata:
106
+ bug_tracker_uri: https://github.com/valkey-io/valkey-namespace/issues
107
+ changelog_uri: https://github.com/valkey-io/valkey-namespace/blob/master/CHANGELOG.md
108
+ documentation_uri: https://www.rubydoc.info/gems/valkey-namespace/1.0.0
109
+ source_code_uri: https://github.com/valkey-io/valkey-namespace
110
+ homepage_uri: https://github.com/valkey-io/valkey-namespace
111
+ rubygems_mfa_required: 'true'
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '2.7'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.4.19
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Namespaces Valkey commands.
131
+ test_files: []