io-endpoint 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10203d535af830bf9ff71034262b5851ec323024a9de4db527f061cb1cd76102
4
- data.tar.gz: b150061a0414c379afb7fb5f2c142767afc5ed1822e5b106b78ece023d1cf459
3
+ metadata.gz: 204ed5172990d0df3a5d02e63b8e75d8caaf161808fea4e99b67f0d422241cc6
4
+ data.tar.gz: 7ba7e7052949565c4cb327076c493db15d6f3057493f93d4210ec5ac5bc7808b
5
5
  SHA512:
6
- metadata.gz: 82930d72359dee91627b19c771f79c9f2c6cc43dc93b9514a372a4568d1fb92c653ff2da1403c8519eddc4470dc14ef2356abe41cd202fba80bc35ba10e2d053
7
- data.tar.gz: 556e87a95eb514e643a3e6bb40468ac851cceea5da6fb6f72db37767dca18882bb9b63e61e05d2e69629a1e4eef1859c3a2b3e0797b36e0e4eb1202684f8d44c
6
+ metadata.gz: 12aa8f963bc0787de988e9efcf80fbff38a857e8130e663ed8c329402048ab9e1940dcc2637f17de2f77f43d9222914808b5ec89e91420ed0d07475dbabc9d0e
7
+ data.tar.gz: ce830974878205da1f7e91378af0bdb05f1a4017cc60715c2dca5607573992f38d97712e6b879d99ab31db95a5b12c40ab3df959da12cc56f4b2189dd37b314e
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative 'composite_endpoint'
8
+ require_relative 'address_endpoint'
9
+
10
+ module IO::Endpoint
11
+ class BoundEndpoint < Generic
12
+ def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
13
+ sockets = endpoint.bind
14
+
15
+ sockets.each do |server|
16
+ # This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
17
+ begin
18
+ server.listen(backlog)
19
+ rescue Errno::EOPNOTSUPP
20
+ # Ignore.
21
+ end
22
+
23
+ server.close_on_exec = close_on_exec
24
+ end
25
+
26
+ return self.new(endpoint, sockets, **endpoint.options)
27
+ end
28
+
29
+ def initialize(endpoint, sockets, **options)
30
+ super(**options)
31
+
32
+ @endpoint = endpoint
33
+ @sockets = sockets
34
+ end
35
+
36
+ attr :endpoint
37
+ attr :sockets
38
+
39
+ # A endpoint for the local end of the bound socket.
40
+ # @returns [CompositeEndpoint] A composite endpoint for the local end of the bound socket.
41
+ def local_address_endpoint(**options)
42
+ endpoints = @sockets.map do |socket|
43
+ AddressEndpoint.new(socket.to_io.local_address, **options)
44
+ end
45
+
46
+ return CompositeEndpoint.new(endpoints)
47
+ end
48
+
49
+ # A endpoint for the remote end of the bound socket.
50
+ # @returns [CompositeEndpoint] A composite endpoint for the remote end of the bound socket.
51
+ def remote_address_endpoint(**options)
52
+ endpoints = @sockets.map do |wrapper|
53
+ AddressEndpoint.new(socket.to_io.remote_address, **options)
54
+ end
55
+
56
+ return CompositeEndpoint.new(endpoints)
57
+ end
58
+
59
+ def close
60
+ @sockets.each(&:close)
61
+ @sockets.clear
62
+ end
63
+
64
+ def to_s
65
+ "\#<#{self.class} #{@sockets.size} bound sockets for #{@endpoint}>"
66
+ end
67
+
68
+ def bind(wrapper = Wrapper.default, &block)
69
+ @sockets.map do |server|
70
+ if block_given?
71
+ wrapper.async do
72
+ yield server
73
+ end
74
+ else
75
+ server.dup
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ class Generic
82
+ def bound(**options)
83
+ BoundEndpoint.bound(self, **options)
84
+ end
85
+ end
86
+ end
@@ -6,6 +6,7 @@
6
6
  require_relative 'generic'
7
7
 
8
8
  module IO::Endpoint
9
+ # A composite endpoint is a collection of endpoints that are used in order.
9
10
  class CompositeEndpoint < Generic
10
11
  def initialize(endpoints, **options)
11
12
  super(**options)
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'generic'
7
+ require_relative 'composite_endpoint'
8
+ require_relative 'socket_endpoint'
9
+
10
+ require 'openssl'
11
+
12
+ module IO::Endpoint
13
+ class ConnectedEndpoint < Generic
14
+ def self.connected(endpoint, close_on_exec: false)
15
+ socket = endpoint.connect
16
+
17
+ socket.close_on_exec = close_on_exec
18
+
19
+ return self.new(endpoint, socket, **endpoint.options)
20
+ end
21
+
22
+ def initialize(endpoint, socket, **options)
23
+ super(**options)
24
+
25
+ @endpoint = endpoint
26
+ @socket = socket
27
+ end
28
+
29
+ attr :endpoint
30
+ attr :socket
31
+
32
+ # A endpoint for the local end of the bound socket.
33
+ # @returns [AddressEndpoint] A endpoint for the local end of the connected socket.
34
+ def local_address_endpoint(**options)
35
+ AddressEndpoint.new(socket.to_io.local_address, **options)
36
+ end
37
+
38
+ # A endpoint for the remote end of the bound socket.
39
+ # @returns [AddressEndpoint] A endpoint for the remote end of the connected socket.
40
+ def remote_address_endpoint(**options)
41
+ AddressEndpoint.new(socket.to_io.remote_address, **options)
42
+ end
43
+
44
+ def connect(wrapper = Wrapper.default, &block)
45
+ if block_given?
46
+ yield @socket
47
+ else
48
+ return @socket.dup
49
+ end
50
+ end
51
+
52
+ def close
53
+ if @socket
54
+ @socket.close
55
+ @socket = nil
56
+ end
57
+ end
58
+
59
+ def to_s
60
+ "\#<#{self.class} #{@socket} connected for #{@endpoint}>"
61
+ end
62
+ end
63
+
64
+ class Generic
65
+ def connected(**options)
66
+ ConnectedEndpoint.connected(self, **options)
67
+ end
68
+ end
69
+ end
@@ -59,22 +59,40 @@ module IO::Endpoint
59
59
  @options[:local_address]
60
60
  end
61
61
 
62
- # Endpoints sometimes have multiple paths.
63
- # @yield [Endpoint] Enumerate all discrete paths as endpoints.
62
+ # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
63
+ # @parameter wrapper [Wrapper] The wrapper to use for binding.
64
+ # @yields {|socket| ...} An optional block which will be passed the socket.
65
+ # @parameter socket [Socket] The socket which has been bound.
66
+ # @returns [Array(Socket)] the bound socket
67
+ def bind(wrapper = Wrapper.default, &block)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
72
+ # @parameter wrapper [Wrapper] The wrapper to use for connecting.
73
+ # @return [Socket] the connected socket
74
+ def connect(wrapper = Wrapper.default, &block)
75
+ raise NotImplementedError
76
+ end
77
+
78
+ # Bind and accept connections on the given address.
79
+ # @parameter wrapper [Wrapper] The wrapper to use for accepting connections.
80
+ # @yields [Socket] The accepted socket.
81
+ def accept(wrapper = Wrapper.default, &block)
82
+ bind(wrapper) do |server|
83
+ wrapper.accept(server, **@options, &block)
84
+ end
85
+ end
86
+
87
+ # Enumerate all discrete paths as endpoints.
88
+ # @yields {|endpoint| ...} A block which will be passed each endpoint.
89
+ # @parameter endpoint [Endpoint] The endpoint.
64
90
  def each
65
91
  return to_enum unless block_given?
66
92
 
67
93
  yield self
68
94
  end
69
95
 
70
- # Accept connections from the specified endpoint.
71
- # @param backlog [Integer] the number of connections to listen for.
72
- def accept(wrapper = Wrapper.default, *arguments, **options, &block)
73
- bind(wrapper, *arguments, **options) do |server|
74
- wrapper.accept(server, &block)
75
- end
76
- end
77
-
78
96
  # Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
79
97
  #
80
98
  # You should not use untrusted input as it may execute arbitrary code.
@@ -1,136 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023, by Samuel Williams.
4
+ # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative 'generic'
7
- require_relative 'composite_endpoint'
8
- require_relative 'socket_endpoint'
9
-
10
- require 'openssl'
11
-
12
- module IO::Endpoint
13
- # Pre-connect and pre-bind sockets so that it can be used between processes.
14
- class SharedEndpoint < Generic
15
- # Create a new `SharedEndpoint` by binding to the given endpoint.
16
- def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false, **options)
17
- sockets = endpoint.bind(**options)
18
-
19
- sockets.each do |server|
20
- # This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
21
- begin
22
- server.listen(backlog)
23
- rescue Errno::EOPNOTSUPP
24
- # Ignore.
25
- end
26
-
27
- server.close_on_exec = close_on_exec
28
- end
29
-
30
- return self.new(endpoint, sockets)
31
- end
32
-
33
- # Create a new `SharedEndpoint` by connecting to the given endpoint.
34
- def self.connected(endpoint, close_on_exec: false)
35
- socket = endpoint.connect
36
-
37
- socket.close_on_exec = close_on_exec
38
-
39
- return self.new(endpoint, [socket])
40
- end
41
-
42
- def initialize(endpoint, sockets, **options)
43
- super(**options)
44
-
45
- raise TypeError, "sockets must be an Array" unless sockets.is_a?(Array)
46
-
47
- @endpoint = endpoint
48
- @sockets = sockets
49
- end
50
-
51
- attr :endpoint
52
- attr :sockets
53
-
54
- def local_address_endpoint(**options)
55
- endpoints = @sockets.map do |wrapper|
56
- AddressEndpoint.new(wrapper.to_io.local_address, **options)
57
- end
58
-
59
- return CompositeEndpoint.new(endpoints)
60
- end
61
-
62
- def remote_address_endpoint(**options)
63
- endpoints = @sockets.map do |wrapper|
64
- AddressEndpoint.new(wrapper.to_io.remote_address, **options)
65
- end
66
-
67
- return CompositeEndpoint.new(endpoints)
68
- end
69
-
70
- # Close all the internal sockets.
71
- def close
72
- @sockets.each(&:close)
73
- @sockets.clear
74
- end
75
-
76
- def each(&block)
77
- return to_enum unless block_given?
78
-
79
- @sockets.each do |socket|
80
- yield SocketEndpoint.new(socket.dup)
81
- end
82
- end
83
-
84
- def bind(wrapper = Wrapper.default, &block)
85
- @sockets.each.map do |server|
86
- server = server.dup
87
-
88
- if block_given?
89
- wrapper.async do
90
- begin
91
- yield server
92
- ensure
93
- server.close
94
- end
95
- end
96
- else
97
- server
98
- end
99
- end
100
- end
101
-
102
- def connect(wrapper = Wrapper.default, &block)
103
- @sockets.each do |socket|
104
- socket = socket.dup
105
-
106
- return socket unless block_given?
107
-
108
- begin
109
- return yield(socket)
110
- ensure
111
- socket.close
112
- end
113
- end
114
- end
115
-
116
- def accept(wrapper = Wrapper.default, &block)
117
- bind(wrapper) do |server|
118
- wrapper.accept(server, &block)
119
- end
120
- end
121
-
122
- def to_s
123
- "\#<#{self.class} #{@sockets.size} descriptors for #{@endpoint}>"
124
- end
125
- end
126
-
127
- class Generic
128
- def bound(**options)
129
- SharedEndpoint.bound(self, **options)
130
- end
131
-
132
- def connected(**options)
133
- SharedEndpoint.connected(self, **options)
134
- end
135
- end
136
- end
6
+ require_relative 'bound_endpoint'
7
+ require_relative 'connected_endpoint'
@@ -8,34 +8,52 @@ require_relative 'generic'
8
8
 
9
9
  require 'openssl'
10
10
 
11
- module OpenSSL::SSL::SocketForwarder
12
- unless method_defined?(:close_on_exec=)
13
- def close_on_exec=(value)
14
- to_io.close_on_exec = value
15
- end
16
- end
17
-
18
- unless method_defined?(:close_on_exec)
19
- def local_address
20
- to_io.local_address
21
- end
22
- end
23
-
24
- unless method_defined?(:wait)
25
- def wait(*arguments)
26
- to_io.wait(*arguments)
27
- end
28
- end
29
-
30
- unless method_defined?(:wait_readable)
31
- def wait_readable(*arguments)
32
- to_io.wait_readable(*arguments)
33
- end
34
- end
35
-
36
- unless method_defined?(:wait_writable)
37
- def wait_writable(*arguments)
38
- to_io.wait_writable(*arguments)
11
+ module OpenSSL
12
+ module SSL
13
+ module SocketForwarder
14
+ unless method_defined?(:close_on_exec=)
15
+ def close_on_exec=(value)
16
+ to_io.close_on_exec = value
17
+ end
18
+ end
19
+
20
+ unless method_defined?(:close_on_exec)
21
+ def local_address
22
+ to_io.local_address
23
+ end
24
+ end
25
+
26
+ unless method_defined?(:wait)
27
+ def wait(*arguments)
28
+ to_io.wait(*arguments)
29
+ end
30
+ end
31
+
32
+ unless method_defined?(:wait_readable)
33
+ def wait_readable(*arguments)
34
+ to_io.wait_readable(*arguments)
35
+ end
36
+ end
37
+
38
+ unless method_defined?(:wait_writable)
39
+ def wait_writable(*arguments)
40
+ to_io.wait_writable(*arguments)
41
+ end
42
+ end
43
+
44
+ if IO.method_defined?(:timeout)
45
+ unless method_defined?(:timeout)
46
+ def timeout
47
+ to_io.timeout
48
+ end
49
+ end
50
+
51
+ unless method_defined?(:timeout=)
52
+ def timeout=(value)
53
+ to_io.timeout = value
54
+ end
55
+ end
56
+ end
39
57
  end
40
58
  end
41
59
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  class IO
7
7
  module Endpoint
8
- VERSION = "0.4.0"
8
+ VERSION = "0.5.0"
9
9
  end
10
10
  end
@@ -38,50 +38,30 @@ module IO::Endpoint
38
38
  raise NotImplementedError
39
39
  end
40
40
 
41
- # Build and wrap the underlying io.
42
- # @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
43
- # @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
44
- # @option linger [Boolean] Wait for data to be sent before closing the socket.
45
- # @option buffered [Boolean] Enable or disable Nagle's algorithm for TCP sockets.
46
- def build(*arguments, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, buffered: false)
47
- socket = ::Socket.new(*arguments)
48
-
49
- # Set the timeout:
50
- if timeout
51
- set_timeout(socket, timeout)
52
- end
53
-
54
- if reuse_address
55
- socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
56
- end
57
-
58
- if reuse_port
59
- socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
60
- end
61
-
62
- if linger
63
- socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
64
- end
65
-
66
- if buffered == false
67
- set_buffered(socket, buffered)
68
- end
69
-
70
- yield socket if block_given?
71
-
72
- return socket
73
- rescue
74
- socket&.close
75
- raise
76
- end
77
-
78
41
  # Establish a connection to a given `remote_address`.
79
42
  # @example
80
43
  # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
81
- # @param remote_address [Address] The remote address to connect to.
82
- # @option local_address [Address] The local address to bind to before connecting.
83
- def connect(remote_address, local_address: nil, **options)
84
- socket = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket|
44
+ # @parameter remote_address [Address] The remote address to connect to.
45
+ # @parameter linger [Boolean] Wait for data to be sent before closing the socket.
46
+ # @parameter local_address [Address] The local address to bind to before connecting.
47
+ def connect(remote_address, local_address: nil, linger: nil, timeout: nil, buffered: false, **options)
48
+ socket = nil
49
+
50
+ begin
51
+ socket = ::Socket.new(remote_address.afamily, remote_address.socktype, remote_address.protocol)
52
+
53
+ if linger
54
+ socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
55
+ end
56
+
57
+ if buffered == false
58
+ set_buffered(socket, buffered)
59
+ end
60
+
61
+ if timeout
62
+ set_timeout(socket, timeout)
63
+ end
64
+
85
65
  if local_address
86
66
  if defined?(IP_BIND_ADDRESS_NO_PORT)
87
67
  # Inform the kernel (Linux 4.2+) to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-tuple is unique.
@@ -90,6 +70,9 @@ module IO::Endpoint
90
70
 
91
71
  socket.bind(local_address.to_sockaddr)
92
72
  end
73
+ rescue
74
+ socket&.close
75
+ raise
93
76
  end
94
77
 
95
78
  begin
@@ -108,14 +91,48 @@ module IO::Endpoint
108
91
  end
109
92
  end
110
93
 
94
+ # JRuby requires ServerSocket
95
+ if defined?(::ServerSocket)
96
+ ServerSocket = ::ServerSocket
97
+ else
98
+ ServerSocket = ::Socket
99
+ end
100
+
111
101
  # Bind to a local address.
112
102
  # @example
113
103
  # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
114
- # @param local_address [Address] The local address to bind to.
115
- # @option protocol [Integer] The socket protocol to use.
116
- def bind(local_address, protocol: 0, **options, &block)
117
- socket = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket|
104
+ # @parameter local_address [Address] The local address to bind to.
105
+ # @parameter reuse_port [Boolean] Allow this port to be bound in multiple processes.
106
+ # @parameter reuse_address [Boolean] Allow this port to be bound in multiple processes.
107
+ # @parameter linger [Boolean] Wait for data to be sent before closing the socket.
108
+ # @parameter protocol [Integer] The socket protocol to use.
109
+ def bind(local_address, protocol: 0, reuse_address: true, reuse_port: nil, linger: nil, bound_timeout: nil, **options, &block)
110
+ socket = nil
111
+
112
+ begin
113
+ socket = ServerSocket.new(local_address.afamily, local_address.socktype, protocol)
114
+
115
+ if reuse_address
116
+ socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
117
+ end
118
+
119
+ if reuse_port
120
+ socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
121
+ end
122
+
123
+ if linger
124
+ socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
125
+ end
126
+
127
+ # Set the timeout:
128
+ if bound_timeout
129
+ set_timeout(socket, bound_timeout)
130
+ end
131
+
118
132
  socket.bind(local_address.to_sockaddr)
133
+ rescue
134
+ socket&.close
135
+ raise
119
136
  end
120
137
 
121
138
  return socket unless block_given?
@@ -130,11 +147,11 @@ module IO::Endpoint
130
147
  end
131
148
 
132
149
  # Bind to a local address and accept connections in a loop.
133
- def accept(server, timeout: server.timeout, &block)
150
+ def accept(server, timeout: nil, &block)
134
151
  while true
135
152
  socket, address = server.accept
136
153
 
137
- socket.timeout = timeout if timeout != false
154
+ set_timeout(socket, timeout) if timeout != false
138
155
 
139
156
  async do
140
157
  yield socket, address
@@ -145,20 +162,26 @@ module IO::Endpoint
145
162
 
146
163
  class ThreadWrapper < Wrapper
147
164
  def async(&block)
148
- Thread.new(&block)
165
+ ::Thread.new(&block)
149
166
  end
150
167
  end
151
168
 
152
169
  class FiberWrapper < Wrapper
153
170
  def async(&block)
154
- Fiber.schedule(&block)
171
+ ::Fiber.schedule(&block)
155
172
  end
156
173
  end
157
174
 
158
- def Wrapper.default
159
- if Fiber.scheduler
160
- FiberWrapper.new
161
- else
175
+ if Fiber.respond_to?(:scheduler)
176
+ def Wrapper.default
177
+ if Fiber.scheduler
178
+ FiberWrapper.new
179
+ else
180
+ ThreadWrapper.new
181
+ end
182
+ end
183
+ else
184
+ def Wrapper.default
162
185
  ThreadWrapper.new
163
186
  end
164
187
  end
data/lib/io/endpoint.rb CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative "endpoint/version"
7
7
  require_relative "endpoint/generic"
8
+ require_relative "endpoint/shared_endpoint"
8
9
 
9
10
  module IO::Endpoint
10
11
  def self.file_descriptor_limit
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-endpoint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,7 +37,7 @@ cert_chain:
37
37
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
39
  -----END CERTIFICATE-----
40
- date: 2024-01-01 00:00:00.000000000 Z
40
+ date: 2024-01-25 00:00:00.000000000 Z
41
41
  dependencies: []
42
42
  description:
43
43
  email:
@@ -48,7 +48,9 @@ files:
48
48
  - lib/io/connected.rb
49
49
  - lib/io/endpoint.rb
50
50
  - lib/io/endpoint/address_endpoint.rb
51
+ - lib/io/endpoint/bound_endpoint.rb
51
52
  - lib/io/endpoint/composite_endpoint.rb
53
+ - lib/io/endpoint/connected_endpoint.rb
52
54
  - lib/io/endpoint/generic.rb
53
55
  - lib/io/endpoint/host_endpoint.rb
54
56
  - lib/io/endpoint/shared_endpoint.rb
@@ -79,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
81
  - !ruby/object:Gem::Version
80
82
  version: '0'
81
83
  requirements: []
82
- rubygems_version: 3.4.10
84
+ rubygems_version: 3.5.3
83
85
  signing_key:
84
86
  specification_version: 4
85
87
  summary: Provides a separation of concerns interface for IO endpoints.
metadata.gz.sig CHANGED
Binary file