io-endpoint 0.1.0 → 0.3.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: 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