async-redis 0.11.1 → 0.12.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.
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "async/limited_queue"
7
+ require "async/barrier"
8
+
9
+ module Async
10
+ module Redis
11
+ # Context for managing sharded subscriptions across multiple Redis cluster nodes.
12
+ # This class handles the complexity of subscribing to channels that may be distributed
13
+ # across different shards in a Redis cluster.
14
+ class ClusterSubscription
15
+ # Represents a failure in the subscription process, e.g. network issues, shard failures.
16
+ class SubscriptionError < StandardError
17
+ end
18
+
19
+ # Initialize a new shard subscription context.
20
+ # @parameter cluster_client [ClusterClient] The cluster client to use.
21
+ def initialize(cluster_client, queue: Async::LimitedQueue.new)
22
+ @cluster_client = cluster_client
23
+ @subscriptions = {}
24
+ @channels = []
25
+
26
+ @barrier = Async::Barrier.new
27
+ @queue = queue
28
+ end
29
+
30
+ # Close all shard subscriptions.
31
+ def close
32
+ if barrier = @barrier
33
+ @barrier = nil
34
+ barrier.stop
35
+ end
36
+
37
+ @subscriptions.each_value(&:close)
38
+ @subscriptions.clear
39
+ end
40
+
41
+ # Listen for the next message from any subscribed shard.
42
+ # @returns [Array] The next message response.
43
+ # @raises [SubscriptionError] If the subscription has failed for any reason.
44
+ def listen
45
+ @queue.pop
46
+ rescue => error
47
+ raise SubscriptionError, "Failed to read message!"
48
+ end
49
+
50
+ # Iterate over all messages from all subscribed shards.
51
+ # @yields {|response| ...} Block called for each message.
52
+ # @parameter response [Array] The message response.
53
+ def each
54
+ return to_enum unless block_given?
55
+
56
+ while response = self.listen
57
+ yield response
58
+ end
59
+ end
60
+
61
+ # Subscribe to additional sharded channels.
62
+ # @parameter channels [Array(String)] The channels to subscribe to.
63
+ def subscribe(channels)
64
+ slots = @cluster_client.slots_for(channels)
65
+
66
+ slots.each do |slot, channels_for_slot|
67
+ if subscription = @subscriptions[slot]
68
+ # Add to existing subscription for this shard
69
+ subscription.ssubscribe(channels_for_slot)
70
+ else
71
+ # Create new subscription for this shard
72
+ client = @cluster_client.client_for(slot)
73
+ subscription = @subscriptions[slot] = client.ssubscribe(*channels_for_slot)
74
+
75
+ @barrier.async do
76
+ # This is optimistic, in other words, subscription.listen will also fail on close.
77
+ until subscription.closed?
78
+ @queue << subscription.listen
79
+ end
80
+ ensure
81
+ # If we are exiting here for any reason OTHER than the subscription was closed, we need to re-create the subscription state:
82
+ unless subscription.closed?
83
+ @queue.close
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ @channels.concat(channels)
90
+ end
91
+
92
+ # Unsubscribe from sharded channels.
93
+ # @parameter channels [Array(String)] The channels to unsubscribe from.
94
+ def unsubscribe(channels)
95
+ slots = @cluster_client.slots_for(channels)
96
+
97
+ slots.each do |slot, channels_for_slot|
98
+ if subscription = @subscriptions[slot]
99
+ subscription.sunsubscribe(channels_for_slot)
100
+
101
+ # Remove channels from our tracking
102
+ @channels -= channels_for_slot
103
+
104
+ # Check if this shard still has channels
105
+ remaining_channels_for_slot = @channels.select {|ch| @cluster_client.slot_for(ch) == slot}
106
+
107
+ # If no channels left for this shard, close and remove it
108
+ if remaining_channels_for_slot.empty?
109
+ @subscriptions.delete(slot)
110
+ subscription.close
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Get the list of currently subscribed channels.
117
+ # @returns [Array(String)] The list of subscribed channels.
118
+ def channels
119
+ @channels.dup
120
+ end
121
+
122
+ # Get the number of active shard subscriptions.
123
+ # @returns [Integer] The number of shard connections.
124
+ def shard_count
125
+ @subscriptions.size
126
+ end
127
+ end
128
+ end
129
+ end
@@ -8,30 +8,50 @@ require "protocol/redis/methods"
8
8
 
9
9
  module Async
10
10
  module Redis
11
+ # @namespace
11
12
  module Context
13
+ # Base class for Redis command execution contexts.
12
14
  class Generic
15
+ # Initialize a new generic context.
16
+ # @parameter pool [Pool] The connection pool to use.
17
+ # @parameter arguments [Array] Additional arguments for the context.
13
18
  def initialize(pool, *arguments)
14
19
  @pool = pool
15
20
  @connection = pool.acquire
16
21
  end
17
22
 
23
+ # Close the context and release the connection back to the pool.
18
24
  def close
19
- if @connection
20
- @pool.release(@connection)
25
+ if connection = @connection
21
26
  @connection = nil
27
+ @pool.release(connection)
22
28
  end
23
29
  end
24
30
 
31
+ # @returns [Boolean] Whether the context is closed.
32
+ def closed?
33
+ @connection.nil?
34
+ end
35
+
36
+ # Write a Redis command request to the connection.
37
+ # @parameter command [String] The Redis command.
38
+ # @parameter arguments [Array] The command arguments.
25
39
  def write_request(command, *arguments)
26
40
  @connection.write_request([command, *arguments])
27
41
  end
28
42
 
43
+ # Read a response from the Redis connection.
44
+ # @returns [Object] The Redis response.
29
45
  def read_response
30
46
  @connection.flush
31
47
 
32
48
  return @connection.read_response
33
49
  end
34
50
 
51
+ # Execute a Redis command and return the response.
52
+ # @parameter command [String] The Redis command.
53
+ # @parameter arguments [Array] The command arguments.
54
+ # @returns [Object] The Redis response.
35
55
  def call(command, *arguments)
36
56
  write_request(command, *arguments)
37
57
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019, by David Ortiz.
5
- # Copyright, 2019-2024, by Samuel Williams.
5
+ # Copyright, 2019-2025, by Samuel Williams.
6
6
  # Copyright, 2022, by Tim Willard.
7
7
 
8
8
  require_relative "generic"
@@ -14,9 +14,12 @@ module Async
14
14
  class Pipeline < Generic
15
15
  include ::Protocol::Redis::Methods
16
16
 
17
+ # A synchronous wrapper for pipeline operations that executes one command at a time.
17
18
  class Sync
18
19
  include ::Protocol::Redis::Methods
19
20
 
21
+ # Initialize a new sync wrapper.
22
+ # @parameter pipeline [Pipeline] The pipeline to wrap.
20
23
  def initialize(pipeline)
21
24
  @pipeline = pipeline
22
25
  end
@@ -31,6 +34,8 @@ module Async
31
34
  end
32
35
  end
33
36
 
37
+ # Initialize a new pipeline context.
38
+ # @parameter pool [Pool] The connection pool to use.
34
39
  def initialize(pool)
35
40
  super(pool)
36
41
 
@@ -46,6 +51,9 @@ module Async
46
51
  end
47
52
  end
48
53
 
54
+ # Collect all pending responses.
55
+ # @yields {...} Optional block to execute while collecting responses.
56
+ # @returns [Array] Array of all responses if no block given.
49
57
  def collect
50
58
  if block_given?
51
59
  flush
@@ -55,6 +63,8 @@ module Async
55
63
  @count.times.map{read_response}
56
64
  end
57
65
 
66
+ # Get a synchronous wrapper for this pipeline.
67
+ # @returns [Sync] A synchronous wrapper that executes commands immediately.
58
68
  def sync
59
69
  @sync ||= Sync.new(self)
60
70
  end
@@ -73,6 +83,8 @@ module Async
73
83
  return nil
74
84
  end
75
85
 
86
+ # Read a response from the pipeline.
87
+ # @returns [Object] The next response in the pipeline.
76
88
  def read_response
77
89
  if @count > 0
78
90
  @count -= 1
@@ -82,6 +94,7 @@ module Async
82
94
  end
83
95
  end
84
96
 
97
+ # Close the pipeline and flush all pending responses.
85
98
  def close
86
99
  flush
87
100
  ensure
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2018, by Huba Nagy.
5
+ # Copyright, 2018-2025, by Samuel Williams.
6
+
7
+ require_relative "generic"
8
+
9
+ module Async
10
+ module Redis
11
+ module Context
12
+ # Context for Redis pub/sub subscription operations.
13
+ class Subscription < Generic
14
+ MESSAGE = "message"
15
+ PMESSAGE = "pmessage"
16
+ SMESSAGE = "smessage"
17
+
18
+ # Initialize a new subscription context.
19
+ # @parameter pool [Pool] The connection pool to use.
20
+ # @parameter channels [Array(String)] The channels to subscribe to.
21
+ def initialize(pool, channels)
22
+ super(pool)
23
+
24
+ subscribe(channels) if channels.any?
25
+ end
26
+
27
+ # Close the subscription context.
28
+ def close
29
+ # This causes anyone calling `#listen` to exit, as `read_response` will fail. If we decided to use `RESET` instead, we'd need to take that into account.
30
+ @connection&.close
31
+
32
+ super
33
+ end
34
+
35
+ # Listen for the next message from subscribed channels.
36
+ # @returns [Array] The next message response, or nil if connection closed.
37
+ def listen
38
+ while response = @connection.read_response
39
+ type = response.first
40
+
41
+ if type == MESSAGE || type == PMESSAGE || type == SMESSAGE
42
+ return response
43
+ end
44
+ end
45
+ end
46
+
47
+ # Iterate over all messages from subscribed channels.
48
+ # @yields {|response| ...} Block called for each message.
49
+ # @parameter response [Array] The message response.
50
+ def each
51
+ return to_enum unless block_given?
52
+
53
+ while response = self.listen
54
+ yield response
55
+ end
56
+ end
57
+
58
+ # Subscribe to additional channels.
59
+ # @parameter channels [Array(String)] The channels to subscribe to.
60
+ def subscribe(channels)
61
+ @connection.write_request ["SUBSCRIBE", *channels]
62
+ @connection.flush
63
+ end
64
+
65
+ # Unsubscribe from channels.
66
+ # @parameter channels [Array(String)] The channels to unsubscribe from.
67
+ def unsubscribe(channels)
68
+ @connection.write_request ["UNSUBSCRIBE", *channels]
69
+ @connection.flush
70
+ end
71
+
72
+ # Subscribe to channel patterns.
73
+ # @parameter patterns [Array(String)] The channel patterns to subscribe to.
74
+ def psubscribe(patterns)
75
+ @connection.write_request ["PSUBSCRIBE", *patterns]
76
+ @connection.flush
77
+ end
78
+
79
+ # Unsubscribe from channel patterns.
80
+ # @parameter patterns [Array(String)] The channel patterns to unsubscribe from.
81
+ def punsubscribe(patterns)
82
+ @connection.write_request ["PUNSUBSCRIBE", *patterns]
83
+ @connection.flush
84
+ end
85
+
86
+ # Subscribe to sharded channels (Redis 7.0+).
87
+ # @parameter channels [Array(String)] The sharded channels to subscribe to.
88
+ def ssubscribe(channels)
89
+ @connection.write_request ["SSUBSCRIBE", *channels]
90
+ @connection.flush
91
+ end
92
+
93
+ # Unsubscribe from sharded channels (Redis 7.0+).
94
+ # @parameter channels [Array(String)] The sharded channels to unsubscribe from.
95
+ def sunsubscribe(channels)
96
+ @connection.write_request ["SUNSUBSCRIBE", *channels]
97
+ @connection.flush
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -9,15 +9,22 @@ require_relative "pipeline"
9
9
  module Async
10
10
  module Redis
11
11
  module Context
12
+ # Context for Redis transaction operations using MULTI/EXEC.
12
13
  class Transaction < Pipeline
14
+ # Initialize a new transaction context.
15
+ # @parameter pool [Pool] The connection pool to use.
16
+ # @parameter arguments [Array] Additional arguments for the transaction.
13
17
  def initialize(pool, *arguments)
14
18
  super(pool)
15
19
  end
16
20
 
21
+ # Begin a transaction block.
17
22
  def multi
18
23
  call("MULTI")
19
24
  end
20
25
 
26
+ # Watch keys for changes during the transaction.
27
+ # @parameter keys [Array(String)] The keys to watch.
21
28
  def watch(*keys)
22
29
  sync.call("WATCH", *keys)
23
30
  end
@@ -27,6 +34,7 @@ module Async
27
34
  sync.call("EXEC")
28
35
  end
29
36
 
37
+ # Discard all queued commands in the transaction.
30
38
  def discard
31
39
  sync.call("DISCARD")
32
40
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require "io/endpoint"
7
7
  require "io/endpoint/host_endpoint"
@@ -13,20 +13,32 @@ require_relative "protocol/selected"
13
13
 
14
14
  module Async
15
15
  module Redis
16
+ # Create a local Redis endpoint.
17
+ # @parameter options [Hash] Options for the endpoint.
18
+ # @returns [Endpoint] A local Redis endpoint.
16
19
  def self.local_endpoint(**options)
17
20
  Endpoint.local(**options)
18
21
  end
19
22
 
20
23
  # Represents a way to connect to a remote Redis server.
21
24
  class Endpoint < ::IO::Endpoint::Generic
22
- LOCALHOST = URI.parse("redis://localhost").freeze
25
+ LOCALHOST = URI::Generic.build(scheme: "redis", host: "localhost").freeze
23
26
 
27
+ # Create a local Redis endpoint.
28
+ # @parameter options [Hash] Additional options for the endpoint.
29
+ # @returns [Endpoint] A local Redis endpoint.
24
30
  def self.local(**options)
25
31
  self.new(LOCALHOST, **options)
26
32
  end
27
33
 
34
+ # Create a remote Redis endpoint.
35
+ # @parameter host [String] The hostname to connect to.
36
+ # @parameter port [Integer] The port to connect to.
37
+ # @parameter options [Hash] Additional options for the endpoint.
38
+ # @returns [Endpoint] A remote Redis endpoint.
28
39
  def self.remote(host, port = 6379, **options)
29
- self.new(URI.parse("redis://#{host}:#{port}"), **options)
40
+ # URI::Generic.build automatically handles IPv6 addresses correctly:
41
+ self.new(URI::Generic.build(scheme: "redis", host: host, port: port), **options)
30
42
  end
31
43
 
32
44
  SCHEMES = {
@@ -34,18 +46,31 @@ module Async
34
46
  "rediss" => URI::Generic,
35
47
  }
36
48
 
49
+ # Parse a Redis URL string into an endpoint.
50
+ # @parameter string [String] The URL string to parse.
51
+ # @parameter endpoint [Endpoint] Optional underlying endpoint.
52
+ # @parameter options [Hash] Additional options for the endpoint.
53
+ # @returns [Endpoint] The parsed endpoint.
37
54
  def self.parse(string, endpoint = nil, **options)
38
- url = URI.parse(string).normalize
55
+ url = URI.parse(string)
39
56
 
40
57
  return self.new(url, endpoint, **options)
41
58
  end
42
59
 
43
60
  # Construct an endpoint with a specified scheme, hostname, optional path, and options.
61
+ # If no scheme is provided, it will be auto-detected based on SSL context.
44
62
  #
45
- # @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
63
+ # @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
46
64
  # @parameter hostname [String] The hostname to connect to (or bind to).
47
65
  # @parameter options [Hash] Additional options, passed to {#initialize}.
48
- def self.for(scheme, hostname, credentials: nil, port: nil, database: nil, **options)
66
+ def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
67
+ # Auto-detect scheme if not provided:
68
+ if default_scheme = options.delete(:scheme)
69
+ scheme ||= default_scheme
70
+ end
71
+
72
+ scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
73
+
49
74
  uri_klass = SCHEMES.fetch(scheme.downcase) do
50
75
  raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
51
76
  end
@@ -55,7 +80,13 @@ module Async
55
80
  end
56
81
 
57
82
  self.new(
58
- uri_klass.new(scheme, credentials&.join(":"), hostname, port, nil, path, nil, nil, nil).normalize,
83
+ uri_klass.build(
84
+ scheme: scheme,
85
+ userinfo: credentials&.join(":"),
86
+ host: host,
87
+ port: port,
88
+ path: path,
89
+ ),
59
90
  **options
60
91
  )
61
92
  end
@@ -92,6 +123,8 @@ module Async
92
123
  end
93
124
  end
94
125
 
126
+ # Convert the endpoint to a URL.
127
+ # @returns [URI] The URL representation of the endpoint.
95
128
  def to_url
96
129
  url = @url.dup
97
130
 
@@ -102,24 +135,34 @@ module Async
102
135
  return url
103
136
  end
104
137
 
138
+ # Convert the endpoint to a string representation.
139
+ # @returns [String] A string representation of the endpoint.
105
140
  def to_s
106
141
  "\#<#{self.class} #{self.to_url} #{@options}>"
107
142
  end
108
143
 
144
+ # Convert the endpoint to an inspectable string.
145
+ # @returns [String] An inspectable string representation of the endpoint.
109
146
  def inspect
110
147
  "\#<#{self.class} #{self.to_url} #{@options.inspect}>"
111
148
  end
112
149
 
113
150
  attr :url
114
151
 
152
+ # Get the address of the underlying endpoint.
153
+ # @returns [String] The address of the endpoint.
115
154
  def address
116
155
  endpoint.address
117
156
  end
118
157
 
158
+ # Check if the connection is secure (using TLS).
159
+ # @returns [Boolean] True if the connection uses TLS.
119
160
  def secure?
120
161
  ["rediss"].include?(self.scheme)
121
162
  end
122
163
 
164
+ # Get the protocol for this endpoint.
165
+ # @returns [Protocol] The protocol instance configured for this endpoint.
123
166
  def protocol
124
167
  protocol = @options.fetch(:protocol, Protocol::RESP2)
125
168
 
@@ -134,14 +177,20 @@ module Async
134
177
  return protocol
135
178
  end
136
179
 
180
+ # Get the default port for Redis connections.
181
+ # @returns [Integer] The default Redis port (6379).
137
182
  def default_port
138
183
  6379
139
184
  end
140
185
 
186
+ # Check if the endpoint is using the default port.
187
+ # @returns [Boolean] True if using the default port.
141
188
  def default_port?
142
189
  port == default_port
143
190
  end
144
191
 
192
+ # Get the port for this endpoint.
193
+ # @returns [Integer] The port number.
145
194
  def port
146
195
  @options[:port] || @url.port || default_port
147
196
  end
@@ -151,10 +200,14 @@ module Async
151
200
  @options[:hostname] || @url.hostname
152
201
  end
153
202
 
203
+ # Get the scheme for this endpoint.
204
+ # @returns [String] The URL scheme (e.g., "redis" or "rediss").
154
205
  def scheme
155
206
  @options[:scheme] || @url.scheme
156
207
  end
157
208
 
209
+ # Get the database number for this endpoint.
210
+ # @returns [Integer | Nil] The database number or nil if not specified.
158
211
  def database
159
212
  @options[:database] || extract_database(@url.path)
160
213
  end
@@ -165,6 +218,8 @@ module Async
165
218
  end
166
219
  end
167
220
 
221
+ # Get the credentials for authentication.
222
+ # @returns [Array(String) | Nil] The username and password credentials or nil if not specified.
168
223
  def credentials
169
224
  @options[:credentials] || extract_userinfo(@url.userinfo)
170
225
  end
@@ -179,6 +234,8 @@ module Async
179
234
  end
180
235
  end
181
236
 
237
+ # Check if the endpoint is connecting to localhost.
238
+ # @returns [Boolean] True if connecting to localhost.
182
239
  def localhost?
183
240
  @url.hostname =~ /^(.*?\.)?localhost\.?$/
184
241
  end
@@ -192,6 +249,8 @@ module Async
192
249
  end
193
250
  end
194
251
 
252
+ # Get the SSL context for secure connections.
253
+ # @returns [OpenSSL::SSL::SSLContext] The SSL context configured for this endpoint.
195
254
  def ssl_context
196
255
  @options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
197
256
  context.set_params(
@@ -200,6 +259,9 @@ module Async
200
259
  end
201
260
  end
202
261
 
262
+ # Build the underlying endpoint with optional SSL wrapping.
263
+ # @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
264
+ # @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
203
265
  def build_endpoint(endpoint = nil)
204
266
  endpoint ||= tcp_endpoint
205
267
 
@@ -215,22 +277,33 @@ module Async
215
277
  return endpoint
216
278
  end
217
279
 
280
+ # Get the underlying endpoint, building it if necessary.
281
+ # @returns [IO::Endpoint] The underlying endpoint for connections.
218
282
  def endpoint
219
283
  @endpoint ||= build_endpoint
220
284
  end
221
285
 
286
+ # Set the underlying endpoint.
287
+ # @parameter endpoint [IO::Endpoint] The endpoint to wrap and use.
222
288
  def endpoint=(endpoint)
223
289
  @endpoint = build_endpoint(endpoint)
224
290
  end
225
291
 
292
+ # Bind to the endpoint and yield the server socket.
293
+ # @parameter arguments [Array] Arguments to pass to the underlying endpoint bind method.
294
+ # @yields [IO] The bound server socket.
226
295
  def bind(*arguments, &block)
227
296
  endpoint.bind(*arguments, &block)
228
297
  end
229
298
 
299
+ # Connect to the endpoint and yield the client socket.
300
+ # @yields [IO] The connected client socket.
230
301
  def connect(&block)
231
302
  endpoint.connect(&block)
232
303
  end
233
304
 
305
+ # Iterate over each possible endpoint variation.
306
+ # @yields [Endpoint] Each endpoint variant.
234
307
  def each
235
308
  return to_enum unless block_given?
236
309
 
@@ -239,14 +312,21 @@ module Async
239
312
  end
240
313
  end
241
314
 
315
+ # Get the key for hashing and equality comparison.
316
+ # @returns [Array] The key components for this endpoint.
242
317
  def key
243
318
  [@url, @options]
244
319
  end
245
320
 
321
+ # Check if this endpoint is equal to another.
322
+ # @parameter other [Endpoint] The other endpoint to compare with.
323
+ # @returns [Boolean] True if the endpoints are equal.
246
324
  def eql? other
247
325
  self.key.eql? other.key
248
326
  end
249
327
 
328
+ # Get the hash code for this endpoint.
329
+ # @returns [Integer] The hash code based on the endpoint's key.
250
330
  def hash
251
331
  self.key.hash
252
332
  end
@@ -1,39 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Redis
8
+ # Represents a Redis key with utility methods for key manipulation.
8
9
  class Key
10
+ # Create a new Key instance.
11
+ # @parameter path [String] The key path.
12
+ # @returns [Key] A new Key instance.
9
13
  def self.[] path
10
14
  self.new(path)
11
15
  end
12
16
 
13
17
  include Comparable
14
18
 
19
+ # Initialize a new Key.
20
+ # @parameter path [String] The key path.
15
21
  def initialize(path)
16
22
  @path = path
17
23
  end
18
24
 
25
+ # Get the byte size of the key.
26
+ # @returns [Integer] The byte size of the key path.
19
27
  def size
20
28
  @path.bytesize
21
29
  end
22
30
 
23
31
  attr :path
24
32
 
33
+ # Convert the key to a string.
34
+ # @returns [String] The key path as a string.
25
35
  def to_s
26
36
  @path
27
37
  end
28
38
 
39
+ # Convert the key to a string (for String compatibility).
40
+ # @returns [String] The key path as a string.
29
41
  def to_str
30
42
  @path
31
43
  end
32
44
 
45
+ # Create a child key by appending a subkey.
46
+ # @parameter key [String] The subkey to append.
47
+ # @returns [Key] A new Key with the appended subkey.
33
48
  def [] key
34
49
  self.class.new("#{@path}:#{key}")
35
50
  end
36
51
 
52
+ # Compare this key with another key.
53
+ # @parameter other [Key] The other key to compare with.
54
+ # @returns [Integer] -1, 0, or 1 for comparison result.
37
55
  def <=> other
38
56
  @path <=> other.to_str
39
57
  end