io-endpoint 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c10e7e5b154e1ad990982315ba03565d5e880f392eb404a696c63eb3c9240cb7
4
- data.tar.gz: 37ebf4a7b0c0d7905c0e4b9d23373e1665f595637ab049163dafab7d7c9df535
3
+ metadata.gz: c519269d1e8459a607159be713100765c2e424f199a08cce97e1ba6c877b5b6a
4
+ data.tar.gz: ee2e142c5320d9412395c80098e669971b8956b43ad58252d52a85e2a97a5a97
5
5
  SHA512:
6
- metadata.gz: f8446d2169e82a34b836edaaf76b4e13409202dc1f0cebf64c80f693728bfe4dcd133dddd2ee7fd932bbc13b45e3d37b8ab2a7b928b3a39629455cd1fd9e62bf
7
- data.tar.gz: 0ca66dd6bffc9925e7e782d46234b61777d432df527b8595ed10d26655a2f4d30a0eb74e5cff54ae5ec31b21a3e94af79eb1609c546d9c2b552817937ef47adc
6
+ metadata.gz: 67ce6a415b998cd1d072a1faee146803f30de818666278846cbdf69f8f72fd8c30d0df371ec44747a2ea74a1fdd0d86f100e187ba15d17275ffbb335d29e6df5
7
+ data.tar.gz: 360a4f1ee5e162e71be467b0090e54eade25f141abde4452f6ba0e389def1398eaabfe5f76aaed4cbf333ba0c40334e014029d7fb76e13d09ff60f428ac63d9f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -17,16 +17,17 @@ module IO::Endpoint
17
17
  end
18
18
 
19
19
  def to_s
20
- "\#<#{self.class} #{@address.inspect}>"
20
+ "\#<#{self.class} address=#{@address.inspect}>"
21
21
  end
22
22
 
23
23
  attr :address
24
24
 
25
25
  # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
26
- # @yield [Socket] the bound socket
27
- # @return [Socket] the bound socket
26
+ # @yield {|socket| ...} An optional block which will be passed the socket.
27
+ # @parameter socket [Socket] The socket which has been bound.
28
+ # @return [Array(Socket)] the bound socket
28
29
  def bind(wrapper = Wrapper.default, &block)
29
- wrapper.bind(@address, **@options, &block)
30
+ [wrapper.bind(@address, **@options, &block)]
30
31
  end
31
32
 
32
33
  # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
@@ -13,7 +13,9 @@ module IO::Endpoint
13
13
  end
14
14
 
15
15
  def each(&block)
16
- @endpoints.each(&block)
16
+ @endpoints.each do |endpoint|
17
+ endpoint.each(&block)
18
+ end
17
19
  end
18
20
 
19
21
  def connect(&block)
@@ -30,13 +32,17 @@ module IO::Endpoint
30
32
  end
31
33
 
32
34
  def bind(&block)
33
- @endpoints.map(&:bind)
35
+ if block_given?
36
+ @endpoints.each do |endpoint|
37
+ endpoint.bind(&block)
38
+ end
39
+ else
40
+ @endpoints.map(&:bind).flatten.compact
41
+ end
34
42
  end
35
43
  end
36
44
 
37
- class Endpoint
38
- def self.composite(*endpoints, **options)
39
- CompositeEndpoint.new(endpoints, **options)
40
- end
45
+ def self.composite(*endpoints, **options)
46
+ CompositeEndpoint.new(endpoints, **options)
41
47
  end
42
48
  end
@@ -77,30 +77,15 @@ module IO::Endpoint
77
77
  socket, address = server.accept
78
78
 
79
79
  Fiber.schedule do
80
- yield accepted(socket), address
80
+ yield socket, address
81
81
  end
82
82
  end
83
83
  end
84
84
  end
85
85
 
86
- # Map all endpoints by invoking `#bind`.
87
- # @yield the bound wrapper.
88
- def bound
89
- wrappers = []
90
-
91
- self.each do |endpoint|
92
- wrapper = endpoint.bind
93
- wrappers << wrapper
94
-
95
- yield wrapper
96
- end
97
-
98
- return wrappers
99
- ensure
100
- wrappers.each(&:close) if $!
101
- end
102
-
103
- # 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.
86
+ # 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.\
87
+ #
88
+ # You should not use untrusted input as it may execute arbitrary code.
104
89
  #
105
90
  # @param string [String] URI as string. Scheme will decide implementation used.
106
91
  # @param options keyword arguments passed through to {#initialize}
@@ -112,13 +97,7 @@ module IO::Endpoint
112
97
  def self.parse(string, **options)
113
98
  uri = URI.parse(string)
114
99
 
115
- self.public_send(uri.scheme, uri.host, uri.port, **options)
116
- end
117
-
118
- protected
119
-
120
- def accepted(socket)
121
- socket
100
+ IO::Endpoint.public_send(uri.scheme, uri.host, uri.port, **options)
122
101
  end
123
102
  end
124
103
  end
@@ -19,25 +19,29 @@ module IO::Endpoint
19
19
  "\#<#{self.class} name=#{nodename.inspect} service=#{service.inspect} family=#{family.inspect} type=#{socktype.inspect} protocol=#{protocol.inspect} flags=#{flags.inspect}>"
20
20
  end
21
21
 
22
- def address
23
- @specification
24
- end
22
+
23
+ attr :specification
25
24
 
26
25
  def hostname
27
- @specification.first
26
+ @specification[0]
27
+ end
28
+
29
+ def service
30
+ @specification[1]
28
31
  end
29
32
 
30
33
  # Try to connect to the given host by connecting to each address in sequence until a connection is made.
31
34
  # @yield [Socket] the socket which is being connected, may be invoked more than once
32
35
  # @return [Socket] the connected socket
33
36
  # @raise if no connection could complete successfully
34
- def connect
37
+ def connect(wrapper = Wrapper.default, &block)
35
38
  last_error = nil
36
39
 
37
40
  Addrinfo.foreach(*@specification) do |address|
38
41
  begin
39
- socket = Socket.connect(address, **@options)
42
+ socket = wrapper.connect(address, **@options)
40
43
  rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN => last_error
44
+ # Try again unless if possible, otherwise raise...
41
45
  else
42
46
  return socket unless block_given?
43
47
 
@@ -55,9 +59,9 @@ module IO::Endpoint
55
59
  # Invokes the given block for every address which can be bound to.
56
60
  # @yield [Socket] the bound socket
57
61
  # @return [Array<Socket>] an array of bound sockets
58
- def bind(&block)
62
+ def bind(wrapper = Wrapper.default, &block)
59
63
  Addrinfo.foreach(*@specification).map do |address|
60
- Socket.bind(address, **@options, &block)
64
+ wrapper.bind(address, **@options, &block)
61
65
  end
62
66
  end
63
67
 
@@ -71,23 +75,23 @@ module IO::Endpoint
71
75
  end
72
76
  end
73
77
 
74
- # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
78
+ # @param arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
75
79
  # @param options keyword arguments passed on to {HostEndpoint#initialize}
76
80
  #
77
81
  # @return [HostEndpoint]
78
- def self.tcp(*args, **options)
79
- args[3] = ::Socket::SOCK_STREAM
82
+ def self.tcp(*arguments, **options)
83
+ arguments[3] = ::Socket::SOCK_STREAM
80
84
 
81
- HostEndpoint.new(args, **options)
85
+ HostEndpoint.new(arguments, **options)
82
86
  end
83
87
 
84
- # @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
88
+ # @param arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
85
89
  # @param options keyword arguments passed on to {HostEndpoint#initialize}
86
90
  #
87
91
  # @return [HostEndpoint]
88
- def self.udp(*args, **options)
89
- args[3] = ::Socket::SOCK_DGRAM
92
+ def self.udp(*arguments, **options)
93
+ arguments[3] = ::Socket::SOCK_DGRAM
90
94
 
91
- HostEndpoint.new(args, **options)
95
+ HostEndpoint.new(arguments, **options)
92
96
  end
93
97
  end
@@ -5,13 +5,18 @@
5
5
 
6
6
  require_relative 'generic'
7
7
  require_relative 'composite_endpoint'
8
+ require_relative 'socket_endpoint'
9
+
10
+ require 'openssl'
8
11
 
9
12
  module IO::Endpoint
10
13
  # Pre-connect and pre-bind sockets so that it can be used between processes.
11
14
  class SharedEndpoint < Generic
12
15
  # Create a new `SharedEndpoint` by binding to the given endpoint.
13
- def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
14
- wrappers = endpoint.bound do |server|
16
+ def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false, **options)
17
+ sockets = endpoint.bind(**options)
18
+
19
+ sockets.each do |server|
15
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.
16
21
  begin
17
22
  server.listen(backlog)
@@ -22,7 +27,7 @@ module IO::Endpoint
22
27
  server.close_on_exec = close_on_exec
23
28
  end
24
29
 
25
- return self.new(endpoint, wrappers)
30
+ return self.new(endpoint, sockets)
26
31
  end
27
32
 
28
33
  # Create a new `SharedEndpoint` by connecting to the given endpoint.
@@ -34,18 +39,20 @@ module IO::Endpoint
34
39
  return self.new(endpoint, [wrapper])
35
40
  end
36
41
 
37
- def initialize(endpoint, wrappers, **options)
42
+ def initialize(endpoint, sockets, **options)
38
43
  super(**options)
39
44
 
45
+ raise TypeError, "sockets must be an Array" unless sockets.is_a?(Array)
46
+
40
47
  @endpoint = endpoint
41
- @wrappers = wrappers
48
+ @sockets = sockets
42
49
  end
43
50
 
44
51
  attr :endpoint
45
- attr :wrappers
52
+ attr :sockets
46
53
 
47
54
  def local_address_endpoint(**options)
48
- endpoints = @wrappers.map do |wrapper|
55
+ endpoints = @sockets.map do |wrapper|
49
56
  AddressEndpoint.new(wrapper.to_io.local_address)
50
57
  end
51
58
 
@@ -53,51 +60,77 @@ module IO::Endpoint
53
60
  end
54
61
 
55
62
  def remote_address_endpoint(**options)
56
- endpoints = @wrappers.map do |wrapper|
63
+ endpoints = @sockets.map do |wrapper|
57
64
  AddressEndpoint.new(wrapper.to_io.remote_address)
58
65
  end
59
66
 
60
67
  return CompositeEndpoint.new(endpoints, **options)
61
68
  end
62
69
 
63
- # Close all the internal wrappers.
70
+ # Close all the internal sockets.
64
71
  def close
65
- @wrappers.each(&:close)
66
- @wrappers.clear
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
67
82
  end
68
83
 
69
- def bind
70
- @wrappers.each do |server|
84
+ def bind(wrapper = Wrapper.default, &block)
85
+ @sockets.each.map do |server|
71
86
  server = server.dup
72
87
 
73
- begin
74
- yield server
75
- ensure
76
- server.close
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
77
98
  end
78
99
  end
79
100
  end
80
101
 
81
- def connect
82
- @wrappers.each do |peer|
83
- peer = peer.dup
102
+ def connect(wrapper = Wrapper.default, &block)
103
+ @sockets.each do |socket|
104
+ socket = socket.dup
105
+
106
+ return socket unless block_given?
84
107
 
85
108
  begin
86
- yield peer
109
+ return yield(socket)
87
110
  ensure
88
- peer.close
111
+ socket.close
89
112
  end
90
113
  end
91
114
  end
92
115
 
93
- def accept(backlog = nil, &block)
116
+ def accept(**options, &block)
94
117
  bind do |server|
95
- server.accept_each(&block)
118
+ server.accept(&block)
96
119
  end
97
120
  end
98
121
 
99
122
  def to_s
100
- "\#<#{self.class} #{@wrappers.size} descriptors for #{@endpoint}>"
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)
101
134
  end
102
135
  end
103
136
  end
@@ -8,6 +8,38 @@ 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)
39
+ end
40
+ end
41
+ end
42
+
11
43
  module IO::Endpoint
12
44
  class SSLEndpoint < Generic
13
45
  def initialize(endpoint, **options)
@@ -41,13 +73,13 @@ module IO::Endpoint
41
73
  @options[:ssl_params]
42
74
  end
43
75
 
44
- def build_context(context = OpenSSL::SSL::SSLContext.new)
76
+ def build_context(context = ::OpenSSL::SSL::SSLContext.new)
45
77
  if params = self.params
46
78
  context.set_params(params)
47
79
  end
48
80
 
49
- context.setup
50
- context.freeze
81
+ # context.setup
82
+ # context.freeze
51
83
 
52
84
  return context
53
85
  end
@@ -62,10 +94,12 @@ module IO::Endpoint
62
94
  def bind
63
95
  if block_given?
64
96
  @endpoint.bind do |server|
65
- yield OpenSSL::SSL::SSLServer.new(server, context)
97
+ yield ::OpenSSL::SSL::SSLServer.new(server, context)
66
98
  end
67
99
  else
68
- return OpenSSL::SSL::SSLServer.new(@endpoint.bind, context)
100
+ @endpoint.bind.map do |server|
101
+ ::OpenSSL::SSL::SSLServer.new(server, context)
102
+ end
69
103
  end
70
104
  end
71
105
 
@@ -73,7 +107,7 @@ module IO::Endpoint
73
107
  # @yield [Socket] the socket which is being connected
74
108
  # @return [Socket] the connected socket
75
109
  def connect(&block)
76
- socket = OpenSSL::SSL::SSLSocket.new(@endpoint.connect, context)
110
+ socket = ::OpenSSL::SSL::SSLSocket.new(@endpoint.connect, context)
77
111
 
78
112
  if hostname = self.hostname
79
113
  socket.hostname = hostname
@@ -102,22 +136,15 @@ module IO::Endpoint
102
136
  yield self.class.new(endpoint, **@options)
103
137
  end
104
138
  end
105
-
106
- protected
107
-
108
- def accepted(socket)
109
- socket.accept
110
- socket
111
- end
112
139
  end
113
140
 
114
- # @param args
141
+ # @param arguments
115
142
  # @param ssl_context [OpenSSL::SSL::SSLContext, nil]
116
143
  # @param hostname [String, nil]
117
144
  # @param options keyword arguments passed through to {Endpoint.tcp}
118
145
  #
119
146
  # @return [SSLEndpoint]
120
- def self.ssl(*args, ssl_context: nil, hostname: nil, **options)
121
- SSLEndpoint.new(self.tcp(*args, **options), ssl_context: ssl_context, hostname: hostname)
147
+ def self.ssl(*arguments, ssl_context: nil, hostname: nil, **options)
148
+ SSLEndpoint.new(self.tcp(*arguments, **options), ssl_context: ssl_context, hostname: hostname)
122
149
  end
123
150
  end
@@ -27,14 +27,16 @@ module IO::Endpoint
27
27
  end
28
28
  rescue Errno::ECONNREFUSED
29
29
  return false
30
+ rescue Errno::ENOENT
31
+ return false
30
32
  end
31
33
 
32
34
  def bind(&block)
33
35
  super
34
36
  rescue Errno::EADDRINUSE
35
37
  # If you encounter EADDRINUSE from `bind()`, you can check if the socket is actually accepting connections by attempting to `connect()` to it. If the socket is still bound by an active process, the connection will succeed. Otherwise, it should be safe to `unlink()` the path and try again.
36
- if !bound? && File.exist?(@path)
37
- File.unlink(@path)
38
+ if !bound?
39
+ File.unlink(@path) rescue nil
38
40
  retry
39
41
  else
40
42
  raise
@@ -5,6 +5,6 @@
5
5
 
6
6
  class IO
7
7
  module Endpoint
8
- VERSION = "0.1.0"
8
+ VERSION = "0.3.0"
9
9
  end
10
10
  end
@@ -9,16 +9,32 @@ module IO::Endpoint
9
9
  class Wrapper
10
10
  include ::Socket::Constants
11
11
 
12
- if $stdin.respond_to?(:timeout=)
13
- def self.set_timeout(io, timeout)
12
+ if IO.method_defined?(:timeout=)
13
+ def set_timeout(io, timeout)
14
14
  io.timeout = timeout
15
15
  end
16
16
  else
17
- def self.set_timeout(io, timeout)
17
+ def set_timeout(io, timeout)
18
18
  warn "IO#timeout= not supported on this platform."
19
19
  end
20
20
  end
21
21
 
22
+ def set_buffered(socket, buffered)
23
+ case buffered
24
+ when true
25
+ socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 0)
26
+ when false
27
+ socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
28
+ end
29
+ rescue Errno::EINVAL
30
+ # On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result.
31
+ rescue Errno::EOPNOTSUPP
32
+ # Some platforms may simply not support the operation.
33
+ # Console.logger.warn(self) {"Unable to set sync=#{value}!"}
34
+ rescue Errno::ENOPROTOOPT
35
+ # It may not be supported by the protocol (e.g. UDP). ¯\_(ツ)_/¯
36
+ end
37
+
22
38
  def async
23
39
  raise NotImplementedError
24
40
  end
@@ -26,7 +42,9 @@ module IO::Endpoint
26
42
  # Build and wrap the underlying io.
27
43
  # @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
28
44
  # @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
29
- def build(*arguments, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil)
45
+ # @option linger [Boolean] Wait for data to be sent before closing the socket.
46
+ # @option buffered [Boolean] Enable or disable Nagle's algorithm for TCP sockets.
47
+ def build(*arguments, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil, buffered: false)
30
48
  socket = ::Socket.new(*arguments)
31
49
 
32
50
  # Set the timeout:
@@ -43,7 +61,11 @@ module IO::Endpoint
43
61
  end
44
62
 
45
63
  if linger
46
- socket.setsockopt(SOL_SOCKET, SO_LINGER, linger)
64
+ socket.setsockopt(SOL_SOCKET, SO_LINGER, 1)
65
+ end
66
+
67
+ if buffered == false
68
+ set_buffered(socket, buffered)
47
69
  end
48
70
 
49
71
  yield socket if block_given?
@@ -51,6 +73,7 @@ module IO::Endpoint
51
73
  return socket
52
74
  rescue
53
75
  socket&.close
76
+ raise
54
77
  end
55
78
 
56
79
  # Establish a connection to a given `remote_address`.
data/readme.md CHANGED
@@ -18,6 +18,14 @@ We welcome contributions to this project.
18
18
  4. Push to the branch (`git push origin my-new-feature`).
19
19
  5. Create new Pull Request.
20
20
 
21
+ ### Developer Certificate of Origin
22
+
23
+ This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
24
+
25
+ ### Contributor Covenant
26
+
27
+ This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
28
+
21
29
  ## See Also
22
30
 
23
31
  - [async-io](https://github.com/socketry/async-io) — Asynchronous IO primitives.
data.tar.gz.sig CHANGED
@@ -1 +1,3 @@
1
- ���m,�ԡ,��!FWp�$2@�A>���(�����d:y��h7D��?�WmF'���}.uQ}�ٛ����P��+[o1�ˏh2Z�Ճj�;*���, �X�$|�]T6bq����VC�m �u����h�u�KeL����yi��\�O����p$,��
1
+ (Z\��Q���^�;^Ȍ$���+��X te*9��rKg37c$=]���z+��alt��X��^bMG3���maew����d��(#���v+e��B��͸K6�+��>|9��~��*^o[GM�1UT5Sv|"0
2
+ �>���$i3���R�\��ziބg��-"� 5,��E%�3n�p��3�;�a� ���9��|G��K֥ܺ �/���8@)]������9
3
+ ���^5Am ��I����. ͈B�$^.�/8bH�r`�sYe@ �&Xj���ku���M�.=��^�Bڗ=Ʋ�)�������4�֑f��eﶔ��c33r��~�|�W
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,50 +37,8 @@ cert_chain:
37
37
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
39
  -----END CERTIFICATE-----
40
- date: 2023-06-15 00:00:00.000000000 Z
41
- dependencies:
42
- - !ruby/object:Gem::Dependency
43
- name: bake
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - ">="
47
- - !ruby/object:Gem::Version
48
- version: '0'
49
- type: :development
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- version: '0'
56
- - !ruby/object:Gem::Dependency
57
- name: covered
58
- requirement: !ruby/object:Gem::Requirement
59
- requirements:
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: '0'
63
- type: :development
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '0'
70
- - !ruby/object:Gem::Dependency
71
- name: sus
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
40
+ date: 2023-12-28 00:00:00.000000000 Z
41
+ dependencies: []
84
42
  description:
85
43
  email:
86
44
  executables: []
@@ -120,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
78
  - !ruby/object:Gem::Version
121
79
  version: '0'
122
80
  requirements: []
123
- rubygems_version: 3.4.7
81
+ rubygems_version: 3.5.3
124
82
  signing_key:
125
83
  specification_version: 4
126
84
  summary: Provides a separation of concerns interface for IO endpoints.
metadata.gz.sig CHANGED
Binary file